@invariant-ui/cli 0.0.0-stage → 0.2.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 (190) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +250 -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/behavior.js +75 -0
  84. package/dist/compiler-react/detect.js +558 -0
  85. package/dist/compiler-react/emit.js +188 -0
  86. package/dist/compiler-react/index.js +2 -0
  87. package/dist/compiler-react/props.js +157 -0
  88. package/dist/compiler-react/styling.js +138 -0
  89. package/dist/compiler-react/usage.js +116 -0
  90. package/dist/compiler-react/variants.js +111 -0
  91. package/dist/compiler-storybook/adapter.js +253 -0
  92. package/dist/compiler-storybook/csf.js +521 -0
  93. package/dist/compiler-storybook/emit.js +202 -0
  94. package/dist/compiler-storybook/index-json.js +41 -0
  95. package/dist/compiler-storybook/index.js +5 -0
  96. package/dist/compiler-storybook/mdx.js +120 -0
  97. package/dist/compiler-tokens/adapter.js +158 -0
  98. package/dist/compiler-tokens/css.js +59 -0
  99. package/dist/compiler-tokens/index.js +5 -0
  100. package/dist/compiler-tokens/json-lines.js +106 -0
  101. package/dist/compiler-tokens/json-tokens.js +52 -0
  102. package/dist/compiler-vue/adapter.js +107 -0
  103. package/dist/compiler-vue/analyze.js +344 -0
  104. package/dist/compiler-vue/declared.js +343 -0
  105. package/dist/compiler-vue/emit.js +129 -0
  106. package/dist/compiler-vue/index.js +5 -0
  107. package/dist/compiler-vue/script.js +472 -0
  108. package/dist/compiler-vue/sfc.js +44 -0
  109. package/dist/compiler-vue/template.js +73 -0
  110. package/dist/contract/archetype-checks.js +153 -0
  111. package/dist/contract/components.js +102 -0
  112. package/dist/contract/conflicts.js +39 -0
  113. package/dist/contract/contract.js +46 -0
  114. package/dist/contract/evidence.js +84 -0
  115. package/dist/contract/fact-path.js +39 -0
  116. package/dist/contract/facts.js +62 -0
  117. package/dist/contract/index.js +16 -0
  118. package/dist/contract/integrity.js +21 -0
  119. package/dist/contract/json-schema.js +11 -0
  120. package/dist/contract/knowledge.js +95 -0
  121. package/dist/contract/pattern-checks.js +254 -0
  122. package/dist/contract/patterns.js +225 -0
  123. package/dist/contract/primitives.js +81 -0
  124. package/dist/contract/sources.js +30 -0
  125. package/dist/contract/tokens.js +41 -0
  126. package/dist/contract/validate.js +253 -0
  127. package/dist/contract-query/archetype.js +188 -0
  128. package/dist/contract-query/index.js +6 -0
  129. package/dist/contract-query/patterns.js +298 -0
  130. package/dist/contract-query/search.js +132 -0
  131. package/dist/contract-query/system.js +148 -0
  132. package/dist/contract-query/views.js +288 -0
  133. package/dist/governance/decisions.js +229 -0
  134. package/dist/governance/index.js +5 -0
  135. package/dist/governance/patterns-file.js +156 -0
  136. package/dist/governance/patterns.js +510 -0
  137. package/dist/governance/policies.js +148 -0
  138. package/dist/mcp/index.js +7 -0
  139. package/dist/mcp/loader.js +70 -0
  140. package/dist/mcp/protocol.js +27 -0
  141. package/dist/mcp/resources.js +138 -0
  142. package/dist/mcp/server.js +177 -0
  143. package/dist/mcp/stdio.js +28 -0
  144. package/dist/mcp/tools.js +267 -0
  145. package/dist/reconciliation/code.js +62 -0
  146. package/dist/reconciliation/components.js +401 -0
  147. package/dist/reconciliation/conflicts.js +126 -0
  148. package/dist/reconciliation/facts.js +41 -0
  149. package/dist/reconciliation/index.js +7 -0
  150. package/dist/reconciliation/reconcile.js +134 -0
  151. package/dist/reconciliation/resolve.js +158 -0
  152. package/dist/reconciliation/review.js +81 -0
  153. package/dist/reconciliation/storybook.js +370 -0
  154. package/dist/reconciliation/tokens.js +179 -0
  155. package/dist/reconciliation/util.js +55 -0
  156. package/dist/semantic-engine/additive.js +51 -0
  157. package/dist/semantic-engine/compounds.js +91 -0
  158. package/dist/semantic-engine/documents.js +157 -0
  159. package/dist/semantic-engine/enrich.js +42 -0
  160. package/dist/semantic-engine/index.js +7 -0
  161. package/dist/semantic-engine/infer.js +38 -0
  162. package/dist/semantic-engine/tokens.js +120 -0
  163. package/dist/shared/canonical.js +65 -0
  164. package/dist/shared/diagnostic.js +78 -0
  165. package/dist/shared/glob.js +98 -0
  166. package/dist/shared/index.js +4 -0
  167. package/dist/shared/json.js +2 -0
  168. package/dist/validation/card.js +50 -0
  169. package/dist/validation/discovery/arrangements.js +162 -0
  170. package/dist/validation/discovery/discover.js +70 -0
  171. package/dist/validation/discovery/index.js +8 -0
  172. package/dist/validation/discovery/model.js +127 -0
  173. package/dist/validation/discovery/promote.js +24 -0
  174. package/dist/validation/discovery/reviews.js +63 -0
  175. package/dist/validation/discovery/shapes.js +47 -0
  176. package/dist/validation/discovery/similar-files.js +146 -0
  177. package/dist/validation/discovery/util.js +47 -0
  178. package/dist/validation/index.js +11 -0
  179. package/dist/validation/interaction-facts.js +341 -0
  180. package/dist/validation/interaction-workflows.js +633 -0
  181. package/dist/validation/literals.js +251 -0
  182. package/dist/validation/pattern-rules.js +608 -0
  183. package/dist/validation/policy.js +241 -0
  184. package/dist/validation/report.js +163 -0
  185. package/dist/validation/rules.js +269 -0
  186. package/dist/validation/scan.js +295 -0
  187. package/dist/validation/text.js +34 -0
  188. package/dist/validation/validate-code.js +513 -0
  189. package/dist/validation/validate-usage.js +196 -0
  190. package/package.json +38 -4
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Minimal glob matcher for POSIX relative paths: `**`, `*`, `?`, `{a,b}`, `[abc]`, and the extglob
3
+ * `@(a|b)` that Storybook story globs commonly use. Other extglobs are not supported.
4
+ */
5
+ export function globToRegExp(pattern) {
6
+ return new RegExp(`^${convert(pattern.replace(/^\.\//, ""))}$`);
7
+ }
8
+ export function matchesAny(path, matchers) {
9
+ return matchers.some((m) => m.test(path));
10
+ }
11
+ function convert(p) {
12
+ let out = "";
13
+ for (let i = 0; i < p.length; i++) {
14
+ const c = p[i];
15
+ if (c === "*") {
16
+ if (p[i + 1] === "*") {
17
+ if (p[i + 2] === "/") {
18
+ out += "(?:.*/)?";
19
+ i += 2;
20
+ }
21
+ else {
22
+ out += ".*";
23
+ i += 1;
24
+ }
25
+ }
26
+ else
27
+ out += "[^/]*";
28
+ }
29
+ else if (c === "?") {
30
+ out += "[^/]";
31
+ }
32
+ else if (c === "{") {
33
+ const end = matching(p, i, "{", "}");
34
+ if (end < 0)
35
+ out += "\\{";
36
+ else {
37
+ out += `(?:${splitTopLevel(p.slice(i + 1, end), ",")
38
+ .map(convert)
39
+ .join("|")})`;
40
+ i = end;
41
+ }
42
+ }
43
+ else if (c === "@" && p[i + 1] === "(") {
44
+ const end = matching(p, i + 1, "(", ")");
45
+ if (end < 0)
46
+ out += "@";
47
+ else {
48
+ out += `(?:${splitTopLevel(p.slice(i + 2, end), "|")
49
+ .map(convert)
50
+ .join("|")})`;
51
+ i = end;
52
+ }
53
+ }
54
+ else if (c === "[") {
55
+ const end = p.indexOf("]", i + 1);
56
+ if (end < 0)
57
+ out += "\\[";
58
+ else {
59
+ out += p.slice(i, end + 1);
60
+ i = end;
61
+ }
62
+ }
63
+ else {
64
+ out += c.replace(/[.+^$()|\\/]/g, (m) => (m === "/" ? "/" : `\\${m}`));
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ function matching(p, start, open, close) {
70
+ let depth = 0;
71
+ for (let i = start; i < p.length; i++) {
72
+ if (p[i] === open)
73
+ depth++;
74
+ else if (p[i] === close && --depth === 0)
75
+ return i;
76
+ }
77
+ return -1;
78
+ }
79
+ function splitTopLevel(text, separator) {
80
+ const parts = [];
81
+ let depth = 0;
82
+ let current = "";
83
+ for (const c of text) {
84
+ if (c === "{" || c === "(")
85
+ depth++;
86
+ if (c === "}" || c === ")")
87
+ depth--;
88
+ if (c === separator && depth === 0) {
89
+ parts.push(current);
90
+ current = "";
91
+ }
92
+ else
93
+ current += c;
94
+ }
95
+ parts.push(current);
96
+ return parts;
97
+ }
98
+ //# sourceMappingURL=glob.js.map
@@ -0,0 +1,4 @@
1
+ export { canonicalStringify, hashJson, sha256Hex } from "./canonical.js";
2
+ export { DIAGNOSTICS_FILE_VERSION, DiagnosticCollector, diagnostic, diagnosticCodeSchema, diagnosticLocationSchema, diagnosticSchema, diagnosticsFileSchema, hasErrors, severitySchema, sortDiagnostics } from "./diagnostic.js";
3
+ export { globToRegExp, matchesAny } from "./glob.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1,50 @@
1
+ import { sortViolations } from "./report.js";
2
+ /**
3
+ * The most findings a card shows before pointing at the rest. Claude Code caps hook strings at 10,000
4
+ * characters, and a person reads this in a chat: ten is as much as anyone acts on at once.
5
+ */
6
+ export const CARD_FINDINGS_LIMIT = 10;
7
+ const MARK = { error: "✗", warning: "⚠", info: "ℹ" };
8
+ /**
9
+ * The enforcement card: the one finding list every surface shows the agent — the hook's deny reason, the
10
+ * validate tool's text content — so the chat the person is watching says the same thing everywhere it is
11
+ * checked. Rule, what is wrong, where, and the fix, in file order and capped so it stays readable in a
12
+ * message. Show {@link passCard} instead when there is nothing to fix.
13
+ */
14
+ export function enforcementCard(input) {
15
+ const { findings, contractName, next } = input;
16
+ const file = input.file ?? "the snippet";
17
+ const limit = input.limit ?? CARD_FINDINGS_LIMIT;
18
+ const count = (severity) => findings.filter((f) => f.severity === severity).length;
19
+ const errors = count("error");
20
+ const warnings = count("warning");
21
+ const info = count("info");
22
+ const counts = [
23
+ errors > 0 ? `${errors} error${errors === 1 ? "" : "s"}` : undefined,
24
+ warnings > 0 ? `${warnings} warning${warnings === 1 ? "" : "s"}` : undefined,
25
+ info > 0 ? `${info} info` : undefined
26
+ ]
27
+ .filter(Boolean)
28
+ .join(", ");
29
+ const lines = [
30
+ `invariant ${input.blocked === true ? "blocked" : "checked"} ${file}: ${counts} against the ${contractName} contract`,
31
+ ""
32
+ ];
33
+ for (const f of sortViolations([...findings]).slice(0, limit)) {
34
+ const where = f.line !== undefined ? ` (line ${f.line})` : "";
35
+ lines.push(`${MARK[f.severity]} ${f.code}: ${f.message}${where}`);
36
+ if (f.suggestion)
37
+ lines.push(` fix: ${f.suggestion}`);
38
+ }
39
+ if (findings.length > limit)
40
+ lines.push(input.more ? input.more(findings.length - limit) : `…and ${findings.length - limit} more`);
41
+ lines.push("");
42
+ lines.push(`${next} Facts and fixes come from the compiled contract, not guesses.`);
43
+ return lines.join("\n");
44
+ }
45
+ /** The one-line confirmation of a clean pass: what was checked, and that it follows the contract. */
46
+ export function passCard(input) {
47
+ const n = input.componentsChecked.length;
48
+ return `invariant ✓ ${input.file ?? "the snippet"} follows the ${input.contractName} contract (${n === 0 ? "no" : n} component use${n === 1 ? "" : "s"} checked)`;
49
+ }
50
+ //# sourceMappingURL=card.js.map
@@ -0,0 +1,162 @@
1
+ import { walk } from "./shapes.js";
2
+ import { candidateId, capEvidence, listNames, slug, uniqueName } from "./util.js";
3
+ const flags = (n) => `${n.conditional ? "?" : ""}${n.repeated ? "*" : ""}${n.opaque ? "~" : ""}`;
4
+ const bare = (id) => id.replace(/^component:/, "");
5
+ /**
6
+ * The canonical form of a node and what is inside it, to a fixed depth: `card(card.header,card.body*)`.
7
+ * `?` marks an element that may not render, `*` one that may render many times, `~` one whose children may be
8
+ * incomplete. Two nodes with the same form have the same shape of design-system elements and nothing more.
9
+ */
10
+ export function keyOf(node, depth, maxDepth) {
11
+ const head = `${bare(node.id)}${flags(node)}`;
12
+ if (node.children.length === 0)
13
+ return head;
14
+ if (depth >= maxDepth)
15
+ return `${head}(…)`;
16
+ return `${head}(${node.children.map((c) => keyOf(c, depth + 1, maxDepth)).join(",")})`;
17
+ }
18
+ function treeOf(node, depth, maxDepth) {
19
+ return {
20
+ id: node.id,
21
+ name: node.name,
22
+ optional: node.conditional || node.repeated,
23
+ opaque: node.opaque || (depth >= maxDepth && node.children.length > 0),
24
+ children: depth >= maxDepth ? [] : node.children.map((c) => treeOf(c, depth + 1, maxDepth))
25
+ };
26
+ }
27
+ const pos = (file, n) => `${file}:${n.line}:${n.column}`;
28
+ function parts(root, maxDepth) {
29
+ const used = new Set();
30
+ const seen = new Map();
31
+ const order = [];
32
+ const visit = (n, depth) => {
33
+ const role = n.conditional || n.repeated ? "optional" : "required";
34
+ const held = seen.get(n.id);
35
+ if (held === undefined) {
36
+ seen.set(n.id, role);
37
+ order.push(n);
38
+ }
39
+ else if (role === "required")
40
+ seen.set(n.id, "required");
41
+ if (depth < maxDepth)
42
+ for (const c of n.children)
43
+ visit(c, depth + 1);
44
+ };
45
+ visit(root, 0);
46
+ return order.map((n) => ({
47
+ name: uniqueName(slug(n.name), used),
48
+ ref: n.id,
49
+ role: seen.get(n.id)
50
+ }));
51
+ }
52
+ /**
53
+ * Method: exact structure. Every design-system element with something inside it is an arrangement; arrangements
54
+ * with the same canonical form are grouped. A group that recurs in enough files and enough places is a
55
+ * candidate. Nothing here compares meaning, so two arrangements are the same only if they are written the same.
56
+ */
57
+ export function findArrangements(files, options) {
58
+ const groups = new Map();
59
+ for (const f of files)
60
+ for (const node of walk(f.roots)) {
61
+ if (node.children.length === 0)
62
+ continue;
63
+ const key = keyOf(node, 0, options.maxDepth);
64
+ const group = groups.get(key) ?? { key, nodes: [] };
65
+ group.nodes.push({ file: f.file, node });
66
+ groups.set(key, group);
67
+ }
68
+ const kept = [...groups.values()]
69
+ .filter((g) => g.nodes.length >= options.minUses && new Set(g.nodes.map((n) => n.file)).size >= options.minFiles)
70
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
71
+ const inside = new Map();
72
+ for (const g of kept) {
73
+ const set = new Set();
74
+ for (const { file, node } of g.nodes)
75
+ for (const d of walk(node.children))
76
+ set.add(pos(file, d));
77
+ inside.set(g.key, set);
78
+ }
79
+ const taken = new Set();
80
+ const out = kept.map((g) => {
81
+ const first = g.nodes[0];
82
+ const fileCount = new Set(g.nodes.map((n) => n.file)).size;
83
+ const root = first.node;
84
+ const children = [...new Set(walk(root.children).map((c) => c.name))];
85
+ const name = `${root.name} with ${listNames(children)}`;
86
+ const evidence = g.nodes.map(({ file, node }) => ({
87
+ file,
88
+ line: node.line,
89
+ column: node.column,
90
+ detail: `<${node.name}> containing ${listNames([...new Set(node.children.map((c) => c.name))])}`
91
+ }));
92
+ const capped = capEvidence(evidence);
93
+ const guarded = g.key.includes("~");
94
+ return {
95
+ id: candidateId("arrangement", g.key, taken),
96
+ kind: "arrangement",
97
+ status: "proposed",
98
+ basis: "inferred",
99
+ equivalence: "not-established",
100
+ suggestedName: name,
101
+ nameSource: "derived",
102
+ summary: `${root.name} containing ${listNames(children)}, written the same way ${g.nodes.length} times in ${fileCount} files.`,
103
+ explanation: [
104
+ `Every design-system element with something inside it was compared with the others by its canonical form, to a depth of ${options.maxDepth}. This form, ${g.key}, appears ${g.nodes.length} times in ${fileCount} files.`,
105
+ "The form counts design-system elements only. Markup and components from elsewhere between them are ignored, so an element inside another may not be its direct child.",
106
+ "This shows the arrangement recurs. It does not show it is intended, correct, or a standard anyone chose.",
107
+ ...(guarded
108
+ ? [
109
+ "Some elements take their children from elsewhere (marked ~), so what is inside them may be incomplete."
110
+ ]
111
+ : [])
112
+ ],
113
+ support: { files: fileCount, occurrences: g.nodes.length },
114
+ structure: {
115
+ key: g.key,
116
+ common: [],
117
+ frequent: [],
118
+ variable: [],
119
+ variableTruncated: false,
120
+ tree: treeOf(root, 0, options.maxDepth)
121
+ },
122
+ evidence: capped.evidence,
123
+ evidenceTruncated: capped.truncated,
124
+ definition: {
125
+ name,
126
+ category: "composition",
127
+ description: `A recurring arrangement of ${listNames([root.name, ...children])}, found ${g.nodes.length} times in ${fileCount} files by invariant discover. Review before relying on it.`,
128
+ composition: { parts: parts(root, options.maxDepth) }
129
+ },
130
+ derivation: {
131
+ method: "exact-structure",
132
+ version: 1,
133
+ parameters: { minFiles: options.minFiles, minUses: options.minUses, maxDepth: options.maxDepth }
134
+ }
135
+ };
136
+ });
137
+ // A candidate whose every occurrence lies inside another's is the same finding seen from lower down. Say so,
138
+ // and keep it.
139
+ const byKey = new Map(kept.map((g, i) => [g.key, out[i]]));
140
+ for (const [i, g] of kept.entries()) {
141
+ const mine = g.nodes.map((n) => pos(n.file, n.node));
142
+ let best;
143
+ for (const other of kept) {
144
+ if (other.key === g.key)
145
+ continue;
146
+ const set = inside.get(other.key);
147
+ if (!mine.every((p) => set.has(p)))
148
+ continue;
149
+ const candidate = byKey.get(other.key);
150
+ if (!best ||
151
+ candidate.support.occurrences > best.support.occurrences ||
152
+ (candidate.support.occurrences === best.support.occurrences && candidate.id < best.id))
153
+ best = candidate;
154
+ }
155
+ if (best)
156
+ out[i].subsumedBy = best.id;
157
+ }
158
+ return out.sort((a, b) => b.support.files - a.support.files ||
159
+ b.support.occurrences - a.support.occurrences ||
160
+ (a.structure.key < b.structure.key ? -1 : 1));
161
+ }
162
+ //# sourceMappingURL=arrangements.js.map
@@ -0,0 +1,70 @@
1
+ import { buildTree } from "../validate-code.js";
2
+ import { findArrangements } from "./arrangements.js";
3
+ import { CANDIDATES_FILE_VERSION } from "./model.js";
4
+ import { applyReviews } from "./reviews.js";
5
+ import { shapeOf } from "./shapes.js";
6
+ import { findSimilarFiles } from "./similar-files.js";
7
+ export const DEFAULT_OPTIONS = { minFiles: 3, minUses: 3, similarity: 0.7, maxDepth: 3 };
8
+ const READABLE = /\.(tsx|jsx)$/i;
9
+ /**
10
+ * Finds structures that recur in application code, as candidates for people to review. It reads React JSX by
11
+ * syntax and runs nothing. It resolves elements to the contract's components, so it needs a compiled contract.
12
+ * The result is the same for the same files, contract and options. Nothing it produces is approved or enforced.
13
+ */
14
+ export function discover(input) {
15
+ const options = { ...DEFAULT_OPTIONS, ...input.options };
16
+ const diagnostics = [];
17
+ const files = [...input.files].sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
18
+ const shapes = [];
19
+ let unreadable = 0;
20
+ let empty = 0;
21
+ for (const { file, code } of files) {
22
+ if (!READABLE.test(file)) {
23
+ unreadable++;
24
+ continue;
25
+ }
26
+ const shape = shapeOf(file, buildTree(input.ds, code, file));
27
+ if (shape.designSystemElements === 0)
28
+ empty++;
29
+ else
30
+ shapes.push(shape);
31
+ }
32
+ if (unreadable > 0)
33
+ diagnostics.push({
34
+ code: "discovery-unsupported-files",
35
+ severity: "info",
36
+ message: `${unreadable} file(s) were skipped: discovery reads .tsx and .jsx files only. Vue, Angular, HTML and plain script files are not analyzed.`
37
+ });
38
+ if (empty > 0)
39
+ diagnostics.push({
40
+ code: "discovery-no-design-system-elements",
41
+ severity: "info",
42
+ message: `${empty} file(s) use no component from the contract, so they say nothing about how the design system is composed.`
43
+ });
44
+ if (shapes.length === 0)
45
+ diagnostics.push({
46
+ code: "discovery-nothing-to-analyze",
47
+ severity: "warning",
48
+ message: "No file used a component from the contract, so there is nothing to compare. Check the paths and that the contract's components are imported from the packages it names."
49
+ });
50
+ const found = [
51
+ ...findArrangements(shapes, options),
52
+ ...findSimilarFiles(shapes, { minFiles: options.minFiles, similarity: options.similarity })
53
+ ];
54
+ const applied = applyReviews(found, input.reviews);
55
+ for (const r of applied.stale)
56
+ diagnostics.push({
57
+ code: "discovery-stale-review",
58
+ severity: "info",
59
+ message: `${r.candidate} was ${r.decision} by ${r.decidedBy}, but discovery no longer finds that structure. The decision is kept in the reviews file.`
60
+ });
61
+ return {
62
+ version: CANDIDATES_FILE_VERSION,
63
+ contract: { contentHash: input.ds.contract.integrity.contentHash },
64
+ parameters: options,
65
+ summary: { files: files.length, filesAnalyzed: shapes.length, candidates: applied.candidates.length },
66
+ candidates: applied.candidates,
67
+ diagnostics
68
+ };
69
+ }
70
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1,8 @@
1
+ export { findArrangements, keyOf } from "./arrangements.js";
2
+ export { DEFAULT_OPTIONS, discover } from "./discover.js";
3
+ export { CANDIDATE_KINDS, CANDIDATE_STATUSES, CANDIDATES_FILE, candidateSchema, candidatesFileSchema, DECISIONS, REVIEWS_FILE, reviewSchema, reviewsFileSchema } from "./model.js";
4
+ export { toDefinition } from "./promote.js";
5
+ export { applyReviews, emptyReviews, latestReview, parseReviews, recordReview, serializeReviews } from "./reviews.js";
6
+ export { shapeOf, walk } from "./shapes.js";
7
+ export { findSimilarFiles, MIN_FEATURES } from "./similar-files.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,127 @@
1
+ import { z } from "zod";
2
+ export const CANDIDATES_FILE = "invariant.candidates.json";
3
+ export const CANDIDATES_FILE_VERSION = 1;
4
+ export const REVIEWS_FILE = "invariant.candidate-reviews.json";
5
+ export const REVIEWS_FILE_VERSION = 1;
6
+ export const CANDIDATE_KINDS = ["arrangement", "similar-files"];
7
+ /** `proposed` until a person decides; the other states come only from a recorded review. */
8
+ export const CANDIDATE_STATUSES = ["proposed", "accepted", "rejected", "deferred"];
9
+ export const DECISIONS = ["accepted", "rejected", "deferred"];
10
+ const occurrence = z.strictObject({
11
+ file: z.string().min(1),
12
+ line: z.number().int().positive(),
13
+ column: z.number().int().positive(),
14
+ detail: z.string().min(1)
15
+ });
16
+ const part = z.strictObject({
17
+ name: z
18
+ .string()
19
+ .regex(/^[a-z0-9][a-z0-9_-]*$/)
20
+ .max(64),
21
+ ref: z.string().min(1),
22
+ role: z.enum(["required", "optional"])
23
+ });
24
+ /** The start of a pattern definition. It has no id, no intent and no approval: those are for a person to give. */
25
+ const definitionDraft = z.strictObject({
26
+ name: z.string().min(1).max(200),
27
+ /** Always `composition`: what the shape is for is the reviewer's call, made when promoting. */
28
+ category: z.literal("composition"),
29
+ description: z.string().min(1).max(2000),
30
+ composition: z.strictObject({ parts: z.array(part).min(1) })
31
+ });
32
+ const shapeTree = z.lazy(() => z.strictObject({
33
+ id: z.string().min(1),
34
+ name: z.string().min(1),
35
+ optional: z.boolean(),
36
+ opaque: z.boolean(),
37
+ children: z.array(shapeTree)
38
+ }));
39
+ const featureCount = z.strictObject({ feature: z.string().min(1), files: z.number().int().positive() });
40
+ export const reviewSchema = z.strictObject({
41
+ candidate: z.string().min(1),
42
+ decision: z.enum(DECISIONS),
43
+ /** Asserted, not authenticated, like every approver in the project. */
44
+ decidedBy: z.string().min(1).max(200),
45
+ decidedAt: z.iso.datetime(),
46
+ reason: z.string().min(1).max(1000).optional(),
47
+ reference: z.string().min(1).max(500).optional(),
48
+ /** Set when the accepted candidate was written into the definitions file, with the id it was given. */
49
+ promotedTo: z.string().min(1).max(200).optional()
50
+ });
51
+ export const candidateSchema = z.strictObject({
52
+ id: z.string().regex(/^candidate:(arrangement|similar-files)\.[0-9a-f]{8}(-\d+)?$/),
53
+ kind: z.enum(CANDIDATE_KINDS),
54
+ status: z.enum(CANDIDATE_STATUSES),
55
+ /** Always `inferred`: discovery observes recurrence, it does not learn what anyone intended. */
56
+ basis: z.literal("inferred"),
57
+ /** Always `not-established`: structural similarity is measured, never taken to mean the same purpose. */
58
+ equivalence: z.literal("not-established"),
59
+ suggestedName: z.string().min(1),
60
+ nameSource: z.literal("derived"),
61
+ summary: z.string().min(1),
62
+ /** Why this was found and what it does and does not say. */
63
+ explanation: z.array(z.string().min(1)).min(1),
64
+ support: z.strictObject({
65
+ files: z.number().int().positive(),
66
+ occurrences: z.number().int().positive()
67
+ }),
68
+ /** For `similar-files`: how alike the members are, measured, not estimated. */
69
+ consistency: z
70
+ .strictObject({
71
+ minimumPairwise: z.number().min(0).max(1),
72
+ meanPairwise: z.number().min(0).max(1)
73
+ })
74
+ .optional(),
75
+ structure: z.strictObject({
76
+ /** `arrangement`: the canonical form of the shape. `similar-files`: the features every member has. */
77
+ key: z.string().min(1),
78
+ common: z.array(z.string()),
79
+ frequent: z.array(featureCount),
80
+ variable: z.array(featureCount),
81
+ variableTruncated: z.boolean(),
82
+ /** `arrangement` only: the same shape as `key`, as data. */
83
+ tree: shapeTree.optional()
84
+ }),
85
+ evidence: z.array(occurrence).min(1),
86
+ evidenceTruncated: z.boolean(),
87
+ definition: definitionDraft,
88
+ derivation: z.strictObject({
89
+ method: z.enum(["exact-structure", "complete-linkage-overlap"]),
90
+ version: z.literal(1),
91
+ parameters: z.record(z.string(), z.union([z.number(), z.string()]))
92
+ }),
93
+ /** Another candidate whose occurrences contain every occurrence of this one. Kept, not dropped. */
94
+ subsumedBy: z.string().optional(),
95
+ /** The latest recorded decision, when there is one. */
96
+ review: reviewSchema.optional()
97
+ });
98
+ export const discoveryDiagnosticSchema = z.strictObject({
99
+ code: z.string().min(1),
100
+ severity: z.enum(["info", "warning"]),
101
+ message: z.string().min(1),
102
+ file: z.string().optional()
103
+ });
104
+ export const candidatesFileSchema = z.strictObject({
105
+ version: z.literal(CANDIDATES_FILE_VERSION),
106
+ /** The contract the components were resolved against. */
107
+ contract: z.strictObject({ contentHash: z.string().min(1) }),
108
+ parameters: z.strictObject({
109
+ minFiles: z.number().int().positive(),
110
+ minUses: z.number().int().positive(),
111
+ similarity: z.number().min(0).max(1),
112
+ maxDepth: z.number().int().positive()
113
+ }),
114
+ summary: z.strictObject({
115
+ files: z.number().int().nonnegative(),
116
+ filesAnalyzed: z.number().int().nonnegative(),
117
+ candidates: z.number().int().nonnegative()
118
+ }),
119
+ candidates: z.array(candidateSchema),
120
+ diagnostics: z.array(discoveryDiagnosticSchema)
121
+ });
122
+ export const reviewsFileSchema = z.strictObject({
123
+ $schema: z.string().optional(),
124
+ version: z.literal(REVIEWS_FILE_VERSION),
125
+ reviews: z.array(reviewSchema)
126
+ });
127
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A pattern definition for an accepted candidate, ready to add to the definitions file. It carries no approval,
3
+ * no intent and no constraints: it records what was observed, who accepted it, and where it came from. Compiling
4
+ * it gives `documented` facts; approving them, and writing rules to enforce, are separate steps for a person.
5
+ */
6
+ export function toDefinition(candidate, input) {
7
+ const review = candidate.review;
8
+ return {
9
+ id: input.id,
10
+ name: input.name ?? candidate.definition.name,
11
+ category: input.category ?? candidate.definition.category,
12
+ description: candidate.definition.description,
13
+ composition: candidate.definition.composition,
14
+ origin: {
15
+ candidate: candidate.id,
16
+ method: candidate.derivation.method,
17
+ support: candidate.support.occurrences,
18
+ files: candidate.support.files,
19
+ ...(review ? { acceptedBy: review.decidedBy, acceptedAt: review.decidedAt } : {}),
20
+ ...(review?.reference ? { reference: review.reference } : {})
21
+ }
22
+ };
23
+ }
24
+ //# sourceMappingURL=promote.js.map
@@ -0,0 +1,63 @@
1
+ import { REVIEWS_FILE_VERSION, reviewsFileSchema } from "./model.js";
2
+ export function parseReviews(text, file) {
3
+ let raw;
4
+ try {
5
+ raw = JSON.parse(text);
6
+ }
7
+ catch (error) {
8
+ return {
9
+ ok: false,
10
+ message: `${file} is not valid JSON: ${error instanceof Error ? error.message : error}`
11
+ };
12
+ }
13
+ const parsed = reviewsFileSchema.safeParse(raw);
14
+ if (parsed.success)
15
+ return { ok: true, value: parsed.data };
16
+ const first = parsed.error.issues[0];
17
+ return {
18
+ ok: false,
19
+ message: `${file}: ${first ? `${first.path.join(".") || "(root)"}: ${first.message}` : "invalid"}`
20
+ };
21
+ }
22
+ export function emptyReviews() {
23
+ return { version: REVIEWS_FILE_VERSION, reviews: [] };
24
+ }
25
+ /**
26
+ * Adds a decision to the history. Earlier decisions stay: the file is a record of who decided what and when,
27
+ * and the latest one for a candidate is the one that counts.
28
+ */
29
+ export function recordReview(file, review) {
30
+ return { ...file, reviews: [...file.reviews, review] };
31
+ }
32
+ /** The decision in force for a candidate: its latest review. */
33
+ export function latestReview(file, id) {
34
+ for (let i = file.reviews.length - 1; i >= 0; i--) {
35
+ const r = file.reviews[i];
36
+ if (r.candidate === id)
37
+ return r;
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * Sets each candidate's status from its latest review. A candidate is identified by what it is, not by where it
43
+ * was found, so a decision keeps applying as files change, and stops applying when the structure itself does.
44
+ */
45
+ export function applyReviews(candidates, file) {
46
+ if (!file)
47
+ return { candidates: [...candidates], stale: [] };
48
+ const known = new Set(candidates.map((c) => c.id));
49
+ const latest = new Map();
50
+ for (const r of file.reviews)
51
+ latest.set(r.candidate, r);
52
+ return {
53
+ candidates: candidates.map((c) => {
54
+ const r = latest.get(c.id);
55
+ return r ? { ...c, status: r.decision, review: r } : c;
56
+ }),
57
+ stale: [...latest.values()].filter((r) => !known.has(r.candidate))
58
+ };
59
+ }
60
+ export function serializeReviews(file) {
61
+ return `${JSON.stringify(file, null, 2)}\n`;
62
+ }
63
+ //# sourceMappingURL=reviews.js.map
@@ -0,0 +1,47 @@
1
+ /** The design-system elements of one scanned file as a forest. Reads the scan only; nothing is evaluated. */
2
+ export function shapeOf(file, tree) {
3
+ const nodes = new Map();
4
+ const roots = [];
5
+ const nearest = (index) => {
6
+ for (let p = tree.uses[index]?.parent; p !== undefined; p = tree.uses[p]?.parent)
7
+ if (nodes.has(p))
8
+ return p;
9
+ return undefined;
10
+ };
11
+ tree.uses.forEach((use, i) => {
12
+ const component = tree.components[i];
13
+ if (tree.kinds[i] !== "component" || !component)
14
+ return;
15
+ const anchor = nearest(i);
16
+ const above = anchor === undefined ? undefined : tree.uses[anchor];
17
+ const node = {
18
+ id: component.id,
19
+ name: component.name,
20
+ line: use.line,
21
+ column: use.column,
22
+ conditional: above !== undefined && (use.exprDepth ?? 0) > (above.exprDepth ?? 0),
23
+ repeated: above !== undefined && (use.loopDepth ?? 0) > (above.loopDepth ?? 0),
24
+ opaque: use.opaque === true,
25
+ children: []
26
+ };
27
+ nodes.set(i, node);
28
+ if (anchor === undefined)
29
+ roots.push(node);
30
+ else
31
+ nodes.get(anchor)?.children.push(node);
32
+ });
33
+ return { file, roots, elements: tree.uses.length, designSystemElements: nodes.size };
34
+ }
35
+ /** Every node of a forest, parents before children, in source order. */
36
+ export function walk(roots) {
37
+ const out = [];
38
+ const visit = (n) => {
39
+ out.push(n);
40
+ for (const c of n.children)
41
+ visit(c);
42
+ };
43
+ for (const r of roots)
44
+ visit(r);
45
+ return out;
46
+ }
47
+ //# sourceMappingURL=shapes.js.map