@design-parity/kit-index

中文文档
1.18.0发布于 2026/11/07Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 2767

The kit vocabulary a design map resolves variants against: walk a design kit's pages, project the referenced component sets into a committed kit index, and resolve a catalog variant knob to the exact kit node (variant axis, component property, or configur

原始使用文档
文档目录10 节
  1. The problem
  2. The pipeline
  3. Where the code side comes from
  4. Three kinds of variation — and only two are addressable
  5. The governing rule
  6. When the table cannot reach it: naming the kit's own spelli…
  7. Usage
  8. CLI
  9. Vocabulary
  10. Files

@design-parity/kit-index

The design kit's own vocabulary, committed — so a catalog that renders one component at six variants can say which kit node each of the six should be compared against, without anyone maintaining that list by hand.

The problem

A design-map.json entry binds a code component to one design node:

{ "code": "ui/Button.kt#FilledButton", "source": "figma", "ref": "figma:AbCdEf/57994:2324" }

That is exactly right for a parity diff, and not enough for a component that renders more than one variant. A catalog picturing Button at three sizes and two shapes has six renders and one ref, and nothing says which kit node the other five belong to. Enumerating them by hand is the mapping-config sprawl a design map exists to avoid — and it drifts silently the moment the kit gains a variant, because a missing entry reads as "no counterpart in the kit" rather than "nobody looked".

So this package derives them, from what the kit itself publishes.

The pipeline

  design kit
      │  dump   ── walk every page, deep
      ▼
  figma-inventory.json      disposable, megabytes
      │  build  ── keep only what design-map.json references
      │            + fetch the component properties the walk cannot see
      ▼
  figma-kit-index.json      COMMITTED, tens of kilobytes
      │  resolve ── project a code knob onto it
      ▼
  { nodeId, name }

Only the two generation steps touch the network, and both are deliberate "refresh the vocabulary" operations that land in a commit a reviewer can see. Resolution runs against the committed index, never the live kit, so a parity run reports the same thing for everyone and needs no design-tool credentials.

Where the code side comes from

For a Compose catalog, the map and its variant declarations are derived from annotations rather than authored. That half lives upstream in compose-ai-tools, because every field it reads (@CatalogComponent(reference = …), @CatalogVariant, @OverrideVariant) is defined there — and it stops at the point where a design kit's vocabulary would be needed:

  previews.json
      │  emit-design-map.mjs             compose-ai-tools: knows the ANNOTATIONS
      ├──▶ design-map.json               base refs, one per component
      └──▶ design-map-variants.json      "same component, these knobs turned" —
           (compose-preview-design-map-      unresolved, because `size=l` is a
            variants/v1)                     Compose fact and `Size=Large` is a
      │                                      kit fact
      │  design-parity-kit-index resolve  THIS PACKAGE: knows the KIT
      ▼
  design-map.json with a tagged ref/previewId pair per variant

resolve reads only committed files, so unlike dump/build it needs no credential and is safe on every build:

$ design-parity-kit-index resolve
Wrote design-map.json: 1 variant reference(s) across 1 component(s).

1 variant(s) are a component PROPERTY in the kit, not a variant beside it. A
definition node renders at the defaults, and no exact configured instance was
indexed for these values, so they remain unpaired:
  - Button/Filled / true (icon=true) — Button: `Icon` (INSTANCE_SWAP, default "54616:25409")

1 reference(s) draw optional content by default. Every render made from them
includes it, so a sticker that leaves it out is compared against something it
never claimed:
  - Button/Filled — Button: `Show icon`

Three kinds of miss are reported apart because they have different owners: a variant the kit models neither way is a real gap; a property-shaped one means the kit has the thing and a node reference just cannot ask for it; and defaulted content means the reference resolved but draws more than the code did. Rolling them together is what makes a retired pattern read as neglect.

--check turns it into a drift gate instead of a writer. A collision — two previews resolving to one node — is refused outright (exit 2, nothing written): the same node cannot be both previews' counterpart, and a map that said so would have the diff report one of the two renders as wrong.

One case is not a collision: an explicit claim. A variant whose seeds all name their kit property outright (kitAxis + kitValue, which @OverrideVariant(kitProps = …) writes) takes a cell over from a preview that only reached it by translation, whether that is the base preview or an implicit variant. That is how a kit cell drawn in a state the base preview cannot be captured in (a focused first item, say) pairs with the variant that drives it. The slot keeps its ref and tag and only its preview changes. Each claim is reported, and a second explicit claim on the same cell still collides.

Three kinds of variation — and only two are addressable

What it isAddressable?
Variant axis (Size=Large)A sibling node with its own id✅ directly
Component property (Show icon)A switch on the node⚠️ only via an instance
Slot / instance swapA region, or which sub-component nests❌ never

