cascivo 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,24 @@ A **template** is a registry item (`type: "template"`) that bundles a working pa
51
51
 
52
52
  Because the registry model is open, `add owner/repo/component` installs from any compatible registry, not just the first-party one. See the [registry starter](https://github.com/cascivo/cascivo/tree/main/apps/examples/registry-starter) to publish your own.
53
53
 
54
+ ## Prefer the shadcn CLI or an AI tool?
55
+
56
+ cascivo also publishes a [shadcn-compatible registry](https://cascivo.com/r/shadcn/registry.json),
57
+ so you can install the same components with the shadcn CLI or "Open in v0" without
58
+ the `cascivo` binary:
59
+
60
+ ```sh
61
+ npx shadcn@latest add https://cascivo.com/r/shadcn/button.json
62
+ ```
63
+
64
+ Every item inlines its source and resolves its cascivo dependencies transitively.
65
+
66
+ Building **with an AI assistant?** cascivo has no training-data footprint yet, so
67
+ give the tool the knowledge directly rather than relying on it to browse the site:
68
+ paste or link [`https://cascivo.com/llms-full.txt`](https://cascivo.com/llms-full.txt)
69
+ (the whole library — setup + every component — in one file), or point an MCP-capable
70
+ agent at [`@cascivo/mcp`](https://www.npmjs.com/package/@cascivo/mcp).
71
+
54
72
  ## Install
55
73
 
56
74
  ```sh
@@ -138,13 +138,17 @@ function findCssLiteralViolations(source, filename, contract) {
138
138
  if (seen.has(key)) continue;
139
139
  const cls = classify(rawValue, contract);
140
140
  if (!cls) continue;
141
+ const inlineCls = cls.level === "error" ? {
142
+ ...cls,
143
+ level: "warn"
144
+ } : cls;
141
145
  findings.push({
142
146
  file: filename,
143
147
  line: i + 1,
144
148
  property: prop,
145
149
  value: rawValue,
146
150
  rule: "hardcoded-value",
147
- ...cls
151
+ ...inlineCls
148
152
  });
149
153
  }
150
154
  }
@@ -161,8 +165,67 @@ const PASSTHROUGH = new Set([
161
165
  "key",
162
166
  "children"
163
167
  ]);
168
+ /**
169
+ * Standard HTML/React DOM attributes. Every cascade component extends an
170
+ * `HTMLAttributes` interface and spreads `{...props}` onto its element (verified
171
+ * in button.tsx, card.tsx, …), so these are valid at runtime even though the
172
+ * hand-written `*.meta.ts` prop lists (the audit contract) don't enumerate them.
173
+ * Without this set, a legitimate `type`/`name`/`title`/`tabIndex` on a Button is
174
+ * a non-suppressible `unknown-prop` error — the audit-loop deadlock.
175
+ */
176
+ const HTML_PASSTHROUGH = new Set([
177
+ "type",
178
+ "name",
179
+ "value",
180
+ "defaultValue",
181
+ "checked",
182
+ "defaultChecked",
183
+ "placeholder",
184
+ "title",
185
+ "role",
186
+ "tabIndex",
187
+ "form",
188
+ "href",
189
+ "target",
190
+ "rel",
191
+ "download",
192
+ "src",
193
+ "alt",
194
+ "width",
195
+ "height",
196
+ "loading",
197
+ "autoComplete",
198
+ "autoFocus",
199
+ "required",
200
+ "readOnly",
201
+ "min",
202
+ "max",
203
+ "step",
204
+ "rows",
205
+ "cols",
206
+ "wrap",
207
+ "maxLength",
208
+ "minLength",
209
+ "pattern",
210
+ "multiple",
211
+ "accept",
212
+ "size",
213
+ "dir",
214
+ "lang",
215
+ "hidden",
216
+ "draggable",
217
+ "spellCheck",
218
+ "contentEditable",
219
+ "inputMode",
220
+ "enterKeyHint",
221
+ "htmlFor",
222
+ "slot",
223
+ "disabled",
224
+ "open"
225
+ ]);
164
226
  function isPassthrough(prop) {
165
227
  if (PASSTHROUGH.has(prop)) return true;
228
+ if (HTML_PASSTHROUGH.has(prop)) return true;
166
229
  if (prop.startsWith("data-")) return true;
167
230
  if (prop.startsWith("aria-")) return true;
168
231
  if (/^on[A-Z]/.test(prop)) return true;
@@ -307,7 +370,7 @@ function findJsxPropViolations(source, filename, contract) {
307
370
  prop: name,
308
371
  level: "error",
309
372
  rule: "unknown-prop",
310
- message: `<${comp}> has unknown prop "${name}"`
373
+ message: `<${comp}> has unknown prop "${name}". style/className pass through on every component (see the override ladder in docs/AI-RULES.md); for an intentional one-off add \`/* cascivo-audit: allow unknown-prop */\`.`
311
374
  });
312
375
  }
313
376
  }
