@invariant-ui/cli 0.0.0-stage → 0.1.0

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.
Files changed (196) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +248 -2
  3. package/dist/agent-kit/apply.js +232 -0
  4. package/dist/agent-kit/guidance.js +236 -0
  5. package/dist/agent-kit/index.js +5 -0
  6. package/dist/agent-kit/integrations.js +121 -0
  7. package/dist/agent-kit/plugin.js +115 -0
  8. package/dist/artifact-generator/index.js +75 -0
  9. package/dist/browser-verify/driver.js +2 -0
  10. package/dist/browser-verify/index.js +4 -0
  11. package/dist/browser-verify/playwright.js +82 -0
  12. package/dist/browser-verify/runner.js +175 -0
  13. package/dist/browser-verify/spec.js +103 -0
  14. package/dist/cli/args.js +77 -0
  15. package/dist/cli/baseline.js +64 -0
  16. package/dist/cli/cli.js +121 -0
  17. package/dist/cli/commands-browser.js +104 -0
  18. package/dist/cli/commands-discover.js +312 -0
  19. package/dist/cli/commands-enforcement.js +35 -0
  20. package/dist/cli/commands-governance.js +179 -0
  21. package/dist/cli/commands-hook.js +269 -0
  22. package/dist/cli/commands-patterns.js +324 -0
  23. package/dist/cli/commands-setup.js +485 -0
  24. package/dist/cli/commands-validate.js +157 -0
  25. package/dist/cli/commands.js +517 -0
  26. package/dist/cli/config.js +197 -0
  27. package/dist/cli/diff.js +66 -0
  28. package/dist/cli/enforcement-log.js +83 -0
  29. package/dist/cli/enforcement-report.js +96 -0
  30. package/dist/cli/errors.js +10 -0
  31. package/dist/cli/format.js +22 -0
  32. package/dist/cli/index.js +5 -0
  33. package/dist/cli/io.js +2 -0
  34. package/dist/cli/main.js +31 -0
  35. package/dist/cli/pipeline.js +262 -0
  36. package/dist/cli/publish.js +77 -0
  37. package/dist/cli/report.js +127 -0
  38. package/dist/cli/starter-rules.js +57 -0
  39. package/dist/cli/store.js +60 -0
  40. package/dist/cli/version.js +5 -0
  41. package/dist/cli/watch.js +68 -0
  42. package/dist/cli/workflow.js +52 -0
  43. package/dist/compiler-angular/adapter.js +115 -0
  44. package/dist/compiler-angular/analyze.js +222 -0
  45. package/dist/compiler-angular/classes.js +446 -0
  46. package/dist/compiler-angular/declared.js +285 -0
  47. package/dist/compiler-angular/emit.js +183 -0
  48. package/dist/compiler-angular/index.js +4 -0
  49. package/dist/compiler-angular/template.js +142 -0
  50. package/dist/compiler-core/adapter.js +2 -0
  51. package/dist/compiler-core/adapters/inventory.js +312 -0
  52. package/dist/compiler-core/compile.js +148 -0
  53. package/dist/compiler-core/component-facts.js +123 -0
  54. package/dist/compiler-core/discovery/docs.js +66 -0
  55. package/dist/compiler-core/discovery/entries.js +270 -0
  56. package/dist/compiler-core/discovery/exports.js +207 -0
  57. package/dist/compiler-core/discovery/graph.js +59 -0
  58. package/dist/compiler-core/discovery/index.js +103 -0
  59. package/dist/compiler-core/discovery/packages.js +148 -0
  60. package/dist/compiler-core/discovery/schema.js +144 -0
  61. package/dist/compiler-core/discovery/storybook.js +232 -0
  62. package/dist/compiler-core/discovery/support.js +82 -0
  63. package/dist/compiler-core/discovery/tokens.js +186 -0
  64. package/dist/compiler-core/discovery/tooling.js +146 -0
  65. package/dist/compiler-core/fs.js +89 -0
  66. package/dist/compiler-core/glob.js +3 -0
  67. package/dist/compiler-core/index.js +16 -0
  68. package/dist/compiler-core/ir-builder.js +68 -0
  69. package/dist/compiler-core/json-schema.js +11 -0
  70. package/dist/compiler-core/testing/index.js +81 -0
  71. package/dist/compiler-core/ts/ast.js +166 -0
  72. package/dist/compiler-core/ts/members.js +163 -0
  73. package/dist/compiler-core/ts/program.js +81 -0
  74. package/dist/compiler-core/ts/static-eval.js +145 -0
  75. package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
  76. package/dist/compiler-core/walk.js +46 -0
  77. package/dist/compiler-ir/index.js +4 -0
  78. package/dist/compiler-ir/ir.js +67 -0
  79. package/dist/compiler-ir/json-schema.js +6 -0
  80. package/dist/compiler-ir/validate.js +116 -0
  81. package/dist/compiler-react/adapter.js +147 -0
  82. package/dist/compiler-react/analyze.js +199 -0
  83. package/dist/compiler-react/ast.js +105 -0
  84. package/dist/compiler-react/behavior.js +75 -0
  85. package/dist/compiler-react/detect.js +558 -0
  86. package/dist/compiler-react/emit.js +188 -0
  87. package/dist/compiler-react/index.js +2 -0
  88. package/dist/compiler-react/program.js +58 -0
  89. package/dist/compiler-react/props.js +157 -0
  90. package/dist/compiler-react/styling.js +138 -0
  91. package/dist/compiler-react/usage.js +116 -0
  92. package/dist/compiler-react/variants.js +111 -0
  93. package/dist/compiler-storybook/adapter.js +253 -0
  94. package/dist/compiler-storybook/csf.js +521 -0
  95. package/dist/compiler-storybook/emit.js +202 -0
  96. package/dist/compiler-storybook/index-json.js +41 -0
  97. package/dist/compiler-storybook/index.js +5 -0
  98. package/dist/compiler-storybook/mdx.js +120 -0
  99. package/dist/compiler-storybook/static-eval.js +145 -0
  100. package/dist/compiler-tokens/adapter.js +158 -0
  101. package/dist/compiler-tokens/css.js +59 -0
  102. package/dist/compiler-tokens/index.js +5 -0
  103. package/dist/compiler-tokens/json-lines.js +106 -0
  104. package/dist/compiler-tokens/json-tokens.js +52 -0
  105. package/dist/compiler-vue/adapter.js +107 -0
  106. package/dist/compiler-vue/analyze.js +344 -0
  107. package/dist/compiler-vue/declared.js +343 -0
  108. package/dist/compiler-vue/emit.js +129 -0
  109. package/dist/compiler-vue/index.js +5 -0
  110. package/dist/compiler-vue/script.js +472 -0
  111. package/dist/compiler-vue/sfc.js +44 -0
  112. package/dist/compiler-vue/template.js +73 -0
  113. package/dist/contract/archetype-checks.js +153 -0
  114. package/dist/contract/components.js +102 -0
  115. package/dist/contract/conflicts.js +39 -0
  116. package/dist/contract/contract.js +46 -0
  117. package/dist/contract/evidence.js +84 -0
  118. package/dist/contract/fact-path.js +39 -0
  119. package/dist/contract/facts.js +62 -0
  120. package/dist/contract/index.js +16 -0
  121. package/dist/contract/integrity.js +21 -0
  122. package/dist/contract/json-schema.js +11 -0
  123. package/dist/contract/knowledge.js +95 -0
  124. package/dist/contract/pattern-checks.js +254 -0
  125. package/dist/contract/patterns.js +225 -0
  126. package/dist/contract/primitives.js +81 -0
  127. package/dist/contract/sources.js +30 -0
  128. package/dist/contract/tokens.js +41 -0
  129. package/dist/contract/validate.js +253 -0
  130. package/dist/contract-query/archetype.js +188 -0
  131. package/dist/contract-query/index.js +6 -0
  132. package/dist/contract-query/patterns.js +298 -0
  133. package/dist/contract-query/scan.js +164 -0
  134. package/dist/contract-query/search.js +132 -0
  135. package/dist/contract-query/system.js +148 -0
  136. package/dist/contract-query/validate-code.js +239 -0
  137. package/dist/contract-query/validate-usage.js +150 -0
  138. package/dist/contract-query/views.js +288 -0
  139. package/dist/governance/decisions.js +229 -0
  140. package/dist/governance/index.js +5 -0
  141. package/dist/governance/patterns-file.js +156 -0
  142. package/dist/governance/patterns.js +510 -0
  143. package/dist/governance/policies.js +148 -0
  144. package/dist/mcp/index.js +7 -0
  145. package/dist/mcp/loader.js +70 -0
  146. package/dist/mcp/protocol.js +27 -0
  147. package/dist/mcp/resources.js +138 -0
  148. package/dist/mcp/server.js +177 -0
  149. package/dist/mcp/stdio.js +28 -0
  150. package/dist/mcp/tools.js +267 -0
  151. package/dist/reconciliation/code.js +62 -0
  152. package/dist/reconciliation/components.js +401 -0
  153. package/dist/reconciliation/conflicts.js +126 -0
  154. package/dist/reconciliation/facts.js +41 -0
  155. package/dist/reconciliation/index.js +7 -0
  156. package/dist/reconciliation/reconcile.js +134 -0
  157. package/dist/reconciliation/resolve.js +158 -0
  158. package/dist/reconciliation/review.js +81 -0
  159. package/dist/reconciliation/storybook.js +370 -0
  160. package/dist/reconciliation/tokens.js +179 -0
  161. package/dist/reconciliation/util.js +55 -0
  162. package/dist/semantic-engine/additive.js +51 -0
  163. package/dist/semantic-engine/compounds.js +91 -0
  164. package/dist/semantic-engine/documents.js +157 -0
  165. package/dist/semantic-engine/enrich.js +42 -0
  166. package/dist/semantic-engine/index.js +7 -0
  167. package/dist/semantic-engine/infer.js +38 -0
  168. package/dist/semantic-engine/tokens.js +120 -0
  169. package/dist/shared/canonical.js +65 -0
  170. package/dist/shared/diagnostic.js +78 -0
  171. package/dist/shared/glob.js +98 -0
  172. package/dist/shared/index.js +4 -0
  173. package/dist/shared/json.js +2 -0
  174. package/dist/validation/card.js +50 -0
  175. package/dist/validation/discovery/arrangements.js +162 -0
  176. package/dist/validation/discovery/discover.js +70 -0
  177. package/dist/validation/discovery/index.js +8 -0
  178. package/dist/validation/discovery/model.js +127 -0
  179. package/dist/validation/discovery/promote.js +24 -0
  180. package/dist/validation/discovery/reviews.js +63 -0
  181. package/dist/validation/discovery/shapes.js +47 -0
  182. package/dist/validation/discovery/similar-files.js +146 -0
  183. package/dist/validation/discovery/util.js +47 -0
  184. package/dist/validation/index.js +11 -0
  185. package/dist/validation/interaction-facts.js +341 -0
  186. package/dist/validation/interaction-workflows.js +633 -0
  187. package/dist/validation/literals.js +251 -0
  188. package/dist/validation/pattern-rules.js +608 -0
  189. package/dist/validation/policy.js +241 -0
  190. package/dist/validation/report.js +163 -0
  191. package/dist/validation/rules.js +269 -0
  192. package/dist/validation/scan.js +295 -0
  193. package/dist/validation/text.js +34 -0
  194. package/dist/validation/validate-code.js +513 -0
  195. package/dist/validation/validate-usage.js +196 -0
  196. package/package.json +38 -4
