fix(plugin): infer semantic roles for token maps (#3231)

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
This commit is contained in:
chaoxiaoche 2026-05-29 11:50:56 +08:00 committed by GitHub
parent bbf4809a7e
commit 912c7e380a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 332 additions and 0 deletions

View file

@ -0,0 +1,81 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url));
const tokenMapSkill = readFileSync(
`${repoRoot}/plugins/_official/atoms/token-map/SKILL.md`,
'utf8',
);
const semanticInferenceFixture = JSON.parse(
readFileSync(
`${repoRoot}/plugins/_official/atoms/token-map/examples/semantic-inference-before-after.json`,
'utf8',
),
) as {
simulationKind: string;
sourceTokens: Array<{ source: string }>;
beforeRun: {
mapped: Array<{ source: string }>;
unmatched: Array<{ source: string }>;
coverage: { mapped: number; total: number; ratio: number };
};
afterRun: {
mapped: Array<{ source: string; target: string }>;
unmatched: Array<{ source: string; reason: string }>;
coverage: { mapped: number; total: number; ratio: number };
reasoningTrace: Array<{ source: string; semanticEvidence: string[] }>;
};
};
describe('official token-map prompt fragment', () => {
it('requires semantic inference for anonymous Figma tokens', () => {
expect(tokenMapSkill).toContain('## Semantic token inference');
expect(tokenMapSkill).toContain('color-3');
expect(tokenMapSkill).toContain('Node path, component name');
expect(tokenMapSkill).toContain('focus-ring');
expect(tokenMapSkill).toContain('existing `no-target-equivalent` reason');
});
it('documents the before and after expectation without claiming measured lift', () => {
expect(tokenMapSkill).toContain('### Before / after expectation');
expect(tokenMapSkill).toContain('no-target-equivalent');
expect(tokenMapSkill).toContain('Selected tab indicator');
expect(tokenMapSkill).toContain('does not claim a measured accuracy lift');
expect(tokenMapSkill).toContain('Real accuracy numbers require a fixture suite');
});
it('includes a same-token-batch before and after simulation fixture', () => {
expect(tokenMapSkill).toContain('examples/semantic-inference-before-after.json');
expect(semanticInferenceFixture.simulationKind).toBe('deterministic-fixture');
const sources = new Set(semanticInferenceFixture.sourceTokens.map((token) => token.source));
const beforeSources = new Set([
...semanticInferenceFixture.beforeRun.mapped.map((token) => token.source),
...semanticInferenceFixture.beforeRun.unmatched.map((token) => token.source),
]);
const afterSources = new Set([
...semanticInferenceFixture.afterRun.mapped.map((token) => token.source),
...semanticInferenceFixture.afterRun.unmatched.map((token) => token.source),
]);
expect(beforeSources).toEqual(sources);
expect(afterSources).toEqual(sources);
expect(semanticInferenceFixture.beforeRun.coverage).toEqual({ mapped: 0, total: 8, ratio: 0 });
expect(semanticInferenceFixture.afterRun.coverage).toEqual({
mapped: 7,
total: 8,
ratio: 0.875,
});
expect(
semanticInferenceFixture.afterRun.reasoningTrace.every(
(token) => token.semanticEvidence.length > 0,
),
).toBe(true);
expect(
semanticInferenceFixture.afterRun.unmatched.every(
(token) => token.reason === 'no-target-equivalent',
),
).toBe(true);
});
});

View file

@ -35,6 +35,93 @@ project-cwd/
`unmatched.json` is the audit list a human reviews; the agent must
not invent target tokens silently.
## Semantic token inference
Figma often exports anonymous source names such as `color-3`,
`paint/17`, or raw `#5B8DEF`. Do not ask the user to rename those
before mapping. First infer the semantic role from usage evidence:
- Node path, component name, instance overrides, variant/state labels,
frame name, layer name, and nearby text such as `Primary`,
`Selected`, `Link`, `Error`, `Focus`, `Nav`, `Button`, or `CTA`.
- CSS-like position in the rendered tree: background fill, foreground
text/icon, border, divider, overlay, shadow tint, focus ring, status
badge, chart series, or brand/accent treatment.
- Contrast relationships: a color paired repeatedly with the main
canvas is likely foreground; one paired with foreground inside CTA
components is likely primary/accent background; a thin outline around
interactive elements is likely border or focus-ring.
- Reuse topology: a value that appears across primary buttons,
selected tabs, and active nav items is stronger evidence for
`--ds-color-primary` than a value that appears once in an illustration.
Use that role evidence to choose among existing active design-system
tokens and to decide whether an anonymous token should be renamed or
left unmatched before the executable mapping pass. Keep the on-disk
token-map contract unchanged: the atom still writes the existing bucket
files, `unmatched.json`, and `meta.json` only.
For example, this is a useful reasoning note for deciding whether
`color-3` should map to the active primary token:
```jsonc
{
"source": "color-3",
"value": "#5B8DEF",
"role": "primary",
"targetCandidates": ["--ds-color-primary", "--ds-color-link"],
"evidence": [
"Button/Primary fill",
"Selected tab indicator",
"Link text in Settings frame"
]
}
```
Then map to an active design-system token only when the evidence is
role-based, not value-only. If the top candidates are too close to
call, or if the evidence points to conflicting roles (`primary` vs
`link` vs `focus-ring`), leave the source token unmatched using the
existing `no-target-equivalent` reason and include the competing
candidates in the hint. This keeps automation useful for common
anonymous-token cases while preserving human review for ambiguous
brand decisions.
### Before / after expectation
Without semantic inference, an anonymous Figma token can only produce
an uncertain value-level mapping:
```jsonc
{
"source": "color-3",
"value": "#5B8DEF",
"target": null,
"reason": "no-target-equivalent"
}
```
With semantic inference, the same token should carry role evidence
before it is accepted:
```jsonc
{
"source": "color-3",
"value": "#5B8DEF",
"target": "--ds-color-primary",
"via": "name"
}
```
This prompt-only v1 atom does not claim a measured accuracy lift by
itself. Treat the expected improvement as coverage of previously
manual anonymous-token cases when the Figma tree contains enough role
evidence. Real accuracy numbers require a fixture suite with known
source tokens, expected semantic roles, and a before/after agent run.
See `examples/semantic-inference-before-after.json` for a deterministic
same-token-batch simulation that compares the old value-level output
with the semantic inference output.
## Convergence
The atom completes when every input token is either mapped or

