fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint

Brand-loop live test (SDS duplicate, plans/figma/brand-loop-test-plan.md)
proved the recolor chain end-to-end and surfaced three gaps:

- runtime now defines every declared composition variable as a CSS
  custom property (document root at init + scoped sub-comp hosts in the
  loader), so imported var(--slug, literal) fills resolve live — without
  this the frozen literal always won and variable-driven rebranding
  could not propagate. Slug kept byte-compatible with the figma
  importer (parity test). render --variables overrides win.
- figma component --name: variant frames are often all named
  'Platform=Desktop' and slug-collided across imports.
- imported fragments carry data-hf-snippet and the project linter skips
  composition-root rules for them.
- /figma skill documents the field-tested non-Enterprise tokens path
  (MCP get_variable_defs joined with REST boundVariables ids).

Shared-helper extractions (injectScopedStyles, flattenedRoot module,
parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the
dedup/complexity audit the runtime changes tripped.

Validated live: brand-loop renders purple from the attribute alone (no
manual :root); 118 figma + 662 runtime/compiler + 331 lint tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-08 00:47:13 -07:00
co-authored by Claude Fable 5
parent 80271863d3
commit def276524b
14 changed files with 623 additions and 173 deletions
+13
View File
@@ -210,6 +210,11 @@ export async function lintProject(projectDir: string): Promise<ProjectLintResult
const html = readFileSync(filePath, "utf-8");
const compSrcPath = `compositions/${file}`;
allHtmlSources.push({ html, compSrcPath });
// Mountable fragments (figma component imports, registry snippets) are
// not standalone compositions — composition-root rules don't apply.
// Anchored to the file's ROOT element so a real composition that merely
// inlines snippet markup (or mentions the token in text) is still linted.
if (isSnippetFragment(html)) continue;
const result = await lintHyperframeHtml(html, {
filePath,
isSubComposition: true,
@@ -610,3 +615,11 @@ function lintMissingOrEmptySubComposition(
return findings;
}
/** True when the file's first element carries data-hf-snippet — i.e. the file
* IS a mountable fragment, not a composition that merely contains one. */
function isSnippetFragment(html: string): boolean {
const firstTag = html.match(/<[a-zA-Z][^>]*>/);
if (!firstTag) return false;
return /\bdata-hf-snippet\b/.test(firstTag[0]);
}