feat(daemon): consume component manifests (#2053)

* feat(design-systems): extract component manifests

* feat(daemon): consume component manifests

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
This commit is contained in:
chaoxiaoche 2026-05-18 16:50:52 +08:00 committed by GitHub
parent 46a64edce3
commit 1f66c53203
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 221 additions and 15 deletions

View file

@ -6,6 +6,11 @@
import { readdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import {
extractComponentsManifest,
summarizeComponentsManifestForPrompt,
} from '@open-design/contracts';
export type DesignSystemSurface = 'web' | 'image' | 'video' | 'audio';
export type DesignSystemSummary = {
@ -73,10 +78,14 @@ export async function readDesignSystem(root: string, id: string): Promise<string
*
* - `tokensCss` verbatim content of `<brand>/tokens.css`.
* - `fixtureHtml` verbatim content of `<brand>/components.html`.
* - `componentsManifest` concise summary derived from components.html
* for prompt injection; when absent, callers
* can fall back to `fixtureHtml`.
*/
export type DesignSystemAssets = {
tokensCss?: string | undefined;
fixtureHtml?: string | undefined;
componentsManifest?: string | undefined;
};
export async function readDesignSystemAssets(
@ -87,7 +96,7 @@ export async function readDesignSystemAssets(
readFileOptional(path.join(root, id, 'tokens.css')),
readFileOptional(path.join(root, id, 'components.html')),
]);
return { tokensCss, fixtureHtml };
return withComponentsManifest(id, { tokensCss, fixtureHtml });
}
/**
@ -154,10 +163,42 @@ export async function resolveDesignSystemAssets(
}
const userInstalled = await readDesignSystemAssets(userInstalledRoot, designSystemId);
return {
return withComponentsManifest(designSystemId, {
tokensCss: builtIn.tokensCss ?? userInstalled.tokensCss,
fixtureHtml: builtIn.fixtureHtml ?? userInstalled.fixtureHtml,
};
});
}
function withComponentsManifest(
designSystemId: string,
assets: Pick<DesignSystemAssets, 'tokensCss' | 'fixtureHtml'>,
): DesignSystemAssets {
const componentsManifest = buildComponentsManifestSummary(
designSystemId,
assets.fixtureHtml,
assets.tokensCss,
);
return { ...assets, componentsManifest };
}
function buildComponentsManifestSummary(
designSystemId: string,
fixtureHtml: string | undefined,
tokensCss: string | undefined,
): string | undefined {
if (fixtureHtml === undefined || fixtureHtml.trim().length === 0) {
return undefined;
}
try {
const manifest =
tokensCss === undefined
? extractComponentsManifest({ brandId: designSystemId, fixtureHtml })
: extractComponentsManifest({ brandId: designSystemId, fixtureHtml, tokensCss });
return summarizeComponentsManifestForPrompt(manifest);
} catch {
return undefined;
}
}
async function readFileOptional(file: string): Promise<string | undefined> {

View file

@ -185,10 +185,13 @@ export interface ComposeInput {
// - `designSystemTokensCss` — verbatim `tokens.css` :root contract
// that the agent pastes into the
// artifact's <style>.
// - `designSystemFixtureHtml` — verbatim `components.html` reference
// fixture demonstrating button / card /
// type-scale shapes wired to the tokens.
// - `designSystemComponentsManifest` — concise structured summary
// derived from components.html.
// - `designSystemFixtureHtml` — verbatim `components.html`
// fallback when no manifest can
// be derived.
designSystemTokensCss?: string | undefined;
designSystemComponentsManifest?: string | undefined;
designSystemFixtureHtml?: string | undefined;
// Craft references the active skill opted into via `od.craft.requires`.
// The daemon resolves the slug list to file contents and concatenates
@ -271,6 +274,7 @@ export function composeSystemPrompt({
designSystemBody,
designSystemTitle,
designSystemTokensCss,
designSystemComponentsManifest,
designSystemFixtureHtml,
craftBody,
craftSections,
@ -348,18 +352,23 @@ export function composeSystemPrompt({
// sets voice and intent; the tokens.css block below is the SAME
// contract in machine-readable form — names + values the agent pastes
// verbatim instead of re-deriving from prose. The components.html
// fixture grounds the token vocabulary in worked component shapes
// (button / card / type roles) so the agent can copy fragments
// directly. Both blocks are individually gated: missing files (today,
// every brand except `default` and `kami`) skip silently, preserving
// the legacy DESIGN.md-only behaviour for the other ~138 brands.
// manifest grounds the token vocabulary in worked component shapes
// (button / card / type roles) without injecting the full HTML fixture.
// If manifest extraction fails or is unavailable, the composer falls
// back to the verbatim components.html fixture. Both blocks are
// individually gated: missing files skip silently, preserving the
// legacy DESIGN.md-only behaviour for prose-only brands.
if (designSystemTokensCss && designSystemTokensCss.trim().length > 0) {
parts.push(
`\n\n## Active design system tokens${designSystemTitle ? `${designSystemTitle}` : ''}\n\nThe block below is this brand's tokens.css contract — every \`:root\` custom property and any scoped override (e.g. \`:root[lang=...]\`) the brand defines. **Paste the unscoped \`:root { ... }\` block verbatim into the artifact's first \`<style>\`** so every \`var(--*)\` reference resolves at runtime.\n\nDo not invent new tokens. Do not redefine these values. Do not write raw hex outside this :root block. The DESIGN.md above is prose; this is the binding contract.\n\n\`\`\`css\n${designSystemTokensCss.trim()}\n\`\`\``,
);
}
if (designSystemFixtureHtml && designSystemFixtureHtml.trim().length > 0) {
if (designSystemComponentsManifest && designSystemComponentsManifest.trim().length > 0) {
parts.push(
`\n\n## Reference component manifest${designSystemTitle ? `${designSystemTitle}` : ''}\n\nA compact structured summary derived from this brand's components.html fixture. Use it as the component inventory for generated artifacts: match the listed selectors, component groups, class names, token references, focus behavior, and spacing cadence. Prefer these manifest entries over inventing new component shapes.\n\n\`\`\`text\n${designSystemComponentsManifest.trim()}\n\`\`\``,
);
} else if (designSystemFixtureHtml && designSystemFixtureHtml.trim().length > 0) {
parts.push(
`\n\n## Reference fixture${designSystemTitle ? `${designSystemTitle}` : ''}\n\nA self-contained worked artifact in this design system. Match its component shapes (button structure, card structure, type-scale rhythm, focus ring, spacing cadence) when generating new artifacts. Copying fragments is encouraged as long as you keep the \`var(--*)\` references intact — they are already wired to the tokens above.\n\n\`\`\`html\n${designSystemFixtureHtml.trim()}\n\`\`\``,
);

View file

@ -7759,7 +7759,8 @@ export async function startServer({
let designSystemBody;
let designSystemTitle;
// Compiled (tokens.css + components.html) form of the active brand.
// Compiled (tokens.css + components manifest / components.html)
// form of the active brand.
// Default-on as of PR-D — every chat that picks a brand with
// `tokens.css` + `components.html` siblings (today: `default` and
// `kami`; every other brand falls through silently because the
@ -7772,6 +7773,7 @@ export async function startServer({
// `true`, etc.) keeps the new default. Drift on prose-only brands
// is pinned by `scripts/check-design-system-flag-parity.ts`.
let designSystemTokensCss;
let designSystemComponentsManifest;
let designSystemFixtureHtml;
if (effectiveDesignSystemId) {
const systems = await listAllDesignSystems();
@ -7791,6 +7793,7 @@ export async function startServer({
USER_DESIGN_SYSTEMS_DIR,
);
designSystemTokensCss = assets.tokensCss;
designSystemComponentsManifest = assets.componentsManifest;
designSystemFixtureHtml = assets.fixtureHtml;
}
@ -7946,6 +7949,7 @@ export async function startServer({
designSystemBody,
designSystemTitle,
designSystemTokensCss,
designSystemComponentsManifest,
designSystemFixtureHtml,
craftBody,
craftSections,

View file

@ -43,6 +43,7 @@ describe('readDesignSystemAssets', () => {
const assets = await readDesignSystemAssets(root, 'sample');
expect(assets.tokensCss).toContain('--bg: #fff');
expect(assets.fixtureHtml).toContain('fixture');
expect(assets.componentsManifest).toContain('components.manifest schema v1 for sample');
});
it('returns the single field that exists when its sibling is missing (per-file independence)', async () => {
@ -60,6 +61,7 @@ describe('readDesignSystemAssets', () => {
const fixtureOnly = await readDesignSystemAssets(root, 'fixture-only');
expect(fixtureOnly.tokensCss).toBeUndefined();
expect(fixtureOnly.fixtureHtml).toBe('<p>only</p>');
expect(fixtureOnly.componentsManifest).toContain('components.manifest schema v1 for fixture-only');
});
it('returns an empty object when the brand directory has neither file', async () => {
@ -179,6 +181,7 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
const assets = await resolveDesignSystemAssets('sample', builtInRoot, userRoot, {});
expect(assets.tokensCss).toBe(':root { --bg: #fff; }');
expect(assets.fixtureHtml).toBe('<button>btn</button>');
expect(assets.componentsManifest).toContain('Buttons and calls to action');
});
it('returns empty (kill switch) when OD_DESIGN_TOKEN_CHANNEL is `0`, even if files are on disk', async () => {
@ -193,6 +196,7 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
});
expect(assets.tokensCss).toBeUndefined();
expect(assets.fixtureHtml).toBeUndefined();
expect(assets.componentsManifest).toBeUndefined();
});
it('still returns the assets under the legacy explicit opt-in `OD_DESIGN_TOKEN_CHANNEL=1`', async () => {
@ -207,6 +211,7 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
});
expect(assets.tokensCss).toContain('--bg: #fff');
expect(assets.fixtureHtml).toContain('<button>');
expect(assets.componentsManifest).toContain('Buttons and calls to action');
});
it('falls back to user-installed root for files missing in built-in (per-file independence)', async () => {
@ -220,6 +225,7 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
const assets = await resolveDesignSystemAssets('split', builtInRoot, userRoot, {});
expect(assets.tokensCss).toBe(':root { --bg: built-in; }');
expect(assets.fixtureHtml).toBe('<from-user-installed/>');
expect(assets.componentsManifest).toContain('components.manifest schema v1 for split');
});
it('returns the built-in assets verbatim when both files are present built-in (skips the user-installed roundtrip)', async () => {
@ -237,6 +243,7 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
const assets = await resolveDesignSystemAssets('sample', builtInRoot, userRoot, {});
expect(assets.tokensCss).toBe(':root { --bg: built-in; }');
expect(assets.fixtureHtml).toBe('<from-built-in/>');
expect(assets.componentsManifest).toContain('components.manifest schema v1 for sample');
});
it('returns undefined for both fields when the brand ships neither file in either root (legacy ~138-brand fallback)', async () => {
@ -247,6 +254,7 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
const assets = await resolveDesignSystemAssets('prose-only', builtInRoot, userRoot, {});
expect(assets.tokensCss).toBeUndefined();
expect(assets.fixtureHtml).toBeUndefined();
expect(assets.componentsManifest).toBeUndefined();
});
it('returns undefined for both fields when the brand directory does not exist in either root', async () => {
@ -256,5 +264,6 @@ describe('resolveDesignSystemAssets (PR-D server-layer asset resolution)', () =>
const assets = await resolveDesignSystemAssets('nonexistent', builtInRoot, userRoot, {});
expect(assets.tokensCss).toBeUndefined();
expect(assets.fixtureHtml).toBeUndefined();
expect(assets.componentsManifest).toBeUndefined();
});
});

View file

@ -305,6 +305,8 @@ describe('composeSystemPrompt', () => {
describe('design-system token + fixture injection (#PR-C)', () => {
const sampleTokensCss = ':root {\n --bg: #ffffff;\n --fg: #111111;\n --accent: #0050d8;\n}';
const sampleFixtureHtml = '<!doctype html>\n<html lang="en">\n <body><button class="btn btn-primary">Subscribe</button></body>\n</html>';
const sampleComponentsManifest =
'components.manifest schema v1 for default\nAvailable component groups:\n- Buttons and calls to action: selectors .btn, .btn-primary; tokens --accent';
it('appends BOTH a tokens block and a fixture block when both inputs are present', () => {
const prompt = composeSystemPrompt({
@ -323,6 +325,22 @@ describe('composeSystemPrompt', () => {
expect(prompt).toContain('class="btn btn-primary"');
});
it('prefers the component manifest over the full fixture when both are present', () => {
const prompt = composeSystemPrompt({
designSystemTitle: 'default',
designSystemBody: '# Neutral Modern\n\n> Category: Utility\n\nProse description.',
designSystemTokensCss: sampleTokensCss,
designSystemComponentsManifest: sampleComponentsManifest,
designSystemFixtureHtml: sampleFixtureHtml,
});
expect(prompt).toContain('## Reference component manifest — default');
expect(prompt).toContain('components.manifest schema v1 for default');
expect(prompt).toContain('Buttons and calls to action');
expect(prompt).not.toContain('## Reference fixture — default');
expect(prompt).not.toContain('class="btn btn-primary"');
});
it('keeps the prompt byte-equivalent to the legacy path when both inputs are omitted', () => {
const baseline = composeSystemPrompt({
designSystemTitle: 'default',
@ -332,11 +350,13 @@ describe('composeSystemPrompt', () => {
designSystemTitle: 'default',
designSystemBody: '# Neutral Modern\n\nProse only.',
designSystemTokensCss: undefined,
designSystemComponentsManifest: undefined,
designSystemFixtureHtml: undefined,
});
expect(withFlagOffEquivalent).toBe(baseline);
expect(withFlagOffEquivalent).not.toContain('## Active design system tokens');
expect(withFlagOffEquivalent).not.toContain('## Reference component manifest');
expect(withFlagOffEquivalent).not.toContain('## Reference fixture');
});
@ -356,18 +376,27 @@ describe('composeSystemPrompt', () => {
});
expect(fixtureOnly).not.toContain('## Active design system tokens');
expect(fixtureOnly).toContain('## Reference fixture — default');
const manifestOnly = composeSystemPrompt({
designSystemTitle: 'default',
designSystemBody: '# x\n\nbody',
designSystemComponentsManifest: sampleComponentsManifest,
});
expect(manifestOnly).not.toContain('## Active design system tokens');
expect(manifestOnly).toContain('## Reference component manifest — default');
});
it('places the tokens + fixture blocks AFTER the DESIGN.md prose block (prose sets voice, structured form binds names)', () => {
it('places the tokens + component manifest blocks AFTER the DESIGN.md prose block (prose sets voice, structured form binds names)', () => {
const prompt = composeSystemPrompt({
designSystemTitle: 'default',
designSystemBody: 'PROSE_BODY_MARKER',
designSystemTokensCss: sampleTokensCss,
designSystemComponentsManifest: sampleComponentsManifest,
designSystemFixtureHtml: sampleFixtureHtml,
});
const proseAt = prompt.indexOf('PROSE_BODY_MARKER');
const tokensAt = prompt.indexOf('## Active design system tokens');
const fixtureAt = prompt.indexOf('## Reference fixture');
const fixtureAt = prompt.indexOf('## Reference component manifest');
expect(proseAt).toBeGreaterThan(0);
expect(tokensAt).toBeGreaterThan(proseAt);
expect(fixtureAt).toBeGreaterThan(tokensAt);
@ -378,9 +407,11 @@ describe('composeSystemPrompt', () => {
designSystemTitle: 'default',
designSystemBody: '# x\n\nbody',
designSystemTokensCss: ' \n \t ',
designSystemComponentsManifest: '\n\t',
designSystemFixtureHtml: '\n\n',
});
expect(prompt).not.toContain('## Active design system tokens');
expect(prompt).not.toContain('## Reference component manifest');
expect(prompt).not.toContain('## Reference fixture');
});
});

View file

@ -0,0 +1,109 @@
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
extractComponentsManifest,
summarizeComponentsManifestForPrompt,
} from '../packages/contracts/src/design-systems/components-manifest.ts';
const repoRoot = path.resolve(import.meta.dirname, '..');
const designSystemsRoot = path.join(repoRoot, 'design-systems');
const skippedDesignSystemDirectories = new Set(['_schema']);
type BrandSources = {
id: string;
fixturePath: string;
tokensCss: string;
fixtureHtml: string;
};
export async function checkComponentsManifestExtraction(): Promise<boolean> {
const sources = await discoverBrandSources();
const violations: string[] = [];
let selectorCount = 0;
let groupCount = 0;
for (const source of sources) {
try {
const manifest = extractComponentsManifest({
brandId: source.id,
fixtureHtml: source.fixtureHtml,
tokensCss: source.tokensCss,
});
const summary = summarizeComponentsManifestForPrompt(manifest);
selectorCount += manifest.fixture.selectorCount;
groupCount += manifest.groups.filter((group) => group.present).length;
if (manifest.fixture.styleBlockCount === 0) {
violations.push(`[${source.id}] ${toRepositoryPath(source.fixturePath)} has no <style> blocks to summarize.`);
}
if (manifest.fixture.selectorCount === 0) {
violations.push(`[${source.id}] ${toRepositoryPath(source.fixturePath)} produced a manifest with zero CSS selectors.`);
}
if (!summary.includes(`components.manifest schema v${manifest.schemaVersion} for ${source.id}`)) {
violations.push(`[${source.id}] manifest summary is missing its schema/id header.`);
}
} catch (err) {
violations.push(
`[${source.id}] failed to extract manifest from ${toRepositoryPath(source.fixturePath)}: ${
err instanceof Error ? err.message : String(err)
}`,
);
}
}
if (violations.length > 0) {
console.error('Design system component manifest extraction violations:');
for (const violation of violations) {
console.error(`- ${violation}`);
}
console.error('Every compiled design system must remain consumable through the structured component manifest path.');
return false;
}
console.log(
`Design system component manifest extraction passed: ${sources.length} fixtures summarized (${selectorCount} selectors across ${groupCount} present component groups).`,
);
return true;
}
async function discoverBrandSources(): Promise<BrandSources[]> {
const entries = await readdir(designSystemsRoot, { withFileTypes: true });
const sources: BrandSources[] = [];
for (const entry of entries) {
if (!entry.isDirectory() || skippedDesignSystemDirectories.has(entry.name)) continue;
const brandRoot = path.join(designSystemsRoot, entry.name);
const tokensPath = path.join(brandRoot, 'tokens.css');
const fixturePath = path.join(brandRoot, 'components.html');
const [tokensCss, fixtureHtml] = await Promise.all([
readFile(tokensPath, 'utf8'),
readFile(fixturePath, 'utf8'),
]);
sources.push({
id: entry.name,
fixturePath,
tokensCss,
fixtureHtml,
});
}
return sources.sort((a, b) => a.id.localeCompare(b.id));
}
function toRepositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join('/');
}
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
if (isMain) {
checkComponentsManifestExtraction().then((passed) => {
process.exitCode = passed ? 0 : 1;
}, (err: unknown) => {
console.error(err);
process.exitCode = 1;
});
}

View file

@ -182,6 +182,7 @@ export async function checkDesignSystemFlagParity(): Promise<boolean> {
designSystemBody: brand.designMd,
designSystemTitle: brand.title,
designSystemTokensCss: brand.assets.tokensCss,
designSystemComponentsManifest: brand.assets.componentsManifest,
designSystemFixtureHtml: brand.assets.fixtureHtml,
});

View file

@ -3,6 +3,7 @@ import path from "node:path";
import { checkDesignSystemComponentFixtureReport } from "./check-components-fixtures.ts";
import { checkDesignSystemFlagParity } from "./check-design-system-flag-parity.ts";
import { checkComponentsManifestExtraction } from "./check-components-manifest-extraction.ts";
import {
checkDesignSystemA1RequiredTokens,
checkDesignSystemA2DefaultsParity,
@ -713,6 +714,7 @@ const checks: GuardCheck[] = [
{ name: "design system unknown token allowlist", run: checkDesignSystemUnknownTokens },
{ name: "design system A2 defaults parity", run: checkDesignSystemA2DefaultsParity },
{ name: "design system flag parity", run: checkDesignSystemFlagParity },
{ name: "design system component manifest extraction", run: checkComponentsManifestExtraction },
];
const results: boolean[] = [];