The middle row is the one that quietly poisons parity results. GET /v1/images renders a node at its property defaults, and a reference is a node id with nowhere to hang an override. So a kit whose Button defaults Show icon to true renders an icon+label reference for a variant whose name says only Type=Round, Size=Small — and label-only code diffed against it reports a missing icon as though the code were wrong.

The way out is not to mutate the kit. It is that somebody already placed an instance at the wanted vector, on an examples page, and that instance's node id is a renderable handle for a point in property space no definition can express. resolvePropertyInstance finds it, matching the whole property vector so a near-miss never passes as an exact one.

The third row stays unpaired on purpose. leading=icon says what the content means, never which node supplies it; guessing would produce a confident reference to the wrong thing.

The governing rule

A wrong translation must find nothing, rather than produce a confident bad reference.

Under a design-led direction a bad reference drives the code away from the kit it is copying — and does so while reporting a clean parity result, which is the worst outcome available. Every unresolved seed is surfaced as unresolved.

Surfaced with a reason, though, because "no counterpart in the kit" is true of every miss and actionable for almost none. resolve classifies each one (explainUnresolved):

ReasonWhat it meansWhat to do
… declares a kit axis/value the set does not publishA kitAxis / kitValue names something this set does not haveFix the declaration — the reason lists what the set publishes
the reference already draws thisThe base variant carries every seeded value — the render duplicates the referenceNothing. Not a gap.
each of … exists, but no node carries them togetherBoth values are real; the kit's matrix skips their intersectionNothing here — the kit would have to draw the cell
no counterpart for …These seeds have no counterpart at allThe actual lead: map the value, or accept the gap

The third column is the point. On a real catalog this turned error-unselected (state=unselected, status=error) — where state=unselected resolves perfectly well on its own — into no counterpart for status=error, and stopped two size=small renders being read as missing kit nodes when the reference is the small variant.

When the table cannot reach it: naming the kit's own spelling

Some values no translation reaches. The Material 3 kit spells one date-picker variant Type=Full-screen (range); a catalog seeding type=range resolves to nothing, and — before the reason table above — did so silently, dropping the node from the comparison. The workaround was worse than the problem: seed type=full-screen (range) in Kotlin, and a kit spelling, parentheses and all, becomes load-bearing in catalog source that will rot the next time the kit renames a value.

A seed may therefore carry the kit's own names beside its own:

resolver.resolveVariant("figma:AbCdEf/53977:33595", {
  key: "action",
  raw: "longer",              // what the Compose knob is called
  kitAxis: "Configuration",   // what the kit calls the axis
  kitValue: "Text & longer action", // …and the value
});
// → { nodeId: "53977:33576", name: "Configuration=Text & longer action, …" }

Three properties worth knowing:

  • Authoritative, not a hint. A declaration replaces the alias tables for that seed. size=l resolves fine on its own; size=l declaring kitAxis: "Sise" resolves to nothing. Falling back would make a typo indistinguishable from a correct declaration.
  • Still checked against the kit. A declared axis or value must be one the set really publishes, so the governing rule holds: nobody can declare their way to a node the kit does not draw. What they get instead is the declared reason above, naming the spelling that missed and what the set does publish.
  • Matched without punctuation or case, in any script. of lines finds the kit's # of lines; declaring an axis should not be a typing exercise. Only separators and punctuation are ignored — a kit filing its axes as サイズ and 状態 keeps them distinct, where the slug normalisation used elsewhere would erase both to nothing and match whichever was indexed first.

The same exactness applies wherever a declaration lands. A declared name reaches a component property as readily as an axis — which of the two a kit uses is its own business — but it must be the property's real name, so kitAxis: "focus" no longer passes for Show focus indicator the way the knob key focus does. On a family the kit models as folder siblings rather than a set, a declared value names the sibling outright (Middle-inset, not the Inset the near-miss search would find), and a declared axis is refused: there are no axes there to name. And a declaration on an axis another seed already claimed is checked through the same fusion rule as any other — Type=Selected plus a declared Error unselected resolves to nothing rather than silently discarding the first seed.

Either field can be given alone: an axis alone when only the name differs, a value alone when only the spelling does.

One case a declaration deliberately cannot reach: two seeds fusing onto one value in a script that writes no spaces. The fusion rule asks whether a published value says exactly what the two seeds say together, which it answers by comparing word sets — and エラー選択済み is a single token however many words a reader sees in it. Containment is the obvious substitute and is not safe: 選択 (selected) is a substring of 未選択 (unselected), so a check built on it accepts the node drawing the opposite state. Telling lexical containment from semantic fusion needs a segmenter this package has no business carrying, so such a vector stays unresolved and is reported as unresolved. Space-separated scripts fuse normally, whatever the alphabet.

That rule is why the matching is fussier than it first looks. A boolean axis accepts True from any knob, so a naive matcher had footer=true resolving to Show back=True and supporting=on resolving to Leading icon=True — both real nodes, both the wrong component. Candidate axes must therefore share a whole word with the knob or its value, matched word-for-word rather than by substring (Leading icon contains the letters of on).

