@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,298 @@
1
+ import { search } from "./search.js";
2
+ import { cmp } from "./system.js";
3
+ import { page } from "./views.js";
4
+ /**
5
+ * What is known about a pattern, derived from its facts. There is no stored pattern status (ADR 0002): a single
6
+ * field could disagree with the facts it summarizes. A pattern with no facts has no stated basis (`none`); one
7
+ * whose facts share a status has that status; otherwise it is `mixed`. Only `approved` means every fact is.
8
+ */
9
+ export function patternStatus(p) {
10
+ const counts = {};
11
+ for (const f of Object.values(p.facts))
12
+ counts[f.status] = (counts[f.status] ?? 0) + 1;
13
+ const present = Object.keys(counts);
14
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
15
+ const overall = present.length === 0 ? "none" : present.length === 1 ? present[0] : "mixed";
16
+ return { overall, counts, total };
17
+ }
18
+ const kindOf = (id) => id.slice(0, Math.max(0, id.indexOf(":")));
19
+ function describe(ds, id) {
20
+ const name = ds.component(id)?.name ??
21
+ ds.pattern(id)?.name ??
22
+ ds.contract.tokens.find((t) => t.id === id)?.name ??
23
+ ds.contract.constraints.find((c) => c.id === id)?.name;
24
+ return { id, kind: kindOf(id), ...(name ? { name } : {}) };
25
+ }
26
+ /** Policies that make a pattern enforceable: they name it in their scope, or are based on a constraint it owns. */
27
+ function enforcement(ds, p) {
28
+ const owned = new Set(p.constraints ?? []);
29
+ return ds.contract.policies
30
+ .flatMap((policy) => {
31
+ const via = policy.basedOn.filter((id) => owned.has(id));
32
+ const scoped = (policy.scope.entities ?? []).includes(p.id);
33
+ if (via.length === 0 && !scoped)
34
+ return [];
35
+ return [
36
+ {
37
+ policy: policy.id,
38
+ name: policy.name,
39
+ severity: policy.severity,
40
+ enforcement: policy.enforcement,
41
+ approvedBy: policy.approval.approvedBy,
42
+ ...(via.length > 0 ? { constraints: via } : {})
43
+ }
44
+ ];
45
+ })
46
+ .sort((a, b) => cmp(String(a.policy), String(b.policy)));
47
+ }
48
+ const evidenceEntry = (ds, e) => ({
49
+ id: e.id,
50
+ sourceId: e.sourceId,
51
+ method: e.method,
52
+ location: ds.location([e.id]) ?? e.sourceRef,
53
+ ...(e.original ? { original: e.original.length > 200 ? `${e.original.slice(0, 199)}…` : e.original } : {})
54
+ });
55
+ function summary(ds, p) {
56
+ const status = patternStatus(p);
57
+ return {
58
+ id: p.id,
59
+ name: p.name,
60
+ category: p.category,
61
+ description: p.description,
62
+ ...(p.intent ? { intent: p.intent } : {}),
63
+ components: p.components,
64
+ status: { overall: status.overall, counts: status.counts, total: status.total },
65
+ approved: status.overall === "approved",
66
+ enforcedBy: enforcement(ds, p).map((e) => e.policy)
67
+ };
68
+ }
69
+ /** The pattern catalog, filterable by category, derived status and text. Ordered by relevance, else by id. */
70
+ export function patternsView(ds, filters = {}) {
71
+ const all = ds.contract.patterns;
72
+ let rows = all;
73
+ if (filters.query?.trim()) {
74
+ const hits = search(ds, filters.query, ["pattern"], 100).hits;
75
+ const byId = new Map(all.map((p) => [p.id, p]));
76
+ rows = hits.flatMap((h) => byId.get(h.id) ?? []);
77
+ }
78
+ else
79
+ rows = [...all].sort((a, b) => cmp(a.id, b.id));
80
+ rows = rows.filter((p) => (!filters.category || p.category === filters.category) &&
81
+ (!filters.status || patternStatus(p).overall === filters.status));
82
+ const view = page(rows, filters.offset, filters.limit);
83
+ const notes = [];
84
+ if (all.length === 0)
85
+ notes.push("No patterns are defined. Invariant does not infer patterns from code, and no source documents one yet.");
86
+ else if (rows.length === 0)
87
+ notes.push(`No pattern matches these filters; the contract has ${all.length} pattern(s).`);
88
+ const unapproved = view.items.filter((p) => patternStatus(p).overall !== "approved").length;
89
+ if (unapproved > 0)
90
+ notes.push(`${unapproved} of ${view.items.length} pattern(s) are not fully approved. Each shows its status: treat documented, inferred or mixed patterns as proposals, not organizational standards.`);
91
+ return {
92
+ total: view.total,
93
+ offset: view.offset,
94
+ limit: view.limit,
95
+ truncated: view.truncated,
96
+ patterns: view.items.map((p) => summary(ds, p)),
97
+ notes
98
+ };
99
+ }
100
+ function factEntries(ds, p) {
101
+ return Object.keys(p.facts)
102
+ .sort(cmp)
103
+ .map((path) => {
104
+ const fact = p.facts[path];
105
+ const view = ds.factView(p.facts, path);
106
+ return {
107
+ path,
108
+ ...view,
109
+ evidence: ds.evidenceEntries(fact.evidenceIds).map((e) => evidenceEntry(ds, e))
110
+ };
111
+ });
112
+ }
113
+ /** One pattern in full: what it is made of, what is required of it, how sure we are, and where that comes from. */
114
+ export function patternView(ds, p) {
115
+ const status = patternStatus(p);
116
+ const fact = (path) => ds.factView(p.facts, path) ?? null;
117
+ const constraints = (p.constraints ?? []).map((id) => {
118
+ const c = ds.contract.constraints.find((x) => x.id === id);
119
+ return c
120
+ ? {
121
+ id: c.id,
122
+ name: c.name,
123
+ kind: c.kind,
124
+ description: c.description,
125
+ ...(c.details ? { details: c.details } : {}),
126
+ status: (ds.factView(c.facts, "description")?.status ?? null)
127
+ }
128
+ : { id, missing: true };
129
+ });
130
+ const notes = [];
131
+ if (status.overall !== "approved")
132
+ notes.push(status.overall === "none"
133
+ ? "This pattern states no basis (no facts), so it is not approved."
134
+ : `This pattern is not fully approved (${status.overall}). Treat it as a proposal until it is approved.`);
135
+ if ((status.counts.inferred ?? 0) > 0)
136
+ notes.push(`${status.counts.inferred} fact(s) are inferred, with a confidence; confirm them before relying on them.`);
137
+ if (constraints.length === 0)
138
+ notes.push("No constraints are attached, so validation cannot enforce anything from this pattern.");
139
+ const open = ds.conflictsFor(p.id).filter((c) => c.resolution.state === "open");
140
+ if (open.length > 0)
141
+ notes.push(`${open.length} open conflict(s) concern this pattern.`);
142
+ const inheritanceActive = p.inheritsConstraints === true &&
143
+ p.appliesTo?.paths !== undefined &&
144
+ p.facts.appliesTo?.status === "approved" &&
145
+ p.facts.inheritsConstraints?.status === "approved";
146
+ if (p.inheritsConstraints === true && !inheritanceActive)
147
+ notes.push("This pattern opts in to inheriting the constraints of what it composes, but inheritance is off until its paths and the opt-in are approved.");
148
+ return {
149
+ id: p.id,
150
+ name: p.name,
151
+ category: p.category,
152
+ description: p.description,
153
+ ...(p.intent ? { intent: p.intent } : {}),
154
+ ...(p.appliesTo || p.inheritsConstraints !== undefined
155
+ ? {
156
+ scope: {
157
+ ...(p.appliesTo?.paths ? { paths: p.appliesTo.paths } : {}),
158
+ ...(p.appliesTo?.frameworks ? { frameworks: p.appliesTo.frameworks } : {}),
159
+ inheritsConstraints: p.inheritsConstraints === true,
160
+ inheritanceActive
161
+ }
162
+ }
163
+ : {}),
164
+ status: { overall: status.overall, counts: status.counts, total: status.total },
165
+ approved: status.overall === "approved",
166
+ components: p.components.map((id) => describe(ds, id)),
167
+ guidelines: p.guidelines.map((id) => ({
168
+ id,
169
+ name: ds.contract.guidelines.find((g) => g.id === id)?.name ?? null
170
+ })),
171
+ ...(p.composition
172
+ ? {
173
+ composition: {
174
+ parts: p.composition.parts.map((part) => ({
175
+ name: part.name,
176
+ role: part.role,
177
+ ref: describe(ds, part.ref),
178
+ ...(part.region ? { region: part.region } : {}),
179
+ ...(part.description ? { description: part.description } : {}),
180
+ fact: fact(`composition.parts.${part.name}`)
181
+ }))
182
+ }
183
+ }
184
+ : {}),
185
+ ...(p.layout ? { layout: p.layout } : {}),
186
+ ...(p.behavior ? { behavior: p.behavior } : {}),
187
+ ...(p.accessibility ? { accessibility: p.accessibility } : {}),
188
+ relationships: (p.relationships ?? []).map((r) => ({
189
+ id: r.id,
190
+ type: r.type,
191
+ target: describe(ds, r.target),
192
+ ...(r.description ? { description: r.description } : {}),
193
+ fact: fact(`relationships.${r.id}`)
194
+ })),
195
+ constraints,
196
+ enforcedBy: enforcement(ds, p),
197
+ facts: factEntries(ds, p),
198
+ conflicts: ds.conflictsFor(p.id).map((c) => ({ id: c.id, type: c.type, state: c.resolution.state })),
199
+ notes
200
+ };
201
+ }
202
+ const MAX_NODES = 200;
203
+ function outgoing(p) {
204
+ return [
205
+ ...(p.relationships ?? []).map((r) => ({
206
+ from: p.id,
207
+ to: r.target,
208
+ type: r.type,
209
+ via: "relationship",
210
+ id: r.id,
211
+ fact: `relationships.${r.id}`
212
+ })),
213
+ ...(p.composition?.parts ?? []).map((part) => ({
214
+ from: p.id,
215
+ to: part.ref,
216
+ type: "part",
217
+ via: "composition",
218
+ id: part.name,
219
+ role: part.role,
220
+ fact: `composition.parts.${part.name}`
221
+ }))
222
+ ];
223
+ }
224
+ /**
225
+ * The relationships around a pattern, walked breadth-first to `depth` (default 1, at most 5). Components are
226
+ * leaves. A node is expanded once, so cycles (`uses` may be cyclic) cannot loop. Composition parts appear as
227
+ * `part` edges with their role.
228
+ */
229
+ export function patternRelationships(ds, root, options = {}) {
230
+ const direction = options.direction ?? "both";
231
+ const depth = Math.max(1, Math.min(options.depth ?? 1, 5));
232
+ const wanted = options.types && options.types.length > 0 ? new Set(options.types) : undefined;
233
+ const everyEdge = ds.contract.patterns.flatMap(outgoing);
234
+ const edges = new Map();
235
+ const seen = new Set([root.id]);
236
+ let frontier = [root.id];
237
+ let truncated = false;
238
+ for (let level = 0; level < depth && frontier.length > 0; level++) {
239
+ const next = [];
240
+ for (const id of frontier) {
241
+ const found = [
242
+ ...(direction !== "in" ? everyEdge.filter((e) => e.from === id) : []),
243
+ ...(direction !== "out" ? everyEdge.filter((e) => e.to === id) : [])
244
+ ].filter((e) => !wanted || wanted.has(e.type));
245
+ for (const e of found) {
246
+ edges.set(`${e.from}|${e.type}|${e.id}|${e.to}`, e);
247
+ for (const end of [e.from, e.to]) {
248
+ if (seen.has(end))
249
+ continue;
250
+ if (seen.size >= MAX_NODES) {
251
+ truncated = true;
252
+ continue;
253
+ }
254
+ seen.add(end);
255
+ if (end.startsWith("pattern:"))
256
+ next.push(end);
257
+ }
258
+ }
259
+ }
260
+ frontier = next;
261
+ }
262
+ const nodes = [...seen].sort(cmp).map((id) => {
263
+ const p = ds.pattern(id);
264
+ return {
265
+ ...describe(ds, id),
266
+ ...(p ? { category: p.category, status: patternStatus(p).overall } : {}),
267
+ ...(id === root.id ? { root: true } : {})
268
+ };
269
+ });
270
+ const list = [...edges.values()].sort((a, b) => cmp(a.from, b.from) || cmp(a.type, b.type) || cmp(a.to, b.to) || cmp(a.id, b.id));
271
+ return {
272
+ pattern: { id: root.id, name: root.name },
273
+ direction,
274
+ depth,
275
+ edges: list.map((e) => {
276
+ const from = ds.pattern(e.from);
277
+ const view = from && e.fact ? ds.factView(from.facts, e.fact) : undefined;
278
+ return {
279
+ from: e.from,
280
+ to: e.to,
281
+ type: e.type,
282
+ via: e.via,
283
+ id: e.id,
284
+ ...(e.role ? { role: e.role } : {}),
285
+ status: (view?.status ?? null),
286
+ ...(view?.source ? { source: view.source } : {})
287
+ };
288
+ }),
289
+ nodes,
290
+ truncated,
291
+ notes: list.length === 0
292
+ ? ["This pattern has no relationships in the requested direction."]
293
+ : [
294
+ "Relationships are only what the contract states; none is inferred from name similarity. Each edge shows the status of the fact behind it."
295
+ ]
296
+ };
297
+ }
298
+ //# sourceMappingURL=patterns.js.map
@@ -0,0 +1,132 @@
1
+ import { cmp, normalize } from "./system.js";
2
+ const words = (text) => text
3
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
4
+ .toLowerCase()
5
+ .split(/[^a-z0-9]+/)
6
+ .filter(Boolean);
7
+ /** Plural and `-ing` endings only: `buttons` and `button`, `variants` and `variant`, `loading` and `load`. */
8
+ const stem = (w) => w.length > 4 && w.endsWith("ies")
9
+ ? `${w.slice(0, -3)}y`
10
+ : w.length > 4 && w.endsWith("ing")
11
+ ? w.slice(0, -3)
12
+ : w.length > 3 && w.endsWith("s") && !w.endsWith("ss")
13
+ ? w.slice(0, -1)
14
+ : w;
15
+ /** True when two words differ by at most one inserted, deleted or substituted letter. */
16
+ function oneEdit(a, b) {
17
+ if (Math.abs(a.length - b.length) > 1)
18
+ return false;
19
+ let i = 0;
20
+ let j = 0;
21
+ let edits = 0;
22
+ while (i < a.length && j < b.length) {
23
+ if (a[i] === b[j]) {
24
+ i++;
25
+ j++;
26
+ continue;
27
+ }
28
+ if (++edits > 1)
29
+ return false;
30
+ if (a.length > b.length)
31
+ i++;
32
+ else if (a.length < b.length)
33
+ j++;
34
+ else {
35
+ i++;
36
+ j++;
37
+ }
38
+ }
39
+ return edits + (a.length - i) + (b.length - j) <= 1;
40
+ }
41
+ /**
42
+ * Fixed, explainable ranking: name matches beat structure matches beat description matches. Matching is
43
+ * lexical: exact words, plural and `-ing` stems, word prefixes and one-letter typos. There are no synonyms,
44
+ * embeddings or models, so the same query always gives the same answer and every hit says what it matched.
45
+ */
46
+ function score(query, fields) {
47
+ const qn = normalize(query);
48
+ const qw = words(query);
49
+ let total = 0;
50
+ const matchedOn = [];
51
+ for (const f of fields) {
52
+ if (!f.text)
53
+ continue;
54
+ const tn = normalize(f.text);
55
+ const tw = words(f.text);
56
+ let s = 0;
57
+ if (tn === qn)
58
+ s = 10;
59
+ else if (tn.startsWith(qn) && qn.length >= 2)
60
+ s = 6;
61
+ else if (qw.length > 0 && qw.every((w) => tw.includes(w)))
62
+ s = 5;
63
+ else if (qn.length >= 3 && tn.includes(qn))
64
+ s = 3;
65
+ else if (qw.length > 0 && qw.every((w) => tw.some((x) => stem(x) === stem(w))))
66
+ s = 4;
67
+ else if (qw.length > 0 && qw.every((w) => w.length >= 3 && tw.some((x) => x.startsWith(w))))
68
+ s = 2;
69
+ else if (qw.some((w) => w.length >= 3 && tw.includes(w)))
70
+ s = 1;
71
+ else if (qw.length > 0 && qw.every((w) => w.length >= 5 && tw.some((x) => oneEdit(w, x)))) {
72
+ total += f.weight * 0.5;
73
+ matchedOn.push(`${f.label} (approximate)`);
74
+ continue;
75
+ }
76
+ if (s > 0) {
77
+ total += s * f.weight;
78
+ matchedOn.push(f.label);
79
+ }
80
+ }
81
+ return { score: total, matchedOn };
82
+ }
83
+ export function search(ds, query, kinds = ["component", "token", "guideline", "pattern"], limit = 20) {
84
+ const hits = [];
85
+ const add = (kind, id, name, r) => {
86
+ if (r.score > 0)
87
+ hits.push({ kind, id, name, score: r.score, matchedOn: r.matchedOn });
88
+ };
89
+ if (kinds.includes("component"))
90
+ for (const c of ds.contract.components)
91
+ add("component", c.id, c.name, score(query, [
92
+ { label: "name", text: c.name, weight: 10 },
93
+ { label: "prop", text: c.props.map((p) => p.name).join(" "), weight: 3 },
94
+ { label: "variant", text: c.variants.flatMap((v) => [v.name, ...v.values]).join(" "), weight: 3 },
95
+ { label: "slot", text: c.slots.map((s) => s.name).join(" "), weight: 2 },
96
+ { label: "event", text: c.events.map((e) => e.name).join(" "), weight: 2 },
97
+ { label: "description", text: c.description ?? "", weight: 1 }
98
+ ]));
99
+ if (kinds.includes("token"))
100
+ for (const t of ds.contract.tokens)
101
+ add("token", t.id, t.name, score(query, [
102
+ { label: "name", text: t.name, weight: 10 },
103
+ { label: "category", text: t.category, weight: 2 },
104
+ { label: "value", text: typeof t.value.value === "string" ? t.value.value : "", weight: 2 },
105
+ { label: "description", text: t.description ?? "", weight: 1 }
106
+ ]));
107
+ if (kinds.includes("guideline"))
108
+ for (const g of ds.contract.guidelines)
109
+ add("guideline", g.id, g.name, score(query, [
110
+ { label: "title", text: g.name, weight: 8 },
111
+ { label: "kind", text: g.kind, weight: 1 }
112
+ ]));
113
+ if (kinds.includes("pattern"))
114
+ for (const p of ds.contract.patterns)
115
+ add("pattern", p.id, p.name, score(query, [
116
+ { label: "name", text: p.name, weight: 10 },
117
+ { label: "intent", text: p.intent ?? "", weight: 3 },
118
+ { label: "category", text: p.category, weight: 2 },
119
+ {
120
+ label: "part",
121
+ text: (p.composition?.parts ?? []).map((x) => `${x.name} ${x.ref}`).join(" "),
122
+ weight: 3
123
+ },
124
+ { label: "region", text: (p.layout?.regions ?? []).map((r) => r.name).join(" "), weight: 2 },
125
+ { label: "state", text: (p.behavior?.states ?? []).map((x) => x.name).join(" "), weight: 2 },
126
+ { label: "description", text: p.description, weight: 1 }
127
+ ]));
128
+ hits.sort((a, b) => b.score - a.score || cmp(a.kind, b.kind) || cmp(a.id, b.id));
129
+ return { total: hits.length, hits: hits.slice(0, Math.max(1, Math.min(limit, 100))) };
130
+ }
131
+ export const searchJson = (r) => r;
132
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,148 @@
1
+ export const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
2
+ export const normalize = (text) => text.toLowerCase().replace(/[^a-z0-9]/g, "");
3
+ /**
4
+ * Read model over one compiled contract and its evidence. Pure and deterministic: it never touches the
5
+ * file system, a network or a model, and everything it returns is plain JSON.
6
+ */
7
+ export class DesignSystem {
8
+ contract;
9
+ contentHash;
10
+ evidence = new Map();
11
+ byId = new Map();
12
+ patternsById = new Map();
13
+ containers = new Map();
14
+ constructor(contract, evidence) {
15
+ this.contract = contract;
16
+ this.contentHash = contract.integrity.contentHash;
17
+ for (const e of evidence?.evidence ?? [])
18
+ this.evidence.set(e.id, e);
19
+ for (const c of contract.components)
20
+ this.byId.set(c.id, c);
21
+ for (const p of contract.patterns)
22
+ this.patternsById.set(p.id, p);
23
+ for (const c of contract.components)
24
+ c.composition.forEach((r, i) => {
25
+ if (r.type !== "contains")
26
+ return;
27
+ const list = this.containers.get(r.target) ?? [];
28
+ const fact = c.facts[`composition.${i}`];
29
+ list.push({ id: c.id, ...(fact ? { fact } : {}) });
30
+ this.containers.set(r.target, list);
31
+ });
32
+ }
33
+ component(id) {
34
+ return this.byId.get(id);
35
+ }
36
+ /** Resolves an id, an id without its prefix, or a name (ignoring case and separators). */
37
+ findComponent(query) {
38
+ const q = query.trim();
39
+ const exact = this.byId.get(q) ?? this.byId.get(`component:${q.toLowerCase()}`);
40
+ if (exact)
41
+ return { match: exact, candidates: [exact] };
42
+ const n = normalize(q);
43
+ const named = this.contract.components.filter((c) => normalize(c.name) === n || normalize(c.id.slice(10)) === n);
44
+ if (named.length === 1)
45
+ return { match: named[0], candidates: named };
46
+ if (named.length > 1)
47
+ return { candidates: named };
48
+ const near = this.contract.components
49
+ .filter((c) => n.length >= 2 && (normalize(c.name).includes(n) || n.includes(normalize(c.name))))
50
+ .slice(0, 8);
51
+ return { candidates: near };
52
+ }
53
+ pattern(id) {
54
+ return this.patternsById.get(id);
55
+ }
56
+ /**
57
+ * Resolves a pattern by id, by id without its prefix (`page.data-management`), by the part after the
58
+ * category (`data-management`), or by name, ignoring case and separators. Several matches yield candidates only.
59
+ */
60
+ findPattern(query) {
61
+ const q = query.trim();
62
+ const exact = this.patternsById.get(q) ?? this.patternsById.get(`pattern:${q.toLowerCase()}`);
63
+ if (exact)
64
+ return { match: exact, candidates: [exact] };
65
+ const n = normalize(q);
66
+ const named = this.contract.patterns.filter((p) => {
67
+ const body = p.id.slice("pattern:".length);
68
+ const afterCategory = body.includes(".") ? body.slice(body.indexOf(".") + 1) : body;
69
+ return normalize(p.name) === n || normalize(body) === n || normalize(afterCategory) === n;
70
+ });
71
+ if (named.length === 1)
72
+ return { match: named[0], candidates: named };
73
+ if (named.length > 1)
74
+ return { candidates: named };
75
+ const near = this.contract.patterns
76
+ .filter((p) => n.length >= 2 && (normalize(p.name).includes(n) || n.includes(normalize(p.name))))
77
+ .slice(0, 8);
78
+ return { candidates: near };
79
+ }
80
+ /** The evidence entries for these ids, in the given order. Ids missing from the evidence file are skipped. */
81
+ evidenceEntries(ids) {
82
+ return ids.flatMap((id) => {
83
+ const e = this.evidence.get(id);
84
+ return e ? [e] : [];
85
+ });
86
+ }
87
+ containedBy(id) {
88
+ return this.containers.get(id) ?? [];
89
+ }
90
+ location(ids) {
91
+ for (const id of ids) {
92
+ const e = this.evidence.get(id);
93
+ if (!e)
94
+ continue;
95
+ if (e.location.type === "file")
96
+ return `${e.location.path}${e.location.line ? `:${e.location.line}` : ""}`;
97
+ return e.sourceRef;
98
+ }
99
+ return undefined;
100
+ }
101
+ factView(facts, path) {
102
+ const f = facts[path];
103
+ if (!f)
104
+ return undefined;
105
+ const source = this.location(f.evidenceIds);
106
+ return {
107
+ status: f.status,
108
+ ...(f.confidence === undefined ? {} : { confidence: f.confidence }),
109
+ ...(f.derivation ? { rule: f.derivation.rule } : {}),
110
+ ...(source ? { source } : {})
111
+ };
112
+ }
113
+ /** True when the implementation's props are fully enumerated, so an absent prop really is absent. */
114
+ propsComplete(c, framework) {
115
+ const impls = c.implementations.filter((i) => !framework || i.framework === framework);
116
+ if (impls.length === 0)
117
+ return false;
118
+ return impls.every((i) => i.extensions?.[i.framework]?.propsComplete === true);
119
+ }
120
+ conflictsFor(subject) {
121
+ return this.contract.conflicts.filter((c) => c.subject === subject);
122
+ }
123
+ /** Follows aliases to a literal. A cycle or a missing target yields no value rather than a guess. */
124
+ resolveToken(token, theme) {
125
+ const chain = [token.id];
126
+ let current = token;
127
+ for (let i = 0; i < 20 && current; i++) {
128
+ const form = theme
129
+ ? (current.modes?.[theme] ?? current.value)
130
+ : current.value;
131
+ if (form.aliasOf === undefined)
132
+ return { value: form.value, chain };
133
+ const next = this.contract.tokens.find((t) => t.id === form.aliasOf);
134
+ if (!next)
135
+ return {
136
+ chain: [...chain, form.aliasOf],
137
+ note: `alias target ${form.aliasOf} is not in the contract`
138
+ };
139
+ if (chain.includes(next.id))
140
+ return { chain: [...chain, next.id], note: "alias cycle" };
141
+ chain.push(next.id);
142
+ current = next;
143
+ }
144
+ return { chain, note: "alias chain too long" };
145
+ }
146
+ }
147
+ export const sanitizeName = (text) => text.replace(/[^A-Za-z0-9 ._:/@-]/g, "").slice(0, 80);
148
+ //# sourceMappingURL=system.js.map