@@ -0,0 +1,91 @@
1
+ import { cmp, inferredFact } from "./infer.js";
2
+ const SAME_FILE = 0.7;
3
+ const SAME_PACKAGE = 0.4;
4
+ function view(component) {
5
+ return {
6
+ component,
7
+ impls: component.implementations.map((i) => {
8
+ const ext = i.extensions?.[i.framework];
9
+ return {
10
+ id: i.id,
11
+ framework: i.framework,
12
+ ...(i.packageName ? { packageName: i.packageName } : {}),
13
+ ...(typeof ext?.file === "string" ? { file: ext.file } : {})
14
+ };
15
+ })
16
+ };
17
+ }
18
+ /**
19
+ * Compound components by naming and location: `DialogTrigger` and `DialogContent` next to `Dialog` in one
20
+ * package. A parent needs at least two such children, and a child in the parent's own file is more likely
21
+ * than one elsewhere in the package. Existing `contains` relationships are never repeated or changed.
22
+ */
23
+ export function inferCompounds(components) {
24
+ const views = components.map(view);
25
+ const additions = new Map();
26
+ for (const parent of views) {
27
+ const candidates = [];
28
+ for (const child of views) {
29
+ if (child === parent)
30
+ continue;
31
+ const rest = child.component.name.slice(parent.component.name.length);
32
+ if (!child.component.name.startsWith(parent.component.name) || !/^[A-Z]/.test(rest))
33
+ continue;
34
+ for (const p of parent.impls) {
35
+ const match = child.impls.find((c) => c.framework === p.framework && c.packageName && c.packageName === p.packageName);
36
+ if (!match)
37
+ continue;
38
+ candidates.push({
39
+ child,
40
+ sameFile: Boolean(p.file && p.file === match.file),
41
+ parentImpl: p.id,
42
+ childImpl: match.id
43
+ });
44
+ break;
45
+ }
46
+ }
47
+ if (candidates.length < 2)
48
+ continue;
49
+ const known = new Set(parent.component.composition.map((r) => `${r.type}\u0000${r.target}`));
50
+ for (const c of candidates) {
51
+ if (known.has(`contains\u0000${c.child.component.id}`))
52
+ continue;
53
+ const list = additions.get(parent.component.id) ?? [];
54
+ list.push({
55
+ target: c.child.component.id,
56
+ confidence: c.sameFile ? SAME_FILE : SAME_PACKAGE,
57
+ reasoning: `${c.child.component.name} is named ${parent.component.name} plus a suffix, in the same package${c.sameFile ? " and the same file" : ""}, and ${parent.component.name} has ${candidates.length} such components`,
58
+ evidenceIds: [
59
+ ...(parent.component.facts[`implementations.${c.parentImpl}`]?.evidenceIds ?? []),
60
+ ...(c.child.component.facts[`implementations.${c.childImpl}`]?.evidenceIds ?? [])
61
+ ],
62
+ inputs: [`implementations.${c.parentImpl}`, c.child.component.id]
63
+ });
64
+ additions.set(parent.component.id, list);
65
+ }
66
+ }
67
+ return components.map((component) => {
68
+ const add = additions.get(component.id);
69
+ if (!add || add.length === 0)
70
+ return component;
71
+ const sorted = [...add].sort((a, b) => cmp(a.target, b.target));
72
+ const facts = { ...component.facts };
73
+ const composition = [...component.composition];
74
+ for (const a of sorted) {
75
+ facts[`composition.${composition.length}`] = inferredFact({
76
+ rule: "compound.name-prefix",
77
+ confidence: a.confidence,
78
+ reasoning: a.reasoning,
79
+ inputs: a.inputs,
80
+ evidenceIds: a.evidenceIds
81
+ });
82
+ composition.push({ type: "contains", target: a.target });
83
+ }
84
+ return {
85
+ ...component,
86
+ composition,
87
+ facts: Object.fromEntries(Object.entries(facts).sort((a, b) => cmp(a[0], b[0])))
88
+ };
89
+ });
90
+ }
91
+ //# sourceMappingURL=compounds.js.map
@@ -0,0 +1,157 @@
1
+ import { cmp, inferredFact, slugSegment, words } from "./infer.js";
2
+ const KIND_RULES = [
3
+ {
4
+ kind: "accessibility",
5
+ pattern: /\ba11y\b|accessib|\bwcag\b|\baria\b|screen[- ]reader|keyboard/,
6
+ confidence: 0.7
7
+ },
8
+ {
9
+ kind: "do-dont",
10
+ pattern: /do and don'?t|dos and don'?ts|do'?s? ?& ?don'?ts?|do\/don'?t/,
11
+ confidence: 0.7
12
+ },
13
+ { kind: "composition", pattern: /composition|compos(?:e|ing)\b|layout/, confidence: 0.6 },
14
+ {
15
+ kind: "visual-principle",
16
+ pattern: /principle|philosophy|foundations?|brand|voice and tone|visual/,
17
+ confidence: 0.6
18
+ },
19
+ {
20
+ kind: "usage",
21
+ pattern: /usage|guidelines?|how to use|when to use|best practices?|getting started|overview/,
22
+ confidence: 0.6
23
+ }
24
+ ];
25
+ function classify(text) {
26
+ for (const rule of KIND_RULES) {
27
+ const m = rule.pattern.exec(text);
28
+ if (m)
29
+ return { kind: rule.kind, confidence: rule.confidence, matched: m[0] };
30
+ }
31
+ return { kind: "reference", confidence: 0.4 };
32
+ }
33
+ function containsWords(haystack, needle) {
34
+ if (needle.length === 0 || (needle.length === 1 && needle[0].length <= 2))
35
+ return false;
36
+ for (let i = 0; i + needle.length <= haystack.length; i++)
37
+ if (needle.every((w, j) => haystack[i + j] === w))
38
+ return true;
39
+ return false;
40
+ }
41
+ /**
42
+ * One guideline per documentation page. The IR carries a page's title and headings, not its prose, so the
43
+ * statement is the title and the kind comes from keywords: a page that matches none stays a `reference`.
44
+ * `appliesTo` links a page to a component only when the file name or title names it.
45
+ */
46
+ export function classifyDocuments(docs, components) {
47
+ const guidelines = [];
48
+ const evidence = new Map();
49
+ const skipped = [];
50
+ const used = new Set();
51
+ for (const doc of [...docs].sort((a, b) => cmp(a.source.id, b.source.id))) {
52
+ const byId = new Map(doc.evidence.map((e) => [e.id, e]));
53
+ const bySubject = new Map();
54
+ for (const f of doc.facts)
55
+ bySubject.set(f.subject, [...(bySubject.get(f.subject) ?? []), f]);
56
+ for (const entity of doc.entities
57
+ .filter((e) => e.kind === "doc-page")
58
+ .sort((a, b) => cmp(a.ref, b.ref))) {
59
+ const facts = bySubject.get(entity.ref) ?? [];
60
+ const find = (path) => facts.find((f) => f.path === path);
61
+ const titleFact = find("doc.title") ?? facts[0];
62
+ if (!titleFact) {
63
+ skipped.push({ ref: entity.ref, sourceId: doc.source.id, reason: "the page has no facts to cite" });
64
+ continue;
65
+ }
66
+ const title = typeof find("doc.title")?.value === "string" ? find("doc.title")?.value : entity.name;
67
+ const documentId = entity.ref.replace(/^doc:/, "");
68
+ const stem = (documentId.split("/").pop() ?? documentId).replace(/\.[^.]*$/, "");
69
+ const cite = (ids) => {
70
+ for (const id of ids) {
71
+ const found = byId.get(id);
72
+ if (found)
73
+ evidence.set(id, found);
74
+ }
75
+ };
76
+ const text = [title, stem].join(" \n ").toLowerCase();
77
+ const kind = classify(text);
78
+ const segments = documentId
79
+ .replace(/\.[^./]*$/, "")
80
+ .split("/")
81
+ .map(slugSegment);
82
+ let id = `guideline:${segments.join("/")}`;
83
+ for (let n = 2; used.has(id); n++)
84
+ id = `guideline:${segments.join("/")}-${n}`;
85
+ used.add(id);
86
+ const facts_ = {};
87
+ const status = titleFact.origin === "extracted" ? "discovered" : "documented";
88
+ facts_.name = { status, evidenceIds: titleFact.evidenceIds };
89
+ facts_.statement = { status, evidenceIds: titleFact.evidenceIds };
90
+ facts_["references.0"] = { status: "discovered", evidenceIds: titleFact.evidenceIds };
91
+ cite(titleFact.evidenceIds);
92
+ const kindEvidence = [...titleFact.evidenceIds];
93
+ const kindInference = {
94
+ rule: kind.kind === "reference" ? "document.classify.fallback" : "document.classify.keyword",
95
+ confidence: kind.confidence,
96
+ reasoning: kind.kind === "reference"
97
+ ? "No keyword in the title or file name identifies a more specific kind of page"
98
+ : `The title or file name contains "${kind.matched}", which marks a ${kind.kind} page`,
99
+ inputs: ["doc.title"],
100
+ evidenceIds: kindEvidence
101
+ };
102
+ facts_.kind = inferredFact(kindInference);
103
+ cite(kindEvidence);
104
+ const stemWords = words(stem);
105
+ const titleWords = words(title);
106
+ const applies = [];
107
+ for (const c of [...components].sort((a, b) => cmp(a.id, b.id))) {
108
+ const cw = words(c.name);
109
+ if (stemWords.join(" ") === cw.join(" ") && cw.length > 0)
110
+ applies.push({
111
+ id: c.id,
112
+ confidence: 0.8,
113
+ reason: `The file name "${stem}" is the component name "${c.name}"`
114
+ });
115
+ else if (containsWords(titleWords, cw))
116
+ applies.push({
117
+ id: c.id,
118
+ confidence: 0.6,
119
+ reason: `The title "${title}" names the component "${c.name}"`
120
+ });
121
+ }
122
+ const exact = applies
123
+ .filter((a) => a.confidence === 0.8)
124
+ .map((a) => words(components.find((c) => c.id === a.id)?.name ?? ""));
125
+ const covered = (id) => {
126
+ const cw = words(components.find((c) => c.id === id)?.name ?? "");
127
+ return exact.some((e) => e.length > cw.length && containsWords(e, cw));
128
+ };
129
+ const kept = applies.filter((a) => a.confidence === 0.8 || !covered(a.id));
130
+ kept.forEach((a, index) => {
131
+ facts_[`appliesTo.${index}`] = inferredFact({
132
+ rule: "document.names-component",
133
+ confidence: a.confidence,
134
+ reasoning: a.reason,
135
+ inputs: ["doc.title"],
136
+ evidenceIds: titleFact.evidenceIds
137
+ });
138
+ });
139
+ guidelines.push({
140
+ id: id,
141
+ name: title,
142
+ kind: kind.kind,
143
+ statement: title,
144
+ appliesTo: kept.map((a) => a.id),
145
+ examples: [],
146
+ references: [{ sourceId: doc.source.id, documentId, title }],
147
+ facts: Object.fromEntries(Object.entries(facts_).sort((a, b) => cmp(a[0], b[0])))
148
+ });
149
+ }
150
+ }
151
+ return {
152
+ guidelines: guidelines.sort((a, b) => cmp(a.id, b.id)),
153
+ evidence: [...evidence.values()].sort((a, b) => cmp(a.id, b.id)),
154
+ skipped
155
+ };
156
+ }
157
+ //# sourceMappingURL=documents.js.map
@@ -0,0 +1,42 @@
1
+ import { DiagnosticCollector } from "../shared/index.js";
2
+ import { assertAdditive } from "./additive.js";
3
+ import { inferCompounds } from "./compounds.js";
4
+ import { classifyDocuments } from "./documents.js";
5
+ import { cmp } from "./infer.js";
6
+ import { classifyTokens } from "./tokens.js";
7
+ /**
8
+ * Adds inferred semantics to a reconciled contract: token categories, guidelines from documentation pages,
9
+ * and compound-component containment. Every output is an `inferred` fact with a rule, reasoning, confidence
10
+ * and existing evidence. The result is checked to be purely additive; if it is not, the input contract is
11
+ * returned unchanged with an error. Nothing here promotes a fact to `approved` or creates a policy.
12
+ */
13
+ export function enrich(input) {
14
+ const diagnostics = new DiagnosticCollector();
15
+ const documents = classifyDocuments(input.docs, input.contract.components);
16
+ for (const s of documents.skipped)
17
+ diagnostics.info("semantic.page-skipped", `${s.ref} was not classified: ${s.reason}`, {
18
+ sourceId: s.sourceId,
19
+ entityId: s.ref
20
+ });
21
+ const candidate = {
22
+ ...input.contract,
23
+ components: inferCompounds(input.contract.components),
24
+ tokens: classifyTokens(input.contract.tokens),
25
+ guidelines: [...input.contract.guidelines, ...documents.guidelines].sort((a, b) => cmp(a.id, b.id))
26
+ };
27
+ const violations = assertAdditive(input.contract, candidate);
28
+ if (violations.length > 0) {
29
+ for (const v of violations)
30
+ diagnostics.error("semantic.not-additive", `Enrichment would alter existing data: ${v}`);
31
+ return { contract: input.contract, evidence: [...input.evidence], diagnostics: diagnostics.toArray() };
32
+ }
33
+ const evidence = new Map(input.evidence.map((e) => [e.id, e]));
34
+ for (const e of documents.evidence)
35
+ evidence.set(e.id, e);
36
+ return {
37
+ contract: candidate,
38
+ evidence: [...evidence.values()].sort((a, b) => cmp(a.id, b.id)),
39
+ diagnostics: diagnostics.toArray()
40
+ };
41
+ }
42
+ //# sourceMappingURL=enrich.js.map
@@ -0,0 +1,7 @@
1
+ export { assertAdditive } from "./additive.js";
2
+ export { inferCompounds } from "./compounds.js";
3
+ export { classifyDocuments } from "./documents.js";
4
+ export { enrich } from "./enrich.js";
5
+ export { inferredFact } from "./infer.js";
6
+ export { classifyTokens } from "./tokens.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The only way the engine writes a fact. An inference without evidence, reasoning or a usable confidence
3
+ * is a bug in a rule, not something to store: it throws, so a rule can never emit an unattributable claim.
4
+ */
5
+ export function inferredFact(i) {
6
+ if (i.evidenceIds.length === 0)
7
+ throw new Error(`rule ${i.rule} produced an inference without evidence`);
8
+ if (i.reasoning.trim() === "")
9
+ throw new Error(`rule ${i.rule} produced an inference without reasoning`);
10
+ if (!(i.confidence > 0 && i.confidence < 1))
11
+ throw new Error(`rule ${i.rule} produced confidence ${i.confidence}; inferences are never certain`);
12
+ return {
13
+ status: "inferred",
14
+ evidenceIds: [...new Set(i.evidenceIds)].sort(),
15
+ confidence: i.confidence,
16
+ derivation: { rule: i.rule, reasoning: i.reasoning, inputs: [...new Set(i.inputs)].sort() }
17
+ };
18
+ }
19
+ export function words(text) {
20
+ return text
21
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
22
+ .toLowerCase()
23
+ .split(/[^a-z0-9]+/)
24
+ .filter(Boolean);
25
+ }
26
+ export function cmp(a, b) {
27
+ return a < b ? -1 : a > b ? 1 : 0;
28
+ }
29
+ export function slugSegment(text) {
30
+ const s = text
31
+ .toLowerCase()
32
+ .replace(/[^a-z0-9._-]+/g, "-")
33
+ .replace(/^[^a-z0-9]+/, "")
34
+ .replace(/-{2,}/g, "-")
35
+ .replace(/[-.]+$/, "");
36
+ return s === "" ? "x" : s;
37
+ }
38
+ //# sourceMappingURL=infer.js.map
@@ -0,0 +1,120 @@
1
+ import { cmp, inferredFact, words } from "./infer.js";
2
+ const DTCG = {
3
+ color: "color",
4
+ shadow: "shadow",
5
+ typography: "typography",
6
+ fontFamily: "typography",
7
+ fontWeight: "typography",
8
+ fontSize: "typography",
9
+ lineHeight: "typography",
10
+ letterSpacing: "typography",
11
+ border: "border",
12
+ strokeStyle: "border",
13
+ duration: "motion",
14
+ cubicBezier: "motion",
15
+ transition: "motion"
16
+ };
17
+ const NAME_PREFIX = {
18
+ color: "color",
19
+ colour: "color",
20
+ space: "spacing",
21
+ spacing: "spacing",
22
+ gap: "spacing",
23
+ margin: "spacing",
24
+ padding: "spacing",
25
+ size: "sizing",
26
+ sizing: "sizing",
27
+ width: "sizing",
28
+ height: "sizing",
29
+ font: "typography",
30
+ typography: "typography",
31
+ line: "typography",
32
+ letter: "typography",
33
+ radius: "border",
34
+ border: "border",
35
+ stroke: "border",
36
+ shadow: "shadow",
37
+ elevation: "shadow",
38
+ breakpoint: "breakpoint",
39
+ screen: "breakpoint",
40
+ duration: "motion",
41
+ easing: "motion",
42
+ motion: "motion",
43
+ transition: "motion",
44
+ animation: "motion"
45
+ };
46
+ function classify(token) {
47
+ const evidence = (path) => token.facts[path]?.evidenceIds ?? [];
48
+ const named = evidence("name");
49
+ const dtcg = token.extensions?.dtcg?.type;
50
+ if (typeof dtcg === "string" && DTCG[dtcg] && evidence("extensions.dtcg.type").length > 0) {
51
+ return {
52
+ category: DTCG[dtcg],
53
+ inference: {
54
+ rule: "token-category.dtcg-type",
55
+ confidence: 0.95,
56
+ reasoning: `The token declares the DTCG type "${dtcg}", which is the ${DTCG[dtcg]} category`,
57
+ inputs: ["extensions.dtcg.type"],
58
+ evidenceIds: evidence("extensions.dtcg.type")
59
+ }
60
+ };
61
+ }
62
+ const css = token.extensions?.css;
63
+ if (css) {
64
+ const kinds = Object.entries(css)
65
+ .filter(([, v]) => typeof v === "object" && v !== null && v.valueKind === "color")
66
+ .map(([k]) => k);
67
+ const path = kinds.map((k) => `extensions.css.${k}.valueKind`).find((p) => evidence(p).length > 0);
68
+ if (path)
69
+ return {
70
+ category: "color",
71
+ inference: {
72
+ rule: "token-category.value-kind",
73
+ confidence: 0.9,
74
+ reasoning: "The CSS value is a colour literal or colour function",
75
+ inputs: [path],
76
+ evidenceIds: evidence(path)
77
+ }
78
+ };
79
+ }
80
+ const first = words(token.name.replace(/^-+/, ""))[0];
81
+ if (first && NAME_PREFIX[first] && named.length > 0)
82
+ return {
83
+ category: NAME_PREFIX[first],
84
+ inference: {
85
+ rule: "token-category.name-prefix",
86
+ confidence: 0.7,
87
+ reasoning: `The token name starts with "${first}", which conventionally names the ${NAME_PREFIX[first]} category`,
88
+ inputs: ["name"],
89
+ evidenceIds: named
90
+ }
91
+ };
92
+ return named.length > 0
93
+ ? {
94
+ category: "other",
95
+ inference: {
96
+ rule: "token-category.fallback",
97
+ confidence: 0.2,
98
+ reasoning: "No declared type, value kind or conventional name prefix identifies a category",
99
+ inputs: ["name"],
100
+ evidenceIds: named
101
+ }
102
+ }
103
+ : undefined;
104
+ }
105
+ /** Sets `category` on tokens that have only the reconciliation placeholder, and records why. */
106
+ export function classifyTokens(tokens) {
107
+ return tokens.map((token) => {
108
+ if (token.facts.category)
109
+ return token;
110
+ const result = classify(token);
111
+ if (!result)
112
+ return token;
113
+ return {
114
+ ...token,
115
+ category: result.category,
116
+ facts: Object.fromEntries(Object.entries({ ...token.facts, category: inferredFact(result.inference) }).sort((a, b) => cmp(a[0], b[0])))
117
+ };
118
+ });
119
+ }
120
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Canonical JSON: object keys sorted by UTF-16 code unit, no insignificant whitespace
4
+ * (or fixed-width indentation when `indent > 0`). Properties whose value is `undefined`
5
+ * are omitted, matching JSON.stringify. Non-finite numbers and `undefined` array items throw.
6
+ * Array order is preserved: producers are responsible for sorting arrays.
7
+ */
8
+ export function canonicalStringify(value, indent = 0) {
9
+ return write(value, indent, 0);
10
+ }
11
+ export function sha256Hex(text) {
12
+ return createHash("sha256").update(text, "utf8").digest("hex");
13
+ }
14
+ export function hashJson(value) {
15
+ return sha256Hex(canonicalStringify(value));
16
+ }
17
+ function write(value, indent, depth) {
18
+ if (value === null)
19
+ return "null";
20
+ switch (typeof value) {
21
+ case "string":
22
+ return JSON.stringify(value);
23
+ case "boolean":
24
+ return value ? "true" : "false";
25
+ case "number":
26
+ if (!Number.isFinite(value))
27
+ throw new TypeError(`Cannot canonicalize non-finite number: ${value}`);
28
+ return JSON.stringify(value);
29
+ case "object":
30
+ return Array.isArray(value) ? writeArray(value, indent, depth) : writeObject(value, indent, depth);
31
+ default:
32
+ throw new TypeError(`Cannot canonicalize value of type ${typeof value}`);
33
+ }
34
+ }
35
+ function writeArray(items, indent, depth) {
36
+ if (items.length === 0)
37
+ return "[]";
38
+ const parts = items.map((item) => {
39
+ if (item === undefined)
40
+ throw new TypeError("Cannot canonicalize undefined array item");
41
+ return write(item, indent, depth + 1);
42
+ });
43
+ return wrap("[", "]", parts, indent, depth);
44
+ }
45
+ function writeObject(object, indent, depth) {
46
+ const record = object;
47
+ const parts = [];
48
+ for (const key of Object.keys(record).sort()) {
49
+ const member = record[key];
50
+ if (member === undefined)
51
+ continue;
52
+ parts.push(`${JSON.stringify(key)}:${indent > 0 ? " " : ""}${write(member, indent, depth + 1)}`);
53
+ }
54
+ if (parts.length === 0)
55
+ return "{}";
56
+ return wrap("{", "}", parts, indent, depth);
57
+ }
58
+ function wrap(open, close, parts, indent, depth) {
59
+ if (indent <= 0)
60
+ return `${open}${parts.join(",")}${close}`;
61
+ const inner = " ".repeat(indent * (depth + 1));
62
+ const outer = " ".repeat(indent * depth);
63
+ return `${open}\n${inner}${parts.join(`,\n${inner}`)}\n${outer}${close}`;
64
+ }
65
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ export const severitySchema = z.enum(["error", "warning", "info"]);
3
+ export const diagnosticLocationSchema = z.strictObject({
4
+ path: z.string().min(1),
5
+ line: z.number().int().positive().optional(),
6
+ column: z.number().int().positive().optional()
7
+ });
8
+ /** Codes are `area.kebab-name`, for example `contract.dangling-reference`. */
9
+ export const diagnosticCodeSchema = z.string().regex(/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/);
10
+ export const diagnosticSchema = z.strictObject({
11
+ code: diagnosticCodeSchema,
12
+ severity: severitySchema,
13
+ message: z.string().min(1),
14
+ sourceId: z.string().min(1).optional(),
15
+ entityId: z.string().min(1).optional(),
16
+ location: diagnosticLocationSchema.optional(),
17
+ hint: z.string().min(1).optional()
18
+ });
19
+ const SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
20
+ export function diagnostic(input) {
21
+ return diagnosticSchema.parse(input);
22
+ }
23
+ export function hasErrors(diagnostics) {
24
+ return diagnostics.some((d) => d.severity === "error");
25
+ }
26
+ /** Total order so diagnostic output is identical regardless of discovery order. */
27
+ export function sortDiagnostics(diagnostics) {
28
+ return [...diagnostics].sort(compareDiagnostics);
29
+ }
30
+ function compareDiagnostics(a, b) {
31
+ return (SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||
32
+ cmp(a.code, b.code) ||
33
+ cmp(a.sourceId ?? "", b.sourceId ?? "") ||
34
+ cmp(a.entityId ?? "", b.entityId ?? "") ||
35
+ cmp(a.location?.path ?? "", b.location?.path ?? "") ||
36
+ (a.location?.line ?? 0) - (b.location?.line ?? 0) ||
37
+ (a.location?.column ?? 0) - (b.location?.column ?? 0) ||
38
+ cmp(a.message, b.message));
39
+ }
40
+ function cmp(a, b) {
41
+ return a < b ? -1 : a > b ? 1 : 0;
42
+ }
43
+ export const DIAGNOSTICS_FILE_VERSION = "1.0.0";
44
+ export const diagnosticsFileSchema = z.strictObject({
45
+ diagnosticsVersion: z.literal(DIAGNOSTICS_FILE_VERSION),
46
+ diagnostics: z.array(diagnosticSchema)
47
+ });
48
+ /** Accumulates diagnostics during a run. Output is always sorted, so discovery order never leaks. */
49
+ export class DiagnosticCollector {
50
+ items = [];
51
+ defaults;
52
+ constructor(defaults = {}) {
53
+ this.defaults = defaults;
54
+ }
55
+ add(input) {
56
+ this.items.push(diagnostic({ ...this.defaults, ...input }));
57
+ }
58
+ error(code, message, extra = {}) {
59
+ this.add({ ...extra, code, severity: "error", message });
60
+ }
61
+ warning(code, message, extra = {}) {
62
+ this.add({ ...extra, code, severity: "warning", message });
63
+ }
64
+ info(code, message, extra = {}) {
65
+ this.add({ ...extra, code, severity: "info", message });
66
+ }
67
+ addAll(inputs) {
68
+ for (const input of inputs)
69
+ this.add(input);
70
+ }
71
+ hasErrors() {
72
+ return hasErrors(this.items);
73
+ }
74
+ toArray() {
75
+ return sortDiagnostics(this.items);
76
+ }
77
+ }
78
+ //# sourceMappingURL=diagnostic.js.map