Usage

import { KitIndexResolver, loadKitIndex } from "@design-parity/kit-index";

const resolver = new KitIndexResolver(await loadKitIndex("figma-kit-index.json"));

// A rendered cross-product cell — every seed must map to a distinct axis, and
// the exact resulting vector must name a real sibling.
resolver.resolveVariant("figma:AbCdEf/57994:2324", [
  { key: "size", raw: "l" },
  { key: "shape", raw: "square" },
]);
// → { nodeId: "57994:2310", name: "Type=Square, Size=Large, State=Enabled" }

// A property-shaped variant, paired with an instance configured that way.
resolver.resolveVariant("figma:AbCdEf/57994:2324", { key: "icon", raw: "false" });
// → { nodeId: "…", name: "State=Enabled (configured instance)" }  — or undefined

// What a reference draws whether or not the code asked for it.
resolver.defaultedContent("figma:AbCdEf/57994:2324");
// → [{ name: "Show icon", setName: "Button" }]

The resolver takes the index object, not a path: it is a committed artifact the caller has already read — possibly from a bundle, a cache, or a test fixture — and a resolver that read a fixed filename from the working directory could not be used twice in one process or tested without a real kit.

CLI

$ export FIGMA_TOKEN=figd_…

$ design-parity-kit-index dump  --file AbCdEf --depth 8
31 page(s) in AbCdEf
  [1/31] Buttons (11:1833): 42 component(s), 0 hidden-variant example(s), 7 configured instance(s), deepest 6
  …

$ design-parity-kit-index build --file AbCdEf
Wrote figma-kit-index.json: 55 set(s), 1284 variant(s), 6 hidden variant render
alias(es), 43 standalone component(s), 3 specimen node(s), 38 set(s) carrying
component properties, 61 configured instance render handle(s).

$ design-parity-kit-index validate
figma-kit-index.json is a valid kit index.

$ design-parity-kit-index resolve --check   # CI: fail if the committed map drifted
design-map.json is up to date.

Two steps rather than one because they fail differently: dump is the expensive walk of a whole file and its output is disposable; build is the cheap projection worth re-running while a design map is still changing. build degrades without a token — writing an index with no property vocabulary, and saying so — and validate never touches the network.

Vocabulary

The knob→axis and value→spelling tables are data, not logic, and the defaults are tuned against Material-3-shaped kits. A kit that files its variants differently supplies overrides, which merge per key so renaming one axis does not mean restating the other thirty:

new KitIndexResolver(index, {
  vocabulary: { axes: { density: ["Density", "Compactness"] } },
});

resolve reads the same shape from a kit-vocabulary.json beside the index — picked up automatically when present, or named with --vocabulary:

{
  "axes":   { "density": ["Density", "Compactness"] },
  "values": { "cosy": ["Comfortable"] }
}

That file exists so a catalog can learn one more of its kit's spellings without waiting for a release of this package. Only the tables live there — a rule about how values are matched at all (the multi-word slug match, the fused-axis search) is logic, and belongs here where it can be tested.

Files

figma-kit-index.json is generated — regenerate it, never hand-edit it. It is validated against schema/kit-index.schema.json on load, which matters more for a generated file than a hand-authored one: nobody proof-reads it, and a half-written index fails as a resolver that silently finds nothing.

版本与发布记录

@design-parity/kit-index 共发布 57 个版本,首次发布于 2026/08/15;过去 12 个月发布了 57 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.18.0latest2026/11/074 天前
1.17.02026/10/2617 天前
1.16.12026/10/1824 天前
1.16.02026/10/1726 天前
1.15.02026/10/1528 天前
1.14.12026/10/1230 天前

关于 @design-parity/kit-index 的常见问题

@design-parity/kit-index 是什么?

The kit vocabulary a design map resolves variants against: walk a design kit's pages, project the referenced component sets into a committed kit index, and resolve a catalog variant knob to the exact kit node (variant axis, component property, or configur

如何安装 @design-parity/kit-index?

在项目目录运行 npm install @design-parity/kit-index,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 design-parity-kit-index,可以用 npx design-parity-kit-index 直接运行。

@design-parity/kit-index 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@design-parity/kit-index 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@design-parity/kit-index 可以商用吗?

@design-parity/kit-index 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@design-parity/kit-index 的下载量有多少?

根据 npm 官方下载统计,@design-parity/kit-index 在 2026/10/12 – 2026/11/10 这 30 天内被下载 2,767 次,日均约 92 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@design-parity/kit-index 还在维护吗?

最新版本 1.18.0 发布于 2026/11/07(4 天前);至今共发布 57 个版本,其中过去 12 个月发布了 57 个;首次发布于 2026/08/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。