@@ -513,7 +576,7 @@ const CSS_IMPORT_RE = /import\s+(?:[^'"]*?\bfrom\s+)?(['"])([^'"]+\.css(?:[?#][^
513
576
  function isBareSpecifier(spec) {
514
577
  return !spec.startsWith(".") && !spec.startsWith("/");
515
578
  }
516
- const MESSAGE = "CSS imported from a package through JS/TS cannot be wrapped in an @layer, so if this package ships unlayered global CSS it beats every cascivo layer. Route it through a CSS file instead: `@import url(\"<pkg>/styles.css\") layer(vendor);`. See docs/THIRD-PARTY-CSS.md.";
579
+ const MESSAGE = "CSS imported from a package through JS/TS cannot be wrapped in an @layer, so if this package ships unlayered global CSS it beats every cascivo layer. Route it through a CSS file (`@import url(\"<pkg>/styles.css\") layer(vendor);`) or add @cascivo/vite-plugin (`cascivoLayers({ imports: { \"<pkg>/styles.css\": \"vendor\" } })`). See docs/THIRD-PARTY-CSS.md.";
517
580
  /**
518
581
  * Warn on bare (node_modules) `*.css` imports in JS/TS. Relative imports (the
519
582
  * consumer's own CSS modules) and cascivo's own already-layered `@cascivo/*`
@@ -538,6 +601,84 @@ function findVendorCssImports(source, filename) {
538
601
  return findings;
539
602
  }
540
603
  //#endregion
604
+ //#region src/audit-ai/suppress.ts
605
+ /**
606
+ * Inline suppression directives for `cascivo audit --ai`.
607
+ *
608
+ * A comment `cascivo-audit: allow <rule-id>[, <rule-id>…]` (or `allow all`) on the
609
+ * same line as, or the line immediately preceding, a finding downgrades that
610
+ * finding: it is marked `suppressed` so it no longer counts toward the error/warn
611
+ * exit gate, while still being printed so it stays visible. This is the guaranteed
612
+ * loop-breaker for an AI agent — the one escape hatch that no rule can override.
613
+ */
614
+ /** Rule ids a directive may name (the user-facing error/warn rules). */
615
+ const AUDIT_RULES = new Set([
616
+ "unknown-prop",
617
+ "hardcoded-value",
618
+ "missing-prop",
619
+ "raw-string",
620
+ "unlayered-css",
621
+ "vendor-css-import"
622
+ ]);
623
+ const DIRECTIVE_RE = /cascivo-audit:\s*allow\s+([^\n]+)/i;
624
+ /**
625
+ * Scan a source file for suppression directives. Returns the parsed directives
626
+ * plus `warn`-level findings for any unknown rule id (so typos surface rather
627
+ * than silently failing to suppress).
628
+ */
629
+ function parseDirectives(source, file) {
630
+ const directives = [];
631
+ const findings = [];
632
+ const lines = source.split("\n");
633
+ for (let i = 0; i < lines.length; i++) {
634
+ const m = lines[i]?.match(DIRECTIVE_RE);
635
+ if (!m || m[1] === void 0) continue;
636
+ const ids = m[1].replace(/\*\/.*$/, "").replace(/-->.*$/, "").trim().split(/[\s,]+/).filter(Boolean);
637
+ const line = i + 1;
638
+ if (ids.includes("all")) {
639
+ directives.push({
640
+ line,
641
+ rules: "all"
642
+ });
643
+ continue;
644
+ }
645
+ const valid = /* @__PURE__ */ new Set();
646
+ const unknown = [];
647
+ for (const id of ids) if (AUDIT_RULES.has(id)) valid.add(id);
648
+ else unknown.push(id);
649
+ if (valid.size > 0) directives.push({
650
+ line,
651
+ rules: valid
652
+ });
653
+ if (unknown.length > 0) findings.push({
654
+ file,
655
+ line,
656
+ level: "warn",
657
+ rule: "audit-directive",
658
+ message: `unknown audit rule id${unknown.length > 1 ? "s" : ""} in directive: ${unknown.join(", ")}. valid ids: ${[...AUDIT_RULES].join(", ")}.`
659
+ });
660
+ }
661
+ return {
662
+ directives,
663
+ findings
664
+ };
665
+ }
666
+ /**
667
+ * Mark findings covered by a directive as `suppressed`. A directive on line L
668
+ * applies to findings on line L (inline comment) or line L+1 (comment above the
669
+ * code). Level is left unchanged; callers exclude `suppressed` findings from the
670
+ * exit gate.
671
+ */
672
+ function applySuppressions(findings, directives) {
673
+ if (directives.length === 0) return findings;
674
+ return findings.map((f) => {
675
+ return directives.some((d) => (d.line === f.line || d.line === f.line - 1) && (d.rules === "all" || d.rules.has(f.rule))) ? {
676
+ ...f,
677
+ suppressed: true
678
+ } : f;
679
+ });
680
+ }
681
+ //#endregion
541
682
  //#region src/utils/contract.ts
542
683
  const HERE = dirname(fileURLToPath(import.meta.url));
543
684
  /** Walk up from a start directory looking for the apps/site/public dir. */
@@ -632,6 +773,7 @@ function detail(f) {
632
773
  case "raw-string": return `"${f.text}" → use labels prop / i18n`;
633
774
  case "unlayered-css": return `${f.selector} { … } → wrap in @layer`;
634
775
  case "vendor-css-import": return `import '${f.specifier}' → @import url(…) layer(vendor)`;
776
+ case "audit-directive": return f.message;
635
777
  }
636
778
  }
637
779
  function levelLabel(level) {
@@ -644,7 +786,7 @@ function renderFindings(findings) {
644
786
  }
645
787
  const rows = findings.map((f) => ({
646
788
  loc: `${f.file}:${f.line}`,
647
- level: levelLabel(f.level),
789
+ level: f.suppressed ? "suppressed" : levelLabel(f.level),
648
790
  rule: f.rule,
649
791
  detail: detail(f)
650
792
  }));
@@ -653,11 +795,14 @@ function renderFindings(findings) {
653
795
  const ruleW = Math.max(...rows.map((r) => r.rule.length), 4);
654
796
  for (const r of rows) console.log(`${r.loc.padEnd(locW)} ${r.level.padEnd(lvlW)} ${r.rule.padEnd(ruleW)} ${r.detail}`);
655
797
  console.log("---");
656
- const errors = findings.filter((f) => f.level === "error").length;
657
- const warnings = findings.filter((f) => f.level === "warn").length;
658
- const infos = findings.filter((f) => f.level === "info").length;
798
+ const active = findings.filter((f) => !f.suppressed);
799
+ const errors = active.filter((f) => f.level === "error").length;
800
+ const warnings = active.filter((f) => f.level === "warn").length;
801
+ const infos = active.filter((f) => f.level === "info").length;
802
+ const suppressed = findings.filter((f) => f.suppressed).length;
659
803
  const parts = [`${errors} error${errors === 1 ? "" : "s"}`, `${warnings} warning${warnings === 1 ? "" : "s"}`];
660
804
  if (infos) parts.push(`${infos} info`);
805
+ if (suppressed) parts.push(`${suppressed} suppressed`);
661
806
  console.log(parts.join(", "));
662
807
  }
663
808
  function escapeRe(s) {
@@ -726,14 +871,20 @@ async function audit(args, _config) {
726
871
  console.log(`cascade audit --ai --fix: rewrote ${n} literal${n === 1 ? "" : "s"} to tokens.`);
727
872
  }
728
873
  const allFindings = [];
729
- for (const file of files) allFindings.push(...findingsFor(file, readFileSync(file, "utf8"), contract));
874
+ for (const file of files) {
875
+ const source = readFileSync(file, "utf8");
876
+ const { directives, findings: directiveFindings } = parseDirectives(source, file);
877
+ allFindings.push(...applySuppressions(findingsFor(file, source, contract), directives));
878
+ allFindings.push(...directiveFindings);
879
+ }
730
880
  if (jsonOutput) console.log(JSON.stringify(allFindings, null, 2));
731
881
  else renderFindings(allFindings);
732
- const hasErrors = allFindings.some((f) => f.level === "error");
882
+ const active = allFindings.filter((f) => !f.suppressed);
883
+ const hasErrors = active.some((f) => f.level === "error");
733
884
  if (minLevel === "error" && hasErrors) process.exitCode = 1;
734
- if (minLevel === "warn" && allFindings.some((f) => f.level !== "info")) process.exitCode = 1;
885
+ if (minLevel === "warn" && active.some((f) => f.level !== "info")) process.exitCode = 1;
735
886
  }
736
887
  //#endregion
737
888
  export { audit };
738
889
 
739
- //# sourceMappingURL=audit-jseQagw5.mjs.map
890
+ //# sourceMappingURL=audit-g0Ac-Uzl.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-g0Ac-Uzl.mjs","names":["MESSAGE"],"sources":["../src/utils/contract-pure.ts","../src/audit-ai/css-literals.ts","../src/audit-ai/jsx-props.ts","../src/audit-ai/raw-strings.ts","../src/audit-ai/required-props.ts","../src/utils/css-layers.ts","../src/audit-ai/unlayered.ts","../src/audit-ai/vendor-css.ts","../src/audit-ai/suppress.ts","../src/utils/contract.ts","../src/commands/audit.ts"],"sourcesContent":["export interface PropInfo {\n name: string\n type: string\n required: boolean\n}\n\nexport interface ComponentInfo {\n props: PropInfo[]\n /** True if any prop has required: true */\n hasRequiredProps: boolean\n /** Props that have required: true */\n requiredProps: string[]\n /** True if the component declares user-facing chrome text (intent.content) */\n hasContent: boolean\n}\n\nexport interface Contract {\n /** Map from normalized color/size value → token names */\n tokensByValue: Map<string, string[]>\n /** Map from component name (PascalCase) → component info */\n components: Map<string, ComponentInfo>\n}\n\ninterface TokenEntry {\n name: string\n resolvedDefault: string | null\n}\n\ninterface CatalogFile {\n tokens: TokenEntry[]\n}\n\ninterface RegistryPropMeta {\n name: string\n type?: string\n required?: boolean\n}\n\ninterface RegistryComponentMeta {\n name: string\n props?: RegistryPropMeta[]\n}\n\ninterface RegistryEntry {\n meta?: RegistryComponentMeta\n}\n\ninterface RegistryFile {\n components: RegistryEntry[]\n}\n\ninterface ContextComponentEntry {\n name: string\n intent?: { content?: unknown }\n}\n\ninterface ContextFile {\n components: ContextComponentEntry[]\n}\n\nexport interface BuildContractInput {\n catalog: CatalogFile\n registry: RegistryFile\n context: ContextFile\n}\n\n/** Normalize a color/size value for catalog comparison: lowercase, strip spaces. */\nexport function normalizeValue(value: string): string {\n return value.toLowerCase().replace(/\\s+/g, '')\n}\n\n/** Pure builder — assemble a Contract from already-parsed JSON. Testable without fs. */\nexport function buildContract(input: BuildContractInput): Contract {\n const tokensByValue = new Map<string, string[]>()\n for (const token of input.catalog.tokens) {\n if (token.resolvedDefault == null) continue\n const key = normalizeValue(token.resolvedDefault)\n const list = tokensByValue.get(key)\n if (list) list.push(token.name)\n else tokensByValue.set(key, [token.name])\n }\n\n const contentNames = new Set<string>()\n for (const c of input.context.components) {\n if (c.intent?.content) contentNames.add(c.name)\n }\n\n const components = new Map<string, ComponentInfo>()\n for (const entry of input.registry.components) {\n const meta = entry.meta\n if (!meta?.name) continue\n const props: PropInfo[] = (meta.props ?? []).map((p) => ({\n name: p.name,\n type: p.type ?? 'unknown',\n required: p.required === true,\n }))\n const requiredProps = props.filter((p) => p.required).map((p) => p.name)\n components.set(meta.name, {\n props,\n requiredProps,\n hasRequiredProps: requiredProps.length > 0,\n hasContent: contentNames.has(meta.name),\n })\n }\n\n return { tokensByValue, components }\n}\n","import type { Contract } from '../utils/contract-pure.js'\nimport { normalizeValue } from '../utils/contract-pure.js'\n\nexport interface LiteralFinding {\n file: string\n line: number\n property: string\n value: string\n level: 'error' | 'warn' | 'info'\n rule: 'hardcoded-value'\n /** only when exactly one catalog match */\n suggestedToken?: string\n /** when multiple catalog matches → info */\n allMatches?: string[]\n}\n\n/** Visual CSS properties whose literal values should be tokens. */\nconst VISUAL_PROPS = new Set([\n 'color',\n 'background',\n 'background-color',\n 'border-color',\n 'box-shadow',\n 'border-radius',\n 'font-size',\n 'gap',\n 'padding',\n 'margin',\n 'width',\n 'height',\n])\n\n/** Inline-style camelCase → kebab-case for the props we care about. */\nconst INLINE_PROP_MAP: Record<string, string> = {\n color: 'color',\n background: 'background',\n backgroundColor: 'background-color',\n borderColor: 'border-color',\n boxShadow: 'box-shadow',\n borderRadius: 'border-radius',\n fontSize: 'font-size',\n gap: 'gap',\n padding: 'padding',\n margin: 'margin',\n width: 'width',\n height: 'height',\n}\n\n/** A literal value worth checking: hex, oklch/rgb/hsl(a), or px/rem number. */\nfunction isLiteralValue(value: string): boolean {\n const v = value.trim()\n if (v.includes('var(')) return false\n if (/^#[0-9a-fA-F]{3,8}$/.test(v)) return true\n if (/^(oklch|oklab|rgb|rgba|hsl|hsla)\\(/i.test(v)) return true\n if (/^-?\\d*\\.?\\d+(px|rem)$/.test(v)) return true\n return false\n}\n\nfunction classify(\n value: string,\n contract: Contract,\n): Pick<LiteralFinding, 'level' | 'suggestedToken' | 'allMatches'> | null {\n const matches = contract.tokensByValue.get(normalizeValue(value))\n if (!matches || matches.length === 0) return null\n const first = matches[0]\n if (matches.length === 1 && first) return { level: 'error', suggestedToken: first }\n return { level: 'info', allMatches: matches }\n}\n\n/**\n * Detect literal color/size values in CSS declarations and TSX inline styles\n * that exactly match a known cascade token. Heuristic, line-based — no full\n * CSS/JS parse. Values with no catalog match are NOT flagged (arbitrary brand\n * values are allowed).\n */\nexport function findCssLiteralViolations(\n source: string,\n filename: string,\n contract: Contract,\n): LiteralFinding[] {\n const findings: LiteralFinding[] = []\n const lines = source.split('\\n')\n\n // CSS declaration: `property: value;` (also matches the kebab props inside style=\"...\")\n const cssDecl = /(^|[;{\\s])([a-z-]+)\\s*:\\s*([^;}{]+?)\\s*(?=[;}]|$)/gi\n // Inline JSX style object: `color: '#fff'` or `color: \"#fff\"`\n const inlineDecl = /([A-Za-z][A-Za-z]*)\\s*:\\s*(['\"])([^'\"]+)\\2/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]\n if (line === undefined) continue\n const seen = new Set<string>()\n\n for (const m of line.matchAll(cssDecl)) {\n if (m[2] === undefined || m[3] === undefined) continue\n const prop = m[2].toLowerCase()\n const rawValue = m[3].trim()\n if (!VISUAL_PROPS.has(prop)) continue\n if (!isLiteralValue(rawValue)) continue\n const cls = classify(rawValue, contract)\n if (!cls) continue\n const key = `${prop}|${rawValue}`\n seen.add(key)\n findings.push({\n file: filename,\n line: i + 1,\n property: prop,\n value: rawValue,\n rule: 'hardcoded-value',\n ...cls,\n })\n }\n\n for (const m of line.matchAll(inlineDecl)) {\n if (m[1] === undefined || m[3] === undefined) continue\n const prop = INLINE_PROP_MAP[m[1]]\n if (!prop) continue\n const rawValue = m[3].trim()\n if (!isLiteralValue(rawValue)) continue\n const key = `${prop}|${rawValue}`\n if (seen.has(key)) continue\n const cls = classify(rawValue, contract)\n if (!cls) continue\n // Inline TSX `style={{…}}` overrides are a sanctioned fast-prototyping escape\n // hatch — surface a gentle `warn` (not a loop-blocking `error`) so an agent can\n // finish. `.css`-file literals stay `error`: there the fix is mechanical (`--fix`).\n const inlineCls = cls.level === 'error' ? { ...cls, level: 'warn' as const } : cls\n findings.push({\n file: filename,\n line: i + 1,\n property: prop,\n value: rawValue,\n rule: 'hardcoded-value',\n ...inlineCls,\n })\n }\n }\n\n return findings\n}\n","import type { Contract } from '../utils/contract-pure.js'\n\nexport interface PropFinding {\n file: string\n line: number\n component: string\n prop: string\n level: 'error' | 'info'\n rule: 'unknown-prop' | 'spread-suppressed'\n message: string\n}\n\n/** Props always allowed on any cascade component (DOM passthrough / React intrinsics). */\nconst PASSTHROUGH = new Set(['className', 'style', 'id', 'ref', 'key', 'children'])\n\n/**\n * Standard HTML/React DOM attributes. Every cascade component extends an\n * `HTMLAttributes` interface and spreads `{...props}` onto its element (verified\n * in button.tsx, card.tsx, …), so these are valid at runtime even though the\n * hand-written `*.meta.ts` prop lists (the audit contract) don't enumerate them.\n * Without this set, a legitimate `type`/`name`/`title`/`tabIndex` on a Button is\n * a non-suppressible `unknown-prop` error — the audit-loop deadlock.\n */\nconst HTML_PASSTHROUGH = new Set([\n 'type',\n 'name',\n 'value',\n 'defaultValue',\n 'checked',\n 'defaultChecked',\n 'placeholder',\n 'title',\n 'role',\n 'tabIndex',\n 'form',\n 'href',\n 'target',\n 'rel',\n 'download',\n 'src',\n 'alt',\n 'width',\n 'height',\n 'loading',\n 'autoComplete',\n 'autoFocus',\n 'required',\n 'readOnly',\n 'min',\n 'max',\n 'step',\n 'rows',\n 'cols',\n 'wrap',\n 'maxLength',\n 'minLength',\n 'pattern',\n 'multiple',\n 'accept',\n 'size',\n 'dir',\n 'lang',\n 'hidden',\n 'draggable',\n 'spellCheck',\n 'contentEditable',\n 'inputMode',\n 'enterKeyHint',\n 'htmlFor',\n 'slot',\n 'disabled',\n 'open',\n])\n\nfunction isPassthrough(prop: string): boolean {\n if (PASSTHROUGH.has(prop)) return true\n if (HTML_PASSTHROUGH.has(prop)) return true\n if (prop.startsWith('data-')) return true\n if (prop.startsWith('aria-')) return true\n if (/^on[A-Z]/.test(prop)) return true\n return false\n}\n\n/** Names imported from @cascivo/react in this source. */\nexport function importedCascadeComponents(source: string): Set<string> {\n const names = new Set<string>()\n const importRe = /import\\s*\\{([^}]*)\\}\\s*from\\s*['\"]@cascivo\\/react['\"]/g\n for (const m of source.matchAll(importRe)) {\n const group = m[1]\n if (group === undefined) continue\n for (const raw of group.split(',')) {\n const name = raw\n .trim()\n .split(/\\s+as\\s+/)[0]\n ?.trim()\n if (name) names.add(name)\n }\n }\n return names\n}\n\n/** Find each opening tag for `comp`, returning its attribute substring + start index. */\nexport interface OpeningTag {\n attrs: string\n index: number\n hasSpread: boolean\n}\n\nexport function findOpeningTags(source: string, comp: string): OpeningTag[] {\n const tags: OpeningTag[] = []\n const re = new RegExp(`<${comp}(?=[\\\\s/>])`, 'g')\n for (const m of source.matchAll(re)) {\n const start = m.index ?? 0\n // Walk forward to the matching '>' that closes the opening tag, respecting\n // nested braces (JSX expressions) and quoted strings.\n let i = start + m[0].length\n let depth = 0\n let quote = ''\n let attrs = ''\n let closed = false\n for (; i < source.length; i++) {\n const ch = source[i]\n if (quote) {\n if (ch === quote) quote = ''\n attrs += ch\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n quote = ch\n attrs += ch\n continue\n }\n if (ch === '{') depth++\n else if (ch === '}') depth--\n else if (ch === '>' && depth === 0) {\n closed = true\n break\n }\n attrs += ch\n }\n if (!closed) continue\n const cleanAttrs = attrs.replace(/\\/$/, '')\n tags.push({ attrs: cleanAttrs, index: start, hasSpread: /\\{\\s*\\.\\.\\./.test(cleanAttrs) })\n }\n return tags\n}\n\n/** Extract top-level attribute names from an opening-tag attribute string. */\nexport function extractAttrNames(attrs: string): string[] {\n const names: string[] = []\n let depth = 0\n let quote = ''\n let token = ''\n const flush = () => {\n const name = token.trim().split('=')[0]?.trim()\n if (name && /^[A-Za-z]/.test(name)) names.push(name)\n token = ''\n }\n for (let i = 0; i < attrs.length; i++) {\n const ch = attrs[i]\n if (ch === undefined) continue\n if (quote) {\n if (ch === quote) quote = ''\n continue\n }\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n quote = ch\n continue\n }\n if (ch === '{') {\n depth++\n continue\n }\n if (ch === '}') {\n depth--\n continue\n }\n if (depth > 0) continue\n if (ch === '=') {\n flush()\n // skip the value: handled by quote/brace state on next chars; reset token\n token = ''\n continue\n }\n if (/\\s/.test(ch)) {\n if (token.trim()) flush()\n continue\n }\n token += ch\n }\n if (token.trim()) flush()\n return names\n}\n\nexport function lineOf(source: string, index: number): number {\n let line = 1\n for (let i = 0; i < index && i < source.length; i++) {\n if (source[i] === '\\n') line++\n }\n return line\n}\n\n/**\n * Check JSX usages of imported cascade components for unknown props.\n * Heuristic — regex/brace-aware scan, not a full AST. Elements using a spread\n * (`{...rest}`) are reported as info and skipped (props can't be statically known).\n */\nexport function findJsxPropViolations(\n source: string,\n filename: string,\n contract: Contract,\n): PropFinding[] {\n const findings: PropFinding[] = []\n const tracked = importedCascadeComponents(source)\n\n for (const comp of tracked) {\n const info = contract.components.get(comp)\n if (!info) continue\n const known = new Set(info.props.map((p) => p.name))\n\n for (const tag of findOpeningTags(source, comp)) {\n const line = lineOf(source, tag.index)\n if (tag.hasSpread) {\n findings.push({\n file: filename,\n line,\n component: comp,\n prop: '...',\n level: 'info',\n rule: 'spread-suppressed',\n message: `<${comp}> uses a spread — prop checks skipped`,\n })\n continue\n }\n for (const name of extractAttrNames(tag.attrs)) {\n if (isPassthrough(name)) continue\n if (known.has(name)) continue\n findings.push({\n file: filename,\n line,\n component: comp,\n prop: name,\n level: 'error',\n rule: 'unknown-prop',\n message:\n `<${comp}> has unknown prop \"${name}\". ` +\n 'style/className pass through on every component (see the override ladder in ' +\n 'docs/AI-RULES.md); for an intentional one-off add `/* cascivo-audit: allow unknown-prop */`.',\n })\n }\n }\n }\n\n return findings\n}\n","import type { Contract } from '../utils/contract-pure.js'\nimport { findOpeningTags, importedCascadeComponents, lineOf } from './jsx-props.js'\n\nexport interface RawStringFinding {\n file: string\n line: number\n component: string\n text: string\n level: 'warn'\n rule: 'raw-string'\n message: string\n}\n\n/** Looks like English prose worth flagging: ≥2 whitespace-separated words, letters/spaces only. */\nfunction looksLikeProse(text: string): boolean {\n const trimmed = text.trim()\n if (!/^[A-Za-z][A-Za-z\\s]*$/.test(trimmed)) return false\n return trimmed.split(/\\s+/).length >= 2\n}\n\n/**\n * Conservative raw-string check: for cascade components that own user-facing\n * chrome text (intent.content), warn when a literal multi-word English child\n * appears, suggesting the labels prop / i18n. Never errors. Only inspects the\n * immediate text directly after the opening tag (no nested element traversal).\n */\nexport function findRawStringViolations(\n source: string,\n filename: string,\n contract: Contract,\n): RawStringFinding[] {\n const findings: RawStringFinding[] = []\n const tracked = importedCascadeComponents(source)\n\n for (const comp of tracked) {\n const info = contract.components.get(comp)\n if (!info?.hasContent) continue\n\n for (const tag of findOpeningTags(source, comp)) {\n // Locate the end of this opening tag in the source.\n const openEnd = source.indexOf('>', tag.index)\n if (openEnd === -1) continue\n if (source[openEnd - 1] === '/') continue // self-closing, no children\n\n // Grab text up to the next tag/expression boundary.\n const after = source.slice(openEnd + 1)\n const stop = after.search(/[<{]/)\n const child = (stop === -1 ? after : after.slice(0, stop)).trim()\n if (!child || !looksLikeProse(child)) continue\n\n findings.push({\n file: filename,\n line: lineOf(source, openEnd),\n component: comp,\n text: child,\n level: 'warn',\n rule: 'raw-string',\n message: `<${comp}> raw text \"${child}\" — use the labels prop / i18n`,\n })\n }\n }\n\n return findings\n}\n","import type { Contract } from '../utils/contract-pure.js'\nimport {\n extractAttrNames,\n findOpeningTags,\n importedCascadeComponents,\n lineOf,\n} from './jsx-props.js'\n\nexport interface RequiredPropFinding {\n file: string\n line: number\n component: string\n prop: string\n level: 'error'\n rule: 'missing-prop'\n message: string\n}\n\n/**\n * Flag cascade elements that omit a prop the component marks required.\n * Elements using a spread are skipped (the prop may arrive via the spread).\n */\nexport function findRequiredPropViolations(\n source: string,\n filename: string,\n contract: Contract,\n): RequiredPropFinding[] {\n const findings: RequiredPropFinding[] = []\n const tracked = importedCascadeComponents(source)\n\n for (const comp of tracked) {\n const info = contract.components.get(comp)\n if (!info?.hasRequiredProps) continue\n\n for (const tag of findOpeningTags(source, comp)) {\n if (tag.hasSpread) continue\n const present = new Set(extractAttrNames(tag.attrs))\n const line = lineOf(source, tag.index)\n for (const req of info.requiredProps) {\n if (present.has(req)) continue\n findings.push({\n file: filename,\n line,\n component: comp,\n prop: req,\n level: 'error',\n rule: 'missing-prop',\n message: `<${comp}> requires \"${req}\"`,\n })\n }\n }\n }\n\n return findings\n}\n","/**\n * Zero-dependency scanner for **unlayered style rules** in a CSS source.\n *\n * cascivo ships everything inside `@layer` blocks. Unlayered author CSS beats\n * every layered rule regardless of specificity (CSS cascade-layer semantics), so\n * a single unlayered style rule silently overrides the whole design system. This\n * scanner finds top-level style rules that live outside any `@layer` block.\n *\n * It is line-based and brace-tracking, not a full CSS parse — good enough because\n * we only need to know, for each `{`, whether an `@layer` block is an ancestor.\n *\n * Transparent to layering: `@media`, `@container`, `@supports`, `@scope` — a rule\n * inside these is layered iff the group is inside a layer. Ignored entirely:\n * `@keyframes`/`@font-face`/`@property`/`@page`/etc. (their inner blocks are not\n * style rules we govern). `@import`/`@charset`/`@layer;` statements are not blocks.\n */\n\nexport interface UnlayeredRule {\n /** 1-indexed line of the rule's selector. */\n line: number\n /** The selector text (trimmed, truncated) for the report. */\n selector: string\n}\n\n/** Replace comment bodies and string contents with spaces, preserving newlines. */\nfunction blankCommentsAndStrings(source: string): string {\n let out = ''\n let i = 0\n const n = source.length\n while (i < n) {\n const c = source[i]!\n const next = source[i + 1]\n if (c === '/' && next === '*') {\n out += ' '\n i += 2\n while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {\n out += source[i] === '\\n' ? '\\n' : ' '\n i++\n }\n out += ' '\n i += 2\n } else if (c === '\"' || c === \"'\") {\n const quote = c\n out += ' '\n i++\n while (i < n && source[i] !== quote) {\n if (source[i] === '\\\\') {\n out += ' '\n i += 2\n continue\n }\n out += source[i] === '\\n' ? '\\n' : ' '\n i++\n }\n out += ' '\n i++\n } else {\n out += c\n i++\n }\n }\n return out\n}\n\ntype FrameKind = 'layer' | 'group' | 'style' | 'other'\n\n/**\n * Accessibility / user-preference media features whose overrides MUST win over\n * everything — including a consumer's `cascivo.override` layer — so they are\n * legitimately placed unlayered (top-level). A `@media (forced-colors: active)`\n * block outside `@layer` is the sanctioned cascade idiom, not a hotfix leak.\n */\nconst A11Y_GUARANTEE_RE =\n /@media[^{]*\\b(forced-colors|prefers-contrast|prefers-reduced-motion|prefers-reduced-transparency|inverted-colors)\\b/\n\nfunction classifyPrelude(prelude: string): FrameKind {\n const p = prelude.trim().toLowerCase()\n if (p.startsWith('@layer')) return 'layer'\n // A11y-guarantee media queries are an exempt (non-flagging) context.\n if (A11Y_GUARANTEE_RE.test(p)) return 'other'\n if (\n p.startsWith('@media') ||\n p.startsWith('@container') ||\n p.startsWith('@supports') ||\n p.startsWith('@scope')\n ) {\n return 'group'\n }\n if (p.startsWith('@')) return 'other'\n return 'style'\n}\n\n/**\n * Return every top-level style rule that is NOT inside an `@layer` block. Only the\n * outermost unlayered rule is reported (nested rules inside it are implied).\n */\nexport function findUnlayeredRules(source: string): UnlayeredRule[] {\n const clean = blankCommentsAndStrings(source)\n const rules: UnlayeredRule[] = []\n const stack: FrameKind[] = []\n\n let preludeStart = 0\n const lineStarts: number[] = [0]\n for (let i = 0; i < clean.length; i++) {\n if (clean[i] === '\\n') lineStarts.push(i + 1)\n }\n const lineOf = (idx: number): number => {\n // binary search over lineStarts\n let lo = 0\n let hi = lineStarts.length - 1\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (lineStarts[mid]! <= idx) lo = mid\n else hi = mid - 1\n }\n return lo + 1\n }\n\n const hasAncestor = (kind: FrameKind): boolean => stack.includes(kind)\n\n for (let i = 0; i < clean.length; i++) {\n const c = clean[i]\n if (c === '{') {\n const prelude = clean.slice(preludeStart, i)\n const kind = classifyPrelude(prelude)\n if (\n kind === 'style' &&\n !hasAncestor('layer') &&\n !hasAncestor('other') &&\n !hasAncestor('style')\n ) {\n const trimmed = prelude.trim().replace(/\\s+/g, ' ')\n const startIdx = preludeStart + (prelude.length - prelude.trimStart().length)\n rules.push({\n line: lineOf(startIdx),\n selector: trimmed.length > 60 ? `${trimmed.slice(0, 57)}…` : trimmed,\n })\n }\n stack.push(kind)\n preludeStart = i + 1\n } else if (c === '}') {\n stack.pop()\n preludeStart = i + 1\n } else if (c === ';') {\n // A statement terminator (@import, `@layer a, b;`, or a declaration inside a\n // block). Reset the prelude window so the next `{` sees a clean selector.\n preludeStart = i + 1\n }\n }\n\n return rules\n}\n","import { findUnlayeredRules } from '../utils/css-layers.js'\n\nexport interface UnlayeredFinding {\n file: string\n line: number\n selector: string\n level: 'warn'\n rule: 'unlayered-css'\n message: string\n}\n\nconst MESSAGE =\n 'Unlayered CSS beats every cascivo layer regardless of specificity. ' +\n 'For a one-off override use `@layer cascivo.override { … }`; for app styles declare ' +\n 'an app layer (e.g. cascivo.example) in your order statement. See docs/CSS-LAYERS-PITFALL.md.'\n\n/**\n * Warn on top-level style rules that live outside any `@layer` block. This is the\n * consumer-facing half of the zero-unlayered guard: it teaches the fix rather than\n * failing the build, because `docs/USING-WITH-TAILWIND.md` blesses deliberate\n * unlayered CSS as a valid interop escape hatch. Accessibility-guarantee media\n * queries (`forced-colors`, `prefers-contrast`, …) are exempt — see css-layers.ts.\n */\nexport function findUnlayeredViolations(source: string, filename: string): UnlayeredFinding[] {\n return findUnlayeredRules(source).map((rule) => ({\n file: filename,\n line: rule.line,\n selector: rule.selector,\n level: 'warn',\n rule: 'unlayered-css',\n message: MESSAGE,\n }))\n}\n","import { lineOf } from './jsx-props.js'\n\nexport interface VendorCssFinding {\n file: string\n line: number\n specifier: string\n level: 'warn'\n rule: 'vendor-css-import'\n message: string\n}\n\n// `import '<spec>'` or `import x from '<spec>'` where <spec> ends in .css\n// (optionally with a ?query / #hash). Captures the specifier.\nconst CSS_IMPORT_RE = /import\\s+(?:[^'\"]*?\\bfrom\\s+)?(['\"])([^'\"]+\\.css(?:[?#][^'\"]*)?)\\1/g\n\n/** A bare specifier resolves into node_modules (not a relative or absolute path). */\nfunction isBareSpecifier(spec: string): boolean {\n return !spec.startsWith('.') && !spec.startsWith('/')\n}\n\nconst MESSAGE =\n 'CSS imported from a package through JS/TS cannot be wrapped in an @layer, so if this ' +\n 'package ships unlayered global CSS it beats every cascivo layer. Route it through a CSS ' +\n 'file (`@import url(\"<pkg>/styles.css\") layer(vendor);`) or add @cascivo/vite-plugin ' +\n '(`cascivoLayers({ imports: { \"<pkg>/styles.css\": \"vendor\" } })`). See docs/THIRD-PARTY-CSS.md.'\n\n/**\n * Warn on bare (node_modules) `*.css` imports in JS/TS. Relative imports (the\n * consumer's own CSS modules) and cascivo's own already-layered `@cascivo/*`\n * stylesheets are exempt. Level `warn` — teaches the layer(vendor) recipe.\n */\nexport function findVendorCssImports(source: string, filename: string): VendorCssFinding[] {\n const findings: VendorCssFinding[] = []\n for (const m of source.matchAll(CSS_IMPORT_RE)) {\n const spec = m[2]\n if (spec === undefined) continue\n if (!isBareSpecifier(spec)) continue\n // cascivo's own stylesheets ship the layer statement + fully-layered rules.\n if (spec.startsWith('@cascivo/')) continue\n findings.push({\n file: filename,\n line: lineOf(source, m.index ?? 0),\n specifier: spec,\n level: 'warn',\n rule: 'vendor-css-import',\n message: MESSAGE,\n })\n }\n return findings\n}\n","/**\n * Inline suppression directives for `cascivo audit --ai`.\n *\n * A comment `cascivo-audit: allow <rule-id>[, <rule-id>…]` (or `allow all`) on the\n * same line as, or the line immediately preceding, a finding downgrades that\n * finding: it is marked `suppressed` so it no longer counts toward the error/warn\n * exit gate, while still being printed so it stays visible. This is the guaranteed\n * loop-breaker for an AI agent — the one escape hatch that no rule can override.\n */\n\n/** Rule ids a directive may name (the user-facing error/warn rules). */\nexport const AUDIT_RULES = new Set<string>([\n 'unknown-prop',\n 'hardcoded-value',\n 'missing-prop',\n 'raw-string',\n 'unlayered-css',\n 'vendor-css-import',\n])\n\nexport interface DirectiveFinding {\n file: string\n line: number\n level: 'warn'\n rule: 'audit-directive'\n message: string\n}\n\ninterface Directive {\n line: number\n rules: 'all' | Set<string>\n}\n\nconst DIRECTIVE_RE = /cascivo-audit:\\s*allow\\s+([^\\n]+)/i\n\n/**\n * Scan a source file for suppression directives. Returns the parsed directives\n * plus `warn`-level findings for any unknown rule id (so typos surface rather\n * than silently failing to suppress).\n */\nexport function parseDirectives(\n source: string,\n file: string,\n): { directives: Directive[]; findings: DirectiveFinding[] } {\n const directives: Directive[] = []\n const findings: DirectiveFinding[] = []\n const lines = source.split('\\n')\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i]?.match(DIRECTIVE_RE)\n if (!m || m[1] === undefined) continue\n // Strip trailing comment terminators (`*/`, `-->`) and whitespace.\n const raw = m[1]\n .replace(/\\*\\/.*$/, '')\n .replace(/-->.*$/, '')\n .trim()\n const ids = raw.split(/[\\s,]+/).filter(Boolean)\n const line = i + 1\n if (ids.includes('all')) {\n directives.push({ line, rules: 'all' })\n continue\n }\n const valid = new Set<string>()\n const unknown: string[] = []\n for (const id of ids) {\n if (AUDIT_RULES.has(id)) valid.add(id)\n else unknown.push(id)\n }\n if (valid.size > 0) directives.push({ line, rules: valid })\n if (unknown.length > 0) {\n findings.push({\n file,\n line,\n level: 'warn',\n rule: 'audit-directive',\n message:\n `unknown audit rule id${unknown.length > 1 ? 's' : ''} in directive: ${unknown.join(', ')}. ` +\n `valid ids: ${[...AUDIT_RULES].join(', ')}.`,\n })\n }\n }\n return { directives, findings }\n}\n\n/**\n * Mark findings covered by a directive as `suppressed`. A directive on line L\n * applies to findings on line L (inline comment) or line L+1 (comment above the\n * code). Level is left unchanged; callers exclude `suppressed` findings from the\n * exit gate.\n */\nexport function applySuppressions<T extends { line: number; rule: string }>(\n findings: T[],\n directives: Directive[],\n): Array<T & { suppressed?: boolean }> {\n if (directives.length === 0) return findings\n return findings.map((f) => {\n const covered = directives.some(\n (d) =>\n (d.line === f.line || d.line === f.line - 1) && (d.rules === 'all' || d.rules.has(f.rule)),\n )\n return covered ? { ...f, suppressed: true } : f\n })\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nexport type { BuildContractInput, ComponentInfo, Contract, PropInfo } from './contract-pure.js'\nexport { buildContract, normalizeValue } from './contract-pure.js'\nimport type { Contract } from './contract-pure.js'\nimport { buildContract } from './contract-pure.js'\n\nconst HERE = dirname(fileURLToPath(import.meta.url))\n\n/** Walk up from a start directory looking for the apps/site/public dir. */\nfunction findDocsPublic(startDir: string): string | null {\n let dir = startDir\n for (let i = 0; i < 10; i++) {\n const candidate = join(dir, 'apps', 'site', 'public')\n if (existsSync(candidate)) return candidate\n dir = join(dir, '..')\n }\n return null\n}\n\n/** Walk up from a start directory looking for registry.json at the repo root. */\nfunction findRegistry(startDir: string): string | null {\n let dir = startDir\n for (let i = 0; i < 10; i++) {\n const candidate = join(dir, 'registry.json')\n if (existsSync(candidate)) return candidate\n dir = join(dir, '..')\n }\n return null\n}\n\n/**\n * Load the published cascade contract from local generated artifacts:\n * - token catalog (apps/site/public/tokens.catalog.json) → value→token map\n * - registry.json (repo root) → component prop index\n * - context bundle (apps/site/public/context.json) → which components have chrome text\n */\nexport async function loadContract(options?: {\n catalogPath?: string\n contextPath?: string\n registryPath?: string\n}): Promise<Contract> {\n const docsPublic = findDocsPublic(HERE) ?? findDocsPublic(process.cwd())\n const catalogPath =\n options?.catalogPath ?? (docsPublic ? join(docsPublic, 'tokens.catalog.json') : null)\n const contextPath = options?.contextPath ?? (docsPublic ? join(docsPublic, 'context.json') : null)\n const registryPath = options?.registryPath ?? findRegistry(HERE) ?? findRegistry(process.cwd())\n\n if (!catalogPath || !existsSync(catalogPath)) {\n throw new Error('token catalog not found (apps/site/public/tokens.catalog.json)')\n }\n if (!registryPath || !existsSync(registryPath)) {\n throw new Error('registry.json not found')\n }\n if (!contextPath || !existsSync(contextPath)) {\n throw new Error('context bundle not found (apps/site/public/context.json)')\n }\n\n const catalog = JSON.parse(readFileSync(catalogPath, 'utf8')) as Parameters<\n typeof buildContract\n >[0]['catalog']\n const registry = JSON.parse(readFileSync(registryPath, 'utf8')) as Parameters<\n typeof buildContract\n >[0]['registry']\n const context = JSON.parse(readFileSync(contextPath, 'utf8')) as Parameters<\n typeof buildContract\n >[0]['context']\n\n return buildContract({ catalog, registry, context })\n}\n","import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'\nimport { extname, join } from 'node:path'\nimport type { LiteralFinding } from '../audit-ai/css-literals.js'\nimport { findCssLiteralViolations } from '../audit-ai/css-literals.js'\nimport type { PropFinding } from '../audit-ai/jsx-props.js'\nimport { findJsxPropViolations } from '../audit-ai/jsx-props.js'\nimport type { RawStringFinding } from '../audit-ai/raw-strings.js'\nimport { findRawStringViolations } from '../audit-ai/raw-strings.js'\nimport type { RequiredPropFinding } from '../audit-ai/required-props.js'\nimport { findRequiredPropViolations } from '../audit-ai/required-props.js'\nimport type { UnlayeredFinding } from '../audit-ai/unlayered.js'\nimport { findUnlayeredViolations } from '../audit-ai/unlayered.js'\nimport type { VendorCssFinding } from '../audit-ai/vendor-css.js'\nimport { findVendorCssImports } from '../audit-ai/vendor-css.js'\nimport type { DirectiveFinding } from '../audit-ai/suppress.js'\nimport { applySuppressions, parseDirectives } from '../audit-ai/suppress.js'\nimport type { CascadeConfig } from '../utils/config.js'\nimport type { Contract } from '../utils/contract.js'\nimport { loadContract } from '../utils/contract.js'\n\nexport type Finding =\n | LiteralFinding\n | PropFinding\n | RequiredPropFinding\n | RawStringFinding\n | UnlayeredFinding\n | VendorCssFinding\n | DirectiveFinding\n\n/** A finding after suppression directives have been applied. */\nexport type AuditedFinding = Finding & { suppressed?: boolean }\n\nconst SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'build', '.next', 'coverage'])\n\nfunction collectFiles(paths: string[]): string[] {\n const out: string[] = []\n const walk = (p: string) => {\n if (!existsSync(p)) return\n const s = statSync(p)\n if (s.isDirectory()) {\n const base = p.split('/').pop() ?? ''\n if (SKIP_DIRS.has(base)) return\n for (const entry of readdirSync(p)) walk(join(p, entry))\n return\n }\n const ext = extname(p)\n if (ext === '.css' || ext === '.tsx' || ext === '.ts') out.push(p)\n }\n for (const p of paths) walk(p)\n return out\n}\n\nfunction findingsFor(file: string, source: string, contract: Contract): Finding[] {\n const ext = extname(file)\n const findings: Finding[] = []\n if (ext === '.css') {\n findings.push(...findCssLiteralViolations(source, file, contract))\n findings.push(...findUnlayeredViolations(source, file))\n } else if (ext === '.tsx' || ext === '.ts') {\n findings.push(...findCssLiteralViolations(source, file, contract))\n findings.push(...findJsxPropViolations(source, file, contract))\n findings.push(...findRequiredPropViolations(source, file, contract))\n findings.push(...findRawStringViolations(source, file, contract))\n findings.push(...findVendorCssImports(source, file))\n }\n return findings\n}\n\nfunction detail(f: Finding): string {\n switch (f.rule) {\n case 'hardcoded-value':\n if (f.suggestedToken) return `${f.value} → var(${f.suggestedToken})`\n return `${f.value} → ${f.allMatches?.join(' | ') ?? '(ambiguous)'}`\n case 'unknown-prop':\n return `<${f.component} ${f.prop}>`\n case 'spread-suppressed':\n return `<${f.component} {...}> (props not checked)`\n case 'missing-prop':\n return `<${f.component}> requires \"${f.prop}\"`\n case 'raw-string':\n return `\"${f.text}\" → use labels prop / i18n`\n case 'unlayered-css':\n return `${f.selector} { … } → wrap in @layer`\n case 'vendor-css-import':\n return `import '${f.specifier}' → @import url(…) layer(vendor)`\n case 'audit-directive':\n return f.message\n }\n}\n\nfunction levelLabel(level: Finding['level']): string {\n return level === 'warn' ? 'warn' : level\n}\n\nfunction renderFindings(findings: AuditedFinding[]): void {\n if (findings.length === 0) {\n console.log('cascade audit --ai: no findings.')\n return\n }\n const rows = findings.map((f) => ({\n loc: `${f.file}:${f.line}`,\n level: f.suppressed ? 'suppressed' : levelLabel(f.level),\n rule: f.rule,\n detail: detail(f),\n }))\n const locW = Math.max(...rows.map((r) => r.loc.length), 8)\n const lvlW = Math.max(...rows.map((r) => r.level.length), 5)\n const ruleW = Math.max(...rows.map((r) => r.rule.length), 4)\n for (const r of rows) {\n console.log(\n `${r.loc.padEnd(locW)} ${r.level.padEnd(lvlW)} ${r.rule.padEnd(ruleW)} ${r.detail}`,\n )\n }\n console.log('---')\n const active = findings.filter((f) => !f.suppressed)\n const errors = active.filter((f) => f.level === 'error').length\n const warnings = active.filter((f) => f.level === 'warn').length\n const infos = active.filter((f) => f.level === 'info').length\n const suppressed = findings.filter((f) => f.suppressed).length\n const parts = [\n `${errors} error${errors === 1 ? '' : 's'}`,\n `${warnings} warning${warnings === 1 ? '' : 's'}`,\n ]\n if (infos) parts.push(`${infos} info`)\n if (suppressed) parts.push(`${suppressed} suppressed`)\n console.log(parts.join(', '))\n}\n\nfunction escapeRe(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Pure literal→token rewrite. Only rewrites simple `property: #hex;` style\n * declarations where exactly one token matches. Returns the new source and how\n * many findings were resolved (verified by re-running the checker).\n */\nexport function fixCssLiterals(\n source: string,\n file: string,\n contract: Contract,\n): { source: string; fixed: number } {\n const fixable = findCssLiteralViolations(source, file, contract).filter(\n (f) => f.level === 'error' && f.suggestedToken && /^#[0-9a-fA-F]{3,8}$/.test(f.value),\n )\n let next = source\n for (const f of fixable) {\n // Only rewrite a full-value declaration: `prop: #hex;` (or end of block).\n const re = new RegExp(`(${escapeRe(f.property)}\\\\s*:\\\\s*)${escapeRe(f.value)}(\\\\s*[;}])`, 'gi')\n next = next.replace(re, `$1var(${f.suggestedToken})$2`)\n }\n if (next === source) return { source, fixed: 0 }\n const remaining = findCssLiteralViolations(next, file, contract).filter(\n (f) => f.level === 'error' && f.suggestedToken,\n ).length\n return { source: next, fixed: fixable.length - remaining }\n}\n\n/** Apply {@link fixCssLiterals} to each .css file on disk. */\nfunction applyFixes(files: string[], contract: Contract): number {\n let fixed = 0\n for (const file of files) {\n if (extname(file) !== '.css') continue\n const { source, fixed: n } = fixCssLiterals(readFileSync(file, 'utf8'), file, contract)\n if (n > 0) {\n writeFileSync(file, source)\n fixed += n\n }\n }\n return fixed\n}\n\nexport async function audit(args: string[], _config: CascadeConfig): Promise<void> {\n if (!args.includes('--ai')) {\n console.log('cascivo audit: use --ai to audit AI-generated code against the cascivo contract')\n return\n }\n\n const jsonOutput = args.includes('--json')\n const fixMode = args.includes('--fix')\n const levelIdx = args.indexOf('--level')\n const minLevel = levelIdx >= 0 ? (args[levelIdx + 1] ?? 'error') : 'error'\n\n const paths = args.filter((a, i) => {\n if (a.startsWith('--')) return false\n if (levelIdx >= 0 && i === levelIdx + 1) return false\n return true\n })\n\n let contract: Contract\n try {\n contract = await loadContract()\n } catch (e) {\n console.error(`Contract unavailable: ${e instanceof Error ? e.message : String(e)}`)\n process.exitCode = 2\n return\n }\n\n const files = collectFiles(paths.length ? paths : [process.cwd()])\n\n if (fixMode) {\n const n = applyFixes(files, contract)\n console.log(`cascade audit --ai --fix: rewrote ${n} literal${n === 1 ? '' : 's'} to tokens.`)\n }\n\n const allFindings: AuditedFinding[] = []\n for (const file of files) {\n const source = readFileSync(file, 'utf8')\n const { directives, findings: directiveFindings } = parseDirectives(source, file)\n allFindings.push(...applySuppressions(findingsFor(file, source, contract), directives))\n allFindings.push(...directiveFindings)\n }\n\n if (jsonOutput) {\n console.log(JSON.stringify(allFindings, null, 2))\n } else {\n renderFindings(allFindings)\n }\n\n // Suppressed findings never fail the run — that is the escape hatch's whole point.\n const active = allFindings.filter((f) => !f.suppressed)\n const hasErrors = active.some((f) => f.level === 'error')\n if (minLevel === 'error' && hasErrors) process.exitCode = 1\n if (minLevel === 'warn' && active.some((f) => f.level !== 'info')) process.exitCode = 1\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -200,14 +200,14 @@ function resolveTemplateTarget(cwd, target) {
200
200
  }
201
201
  /**
202
202
  * Names from other libraries → the cascivo layout primitive that fills the same
203
- * role, so `cascivo add flex` installs `layout/stack`. Bare names that already
204
- * resolve (e.g. `stack` → `layout/stack` via suffix match) need no alias.
203
+ * role, so `cascivo add box` installs `layout/flex`. Bare names that already
204
+ * resolve (e.g. `flex` → `layout/flex` via suffix match) still list an alias here
205
+ * for discoverability.
205
206
  */
206
207
  const LAYOUT_ALIASES = {
207
- flex: "stack",
208
- box: "stack",
209
- hstack: "stack",
210
- vstack: "stack",
208
+ box: "layout/flex",
209
+ hstack: "layout/flex",
210
+ vstack: "layout/flex",
211
211
  gap: "spacer"
212
212
  };
213
213
  /**
@@ -707,7 +707,8 @@ selector specificity.
707
707
  3. Never nest layers deeper than the shipped \`cascivo.blocks.<name>\` pattern. For
708
708
  sub-elements, use native CSS nesting inside one layer block, not new sublayers.
709
709
  4. Third-party CSS: \`@import url('lib/styles.css') layer(vendor);\` with \`vendor\`
710
- declared before the cascivo layers. Never import vendor CSS from JavaScript.
710
+ declared before the cascivo layers. Don't import vendor CSS from JavaScript — route it
711
+ through a CSS file, or use \`@cascivo/vite-plugin\` (\`cascivoLayers\`) to layer it.
711
712
  5. Style with \`--cascivo-*\` tokens, not raw values.
712
713
 
713
714
  This app's declared layer order (in \`index.html\`):
@@ -941,6 +942,10 @@ async function runDoctor(root) {
941
942
  function isTranslationRef(v) {
942
943
  return typeof v === "object" && v !== null && "$t" in v;
943
944
  }
945
+ /** A JS literal for a state initial value. */
946
+ function serializeInitial(value) {
947
+ return typeof value === "string" ? `'${value.replace(/'/g, "\\'")}'` : String(value);
948
+ }
944
949
  function serializeProp(value) {
945
950
  if (typeof value === "string") return `"${value.replace(/"/g, "\\\"")}"`;
946
951
  if (typeof value === "number" || typeof value === "boolean") return `{${value}}`;
@@ -960,14 +965,15 @@ function renderNode(node, indent, _parentPropNames, knownComponents) {
960
965
  knownComponents.add(compName);
961
966
  const propParts = [];
962
967
  if (node.props) for (const [k, v] of Object.entries(node.props)) propParts.push(`${k}=${serializeProp(v)}`);
963
- if (node.bind) for (const [k, ref] of Object.entries(node.bind)) {
964
- const path = ref.replace(/^\$data\./, "").replace(/\./g, ".");
965
- propParts.push(`${k}={data.${path}}`);
966
- }
967
- if (node.events) for (const [k, ref] of Object.entries(node.events)) {
968
- const actionName = ref.replace(/^\$actions\./, "");
969
- propParts.push(`${k}={actions.${actionName}}`);
970
- }
968
+ if (node.bind) for (const [k, ref] of Object.entries(node.bind)) if (ref.startsWith("$state.")) propParts.push(`${k}={${ref.slice(7)}.value}`);
969
+ else propParts.push(`${k}={data.${ref.replace(/^\$data\./, "")}}`);
970
+ if (node.events) for (const [k, ref] of Object.entries(node.events)) if (ref.startsWith("$state.set.")) {
971
+ const key = ref.slice(11);
972
+ propParts.push(`${k}={(e) => (${key}.value = coerceValue(e))}`);
973
+ } else if (ref.startsWith("$state.toggle.")) {
974
+ const key = ref.slice(14);
975
+ propParts.push(`${k}={() => (${key}.value = !${key}.value)}`);
976
+ } else propParts.push(`${k}={actions.${ref.replace(/^\$actions\./, "")}}`);
971
977
  const propsStr = propParts.length > 0 ? " " + propParts.join(" ") : "";
972
978
  const childrenStr = node.children !== void 0 ? renderChildren(node.children, indent, [], knownComponents) : void 0;
973
979
  if (childrenStr !== void 0 && childrenStr !== "") return `${indent}<${compName}${propsStr}>\n${indent} ${childrenStr}\n${indent}</${compName}>`;
@@ -978,10 +984,12 @@ function collectBindAndEvents(nodes) {
978
984
  const actionProps = [];
979
985
  function walk(node) {
980
986
  if (node.bind) for (const ref of Object.values(node.bind)) {
987
+ if (ref.startsWith("$state.")) continue;
981
988
  const path = ref.replace(/^\$data\./, "");
982
989
  if (!boundProps.includes(path)) boundProps.push(path);
983
990
  }
984
991
  if (node.events) for (const ref of Object.values(node.events)) {
992
+ if (ref.startsWith("$state.")) continue;
985
993
  const name = ref.replace(/^\$actions\./, "");
986
994
  if (!actionProps.includes(name)) actionProps.push(name);
987
995
  }
@@ -993,9 +1001,21 @@ function collectBindAndEvents(nodes) {
993
1001
  actionProps
994
1002
  };
995
1003
  }
1004
+ /** True if any node writes state via "$state.set.*" (needs the coerceValue helper). */
1005
+ function usesStateSetter(nodes) {
1006
+ const walk = (node) => {
1007
+ if (node.events && Object.values(node.events).some((r) => r.startsWith("$state.set."))) return true;
1008
+ return Array.isArray(node.children) ? node.children.some(walk) : false;
1009
+ };
1010
+ return nodes.some(walk);
1011
+ }
996
1012
  function generateTsx(config, _propMetas, componentsPath) {
997
1013
  const knownComponents = /* @__PURE__ */ new Set();
998
- const { boundProps, actionProps } = collectBindAndEvents(Object.values(config.view.regions).flat());
1014
+ const allNodes = Object.values(config.view.regions).flat();
1015
+ const { boundProps, actionProps } = collectBindAndEvents(allNodes);
1016
+ const stateEntries = Object.entries(config.state ?? {});
1017
+ const hasState = stateEntries.length > 0;
1018
+ const needsCoerce = usesStateSetter(allNodes);
999
1019
  const regionBlocks = Object.entries(config.view.regions).map(([regionName, nodes]) => {
1000
1020
  return ` <div className="region-${regionName}">\n${nodes.map((n) => renderNode(n, " ", [], knownComponents)).join("\n")}\n </div>`;
1001
1021
  }).join("\n");
@@ -1003,13 +1023,24 @@ function generateTsx(config, _propMetas, componentsPath) {
1003
1023
  const hasData = boundProps.length > 0;
1004
1024
  const hasActions = actionProps.length > 0;
1005
1025
  const params = [hasData ? `data: {\n${boundProps.map((p) => ` ${p.replace(/\./g, "_")}: unknown // TODO: type your data`).join("\n")}\n }` : "", hasActions ? `actions: {\n${actionProps.map((a) => ` ${a}: (...args: unknown[]) => unknown`).join("\n")}\n }` : ""].filter(Boolean).join(",\n ");
1006
- return `import React from 'react'
1026
+ const propsType = params ? `{\n ${params}\n}` : "{}";
1027
+ const coreImport = hasState ? "\nimport { useSignal, useSignals } from '@cascivo/core'" : "";
1028
+ const coerceHelper = needsCoerce ? `
1029
+ /** Unwrap a DOM event into the value a state setter should store. */
1030
+ function coerceValue(e: unknown) {
1031
+ const t = (e as { target?: { type?: string; checked?: boolean; value?: unknown } })?.target
1032
+ if (t) return t.type === 'checkbox' || t.type === 'radio' ? t.checked : t.value
1033
+ return e
1034
+ }
1035
+ ` : "";
1036
+ const stateDecls = hasState ? " useSignals()\n" + stateEntries.map(([k, v]) => ` const ${k} = useSignal(${serializeInitial(v)})`).join("\n") + "\n" : "";
1037
+ return `import React from 'react'${coreImport}
1007
1038
  ${importLines}
1008
-
1009
- interface PageProps ${params ? `{\n ${params}\n}` : "{}"}
1039
+ ${coerceHelper}
1040
+ interface PageProps ${propsType}
1010
1041
 
1011
1042
  export function GeneratedPage({ ${hasData ? "data, " : ""}${hasActions ? "actions" : ""} }: PageProps) {
1012
- return (
1043
+ ${stateDecls} return (
1013
1044
  <div className="cascade-view">
1014
1045
  ${regionBlocks}
1015
1046
  </div>
@@ -1165,7 +1196,7 @@ async function list(config, options = {}) {
1165
1196
  return existsSync(join(process.cwd(), config.outputDir, outputName));
1166
1197
  });
1167
1198
  console.log(formatList(components));
1168
- console.log("\nTip: install any entry by its bare name — `cascivo add stack` resolves to `layout/stack`. Layout primitives (Stack, Grid, AutoGrid, Columns, Spacer, Center) replace inline-style layout; see docs/cookbooks/layout-and-spacing.md.");
1199
+ console.log("\nTip: install any entry by its bare name — `cascivo add flex` resolves to `layout/flex`. Layout primitives (Flex, Grid, AutoGrid, Columns, Spacer, Center) replace inline-style layout; see docs/cookbooks/layout-and-spacing.md.");
1169
1200
  }
1170
1201
  //#endregion
1171
1202
  //#region src/commands/registry.ts
@@ -2184,7 +2215,7 @@ async function run(args) {
2184
2215
  break;
2185
2216
  }
2186
2217
  case "audit": {
2187
- const { audit } = await import("./audit-jseQagw5.mjs");
2218
+ const { audit } = await import("./audit-g0Ac-Uzl.mjs");
2188
2219
  await audit(rest, await loadConfig());
2189
2220
  break;
2190
2221
  }