View file

@ -0,0 +1,164 @@
{
"simulationKind": "deterministic-fixture",
"notes": [
"This is not a measured production benchmark.",
"It simulates the same anonymous Figma token batch under the old value-level prompt and the semantic inference prompt.",
"The after run keeps the existing executable token-map disk contract: bucket matches plus unmatched.json/meta.json, with no inferred.json or new unmatched reason."
],
"sourceTokens": [
{
"source": "color-1",
"value": "#FFFFFF",
"usage": ["App Shell/Canvas fill", "Card/Default fill", "Modal/Surface fill"]
},
{
"source": "color-2",
"value": "#111827",
"usage": ["Body text", "Icon/Default foreground", "Table cell text"]
},
{
"source": "color-3",
"value": "#5B8DEF",
"usage": ["Button/Primary fill", "Selected tab indicator", "Active nav item"]
},
{
"source": "color-4",
"value": "#5B8DEF",
"usage": ["Link text in Settings frame", "Inline help link", "Docs link"]
},
{
"source": "color-5",
"value": "#D0D5DD",
"usage": ["Input/Default border", "Divider line", "Table row separator"]
},
{
"source": "paint-17",
"value": "#84CAFF",
"usage": ["Button/Focus outline", "Input/Focus ring", "Menu item focus halo"]
},
{
"source": "color-6",
"value": "#E5484D",
"usage": ["Error message text", "Destructive button fill", "Alert/Error icon"]
},
{
"source": "color-7",
"value": "#F59E0B",
"usage": ["Chart series 2", "Illustration accent", "One-off metric badge"]
}
],
"targetDesignSystemTokens": [
"--ds-color-surface",
"--ds-color-fg",
"--ds-color-primary",
"--ds-color-link",
"--ds-color-border",
"--ds-color-focus-ring",
"--ds-color-danger"
],
"beforeRun": {
"promptMode": "value-level-token-map",
"mapped": [],
"unmatched": [
{ "source": "color-1", "reason": "no-target-equivalent" },
{ "source": "color-2", "reason": "no-target-equivalent" },
{ "source": "color-3", "reason": "no-target-equivalent" },
{ "source": "color-4", "reason": "no-target-equivalent" },
{ "source": "color-5", "reason": "no-target-equivalent" },
{ "source": "paint-17", "reason": "no-target-equivalent" },
{ "source": "color-6", "reason": "no-target-equivalent" },
{ "source": "color-7", "reason": "no-target-equivalent" }
],
"coverage": {
"mapped": 0,
"total": 8,
"ratio": 0
}
},
"afterRun": {
"promptMode": "semantic-token-inference",
"mapped": [
{
"source": "color-1",
"target": "--ds-color-surface",
"via": "name"
},
{
"source": "color-2",
"target": "--ds-color-fg",
"via": "name"
},
{
"source": "color-3",
"target": "--ds-color-primary",
"via": "name"
},
{
"source": "color-4",
"target": "--ds-color-link",
"via": "name"
},
{
"source": "color-5",
"target": "--ds-color-border",
"via": "name"
},
{
"source": "paint-17",
"target": "--ds-color-focus-ring",
"via": "name"
},
{
"source": "color-6",
"target": "--ds-color-danger",
"via": "name"
}
],
"unmatched": [
{
"source": "color-7",
"reason": "no-target-equivalent",
"hint": "ambiguous semantic role; candidates: chart-series, illustration-accent, warning"
}
],
"coverage": {
"mapped": 7,
"total": 8,
"ratio": 0.875
},
"reasoningTrace": [
{
"source": "color-1",
"semanticEvidence": ["App Shell/Canvas fill", "Card/Default fill", "Modal/Surface fill"]
},
{
"source": "color-2",
"semanticEvidence": ["Body text", "Icon/Default foreground", "Table cell text"]
},
{
"source": "color-3",
"semanticEvidence": ["Button/Primary fill", "Selected tab indicator", "Active nav item"]
},
{
"source": "color-4",
"semanticEvidence": ["Link text in Settings frame", "Inline help link", "Docs link"]
},
{
"source": "color-5",
"semanticEvidence": ["Input/Default border", "Divider line", "Table row separator"]
},
{
"source": "paint-17",
"semanticEvidence": ["Button/Focus outline", "Input/Focus ring", "Menu item focus halo"]
},
{
"source": "color-6",
"semanticEvidence": ["Error message text", "Destructive button fill", "Alert/Error icon"]
},
{
"source": "color-7",
"semanticEvidence": ["Chart series 2", "Illustration accent", "One-off metric badge"]
}
]
}
}