mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(plugin): infer semantic roles for token maps (#3231)
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
This commit is contained in:
parent
bbf4809a7e
commit
912c7e380a
3 changed files with 332 additions and 0 deletions
81
packages/plugin-runtime/tests/official-token-map.test.ts
Normal file
81
packages/plugin-runtime/tests/official-token-map.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue