@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,370 @@
1
+ import { sortDiagnostics } from "../shared/index.js";
2
+ import { codeComponents } from "./code.js";
3
+ import { FactIndex } from "./facts.js";
4
+ const PREFIX = { group: "group", story: "story" };
5
+ /**
6
+ * Compares Storybook sources with code sources. A Storybook control or arg is never taken as proof that
7
+ * an API exists: it is checked against the implementation's own facts, and claims are only made when the
8
+ * implementation's props are fully known (`propsComplete`). Findings carry evidence from both sides.
9
+ */
10
+ export function reconcileStorybook(docs) {
11
+ const components = codeComponents(docs);
12
+ const byKey = new Map(components.map((c) => [`${c.file}#${c.localName}`, c]));
13
+ const links = [];
14
+ const found = [];
15
+ const demonstrated = new Map();
16
+ for (const doc of docs.filter((d) => d.source.type === "storybook")) {
17
+ const index = new FactIndex(doc);
18
+ const storiesByGroup = new Map();
19
+ for (const e of doc.entities.filter((x) => x.kind === "story")) {
20
+ const group = index.value(e.ref, "story.group");
21
+ if (group)
22
+ storiesByGroup.set(group, [...(storiesByGroup.get(group) ?? []), e.ref]);
23
+ }
24
+ for (const groupEntity of doc.entities.filter((e) => e.kind === "story-group")) {
25
+ const target = index.value(groupEntity.ref, "group.component");
26
+ if (!target?.file || !target.name)
27
+ continue;
28
+ const component = byKey.get(`${target.file}#${target.name}`);
29
+ const storyRefs = (storiesByGroup.get(groupEntity.ref) ?? []).sort();
30
+ if (!component) {
31
+ found.push({
32
+ code: "reconcile.storybook.component-unlinked",
33
+ severity: "info",
34
+ message: `${groupEntity.name} points at ${target.name} in ${target.file}, but no compiled source has a public component there`,
35
+ sourceId: doc.source.id,
36
+ entityId: groupEntity.ref,
37
+ storybookEvidence: index.evidenceIds(groupEntity.ref, "group.component"),
38
+ codeEvidence: [],
39
+ ...locationOf(index, index.evidenceIds(groupEntity.ref, "group.component"))
40
+ });
41
+ continue;
42
+ }
43
+ links.push({
44
+ group: groupEntity.ref,
45
+ storySourceId: doc.source.id,
46
+ stories: storyRefs,
47
+ component: component.ref,
48
+ componentSourceId: component.sourceId,
49
+ via: target.via ?? "component",
50
+ matchedBy: "file+localName"
51
+ });
52
+ const storyViews = storyRefs.map((ref) => view(index, ref, PREFIX.story, doc.entities.find((e) => e.ref === ref)?.name ?? ref, index.value(ref, "story.hasRender") === false));
53
+ // Meta-level args reach the component only through stories that use the default render.
54
+ const groupView = view(index, groupEntity.ref, PREFIX.group, groupEntity.name, storyViews.some((v) => v.defaultRender));
55
+ const seen = demonstrated.get(component) ?? {
56
+ names: new Set(),
57
+ values: new Map(),
58
+ uncertain: new Set(),
59
+ storyCount: 0
60
+ };
61
+ demonstrated.set(component, seen);
62
+ seen.storyCount += storyViews.length;
63
+ const unverifiable = { inherited: new Set(), render: new Set() };
64
+ // Meta-level annotations are checked once; each story's own annotations are checked at the story.
65
+ for (const v of [groupView, ...storyViews]) {
66
+ checkView({ doc, index, view: v, inherited: groupView, component, found, seen, unverifiable });
67
+ }
68
+ for (const [names, reason] of [
69
+ [unverifiable.inherited, `whose props are inherited from types that were not enumerated`],
70
+ [
71
+ unverifiable.render,
72
+ `because the stories that pass them use a custom render, so the args may not be ${component.name}'s props`
73
+ ]
74
+ ]) {
75
+ if (names.size === 0)
76
+ continue;
77
+ found.push({
78
+ code: "reconcile.storybook.unverifiable-names",
79
+ severity: "info",
80
+ message: `${groupEntity.name}: ${[...names].sort().slice(0, 10).join(", ")}${names.size > 10 ? ", …" : ""} could not be verified against ${component.name}, ${reason}`,
81
+ sourceId: doc.source.id,
82
+ entityId: groupEntity.ref,
83
+ component: component.ref,
84
+ storybookEvidence: [],
85
+ codeEvidence: []
86
+ });
87
+ }
88
+ if (component.deprecated) {
89
+ found.push({
90
+ code: "reconcile.storybook.deprecated-demonstrated",
91
+ severity: "warning",
92
+ message: `${groupEntity.name} demonstrates ${component.name}, which is marked deprecated`,
93
+ sourceId: doc.source.id,
94
+ entityId: groupEntity.ref,
95
+ component: component.ref,
96
+ storybookEvidence: index.evidenceIds(groupEntity.ref, "group.component"),
97
+ codeEvidence: component.index.evidenceIds(component.ref, "lifecycle"),
98
+ ...locationOf(index, index.evidenceIds(groupEntity.ref, "group.component"))
99
+ });
100
+ }
101
+ }
102
+ }
103
+ for (const [component, seen] of demonstrated)
104
+ coverage(component, seen, found);
105
+ const discrepancies = found.sort(compare);
106
+ return {
107
+ links: links.sort((a, b) => (a.group < b.group ? -1 : 1)),
108
+ discrepancies,
109
+ diagnostics: sortDiagnostics(discrepancies.map(toDiagnostic))
110
+ };
111
+ }
112
+ function view(index, ref, prefix, name, defaultRender) {
113
+ const args = new Map();
114
+ for (const arg of index.children(ref, `${prefix}.args`))
115
+ args.set(arg, {
116
+ value: index.value(ref, `${prefix}.args.${arg}`),
117
+ evidence: index.evidenceIds(ref, `${prefix}.args.${arg}`)
118
+ });
119
+ const unresolved = new Set();
120
+ for (const path of [...(index.value(ref, `${prefix}.argsUnresolved`) ?? [])]) {
121
+ const first = path.split(".")[0];
122
+ if (first && first !== "..." && first !== "<computed>")
123
+ unresolved.add(first);
124
+ }
125
+ const argTypes = new Map();
126
+ for (const prop of index.children(ref, `${prefix}.argTypes`)) {
127
+ if (prop === "")
128
+ continue;
129
+ const base = `${prefix}.argTypes.${prop}`;
130
+ const options = index.value(ref, `${base}.options`);
131
+ const optionsFact = index.get(ref, `${base}.options`);
132
+ const mapping = index.value(ref, `${base}.mappingKeys`);
133
+ const summary = index.value(ref, `${base}.defaultSummary`);
134
+ argTypes.set(prop, {
135
+ ...(options ? { options } : {}),
136
+ optionsUncertain: index.get(ref, `${base}.optionsUnresolved`) !== undefined || optionsFact?.completeness === "partial",
137
+ ...(mapping ? { mappingKeys: mapping } : {}),
138
+ ...(summary === undefined ? {} : { defaultSummary: summary }),
139
+ hidden: index.value(ref, `${base}.hidden`) === true,
140
+ evidence: index.evidenceIds(ref, `${base}.declared`),
141
+ defaultEvidence: index.evidenceIds(ref, `${base}.defaultSummary`)
142
+ });
143
+ }
144
+ return { ref, name, defaultRender, args, unresolvedArgs: unresolved, argTypes };
145
+ }
146
+ function checkView(input) {
147
+ const { doc, index, view: v, inherited, component, found, seen, unverifiable } = input;
148
+ const declared = (name) => component.props.has(name) ||
149
+ component.variantNames.has(name) ||
150
+ (name === "children" && (component.slotNames.has("default") || component.props.has("children")));
151
+ const add = (d) => {
152
+ found.push({
153
+ ...d,
154
+ sourceId: doc.source.id,
155
+ entityId: v.ref,
156
+ component: component.ref,
157
+ ...locationOf(index, d.storybookEvidence)
158
+ });
159
+ };
160
+ const argNames = new Set([...v.args.keys(), ...v.unresolvedArgs]);
161
+ for (const name of argNames) {
162
+ seen.names.add(name);
163
+ const prop = component.props.get(name);
164
+ const arg = v.args.get(name);
165
+ // With a mapping at either level, an arg value is a mapping key rather than the real prop value.
166
+ const mapped = v.argTypes.get(name)?.mappingKeys !== undefined ||
167
+ inherited.argTypes.get(name)?.mappingKeys !== undefined;
168
+ if (v.unresolvedArgs.has(name))
169
+ seen.uncertain.add(name);
170
+ if (!v.defaultRender) {
171
+ // Args of a custom-render story are not attributed to the component: no claim either way.
172
+ if (!declared(name))
173
+ unverifiable.render.add(name);
174
+ continue;
175
+ }
176
+ if (!declared(name)) {
177
+ if (component.propsComplete) {
178
+ add({
179
+ code: "reconcile.storybook.arg-undeclared",
180
+ severity: "warning",
181
+ message: `${v.name} passes arg "${name}", which ${component.name} does not declare as a prop`,
182
+ prop: name,
183
+ storybookEvidence: arg?.evidence ?? [],
184
+ codeEvidence: [],
185
+ claim: { storybook: true }
186
+ });
187
+ }
188
+ else
189
+ unverifiable.inherited.add(name);
190
+ continue;
191
+ }
192
+ if (prop?.deprecated) {
193
+ add({
194
+ code: "reconcile.storybook.deprecated-demonstrated",
195
+ severity: "warning",
196
+ message: `${v.name} uses arg "${name}", a deprecated prop of ${component.name}`,
197
+ prop: name,
198
+ storybookEvidence: arg?.evidence ?? [],
199
+ codeEvidence: prop.evidence.deprecated,
200
+ claim: { storybook: true, code: "deprecated" }
201
+ });
202
+ }
203
+ if (arg && prop?.allowedValues && isScalar(arg.value) && !mapped) {
204
+ const key = String(arg.value);
205
+ const values = valuesFor(seen, name);
206
+ values.add(key);
207
+ if (!prop.allowedValues.some((a) => a === arg.value)) {
208
+ add({
209
+ code: "reconcile.storybook.arg-value-not-allowed",
210
+ severity: "warning",
211
+ message: `${v.name} sets ${component.name}.${name} to ${JSON.stringify(arg.value)}, which is not one of its declared values (${prop.allowedValues.map((a) => JSON.stringify(a)).join(", ")})`,
212
+ prop: name,
213
+ storybookEvidence: arg.evidence,
214
+ codeEvidence: prop.evidence.allowedValues,
215
+ claim: { storybook: arg.value, code: prop.allowedValues }
216
+ });
217
+ }
218
+ }
219
+ }
220
+ for (const [name, at] of v.argTypes) {
221
+ seen.names.add(name);
222
+ const prop = component.props.get(name);
223
+ if (!declared(name)) {
224
+ if (at.hidden)
225
+ continue;
226
+ if (component.propsComplete) {
227
+ add({
228
+ code: "reconcile.storybook.control-undeclared",
229
+ severity: "warning",
230
+ message: `${v.name} documents a control for "${name}", which ${component.name} does not declare as a prop`,
231
+ prop: name,
232
+ storybookEvidence: at.evidence,
233
+ codeEvidence: [],
234
+ claim: { storybook: true }
235
+ });
236
+ }
237
+ else
238
+ unverifiable.inherited.add(name);
239
+ continue;
240
+ }
241
+ if (prop?.deprecated && !at.hidden) {
242
+ add({
243
+ code: "reconcile.storybook.deprecated-demonstrated",
244
+ severity: "warning",
245
+ message: `${v.name} documents a control for "${name}", a deprecated prop of ${component.name}`,
246
+ prop: name,
247
+ storybookEvidence: at.evidence,
248
+ codeEvidence: prop.evidence.deprecated,
249
+ claim: { storybook: true, code: "deprecated" }
250
+ });
251
+ }
252
+ if (at.options && !at.mappingKeys && !at.optionsUncertain) {
253
+ const values = valuesFor(seen, name);
254
+ for (const o of at.options)
255
+ values.add(String(o));
256
+ if (prop?.allowedValues) {
257
+ const extra = at.options.filter((o) => !prop.allowedValues?.some((a) => a === o));
258
+ if (extra.length > 0) {
259
+ add({
260
+ code: "reconcile.storybook.control-option-not-allowed",
261
+ severity: "warning",
262
+ message: `${v.name}: the control for ${component.name}.${name} offers ${extra.map((o) => JSON.stringify(o)).join(", ")}, which the component does not declare`,
263
+ prop: name,
264
+ storybookEvidence: at.evidence,
265
+ codeEvidence: prop.evidence.allowedValues,
266
+ claim: { storybook: extra, code: prop.allowedValues }
267
+ });
268
+ }
269
+ }
270
+ }
271
+ else if (at.options || at.optionsUncertain || at.mappingKeys)
272
+ seen.uncertain.add(name);
273
+ if (at.defaultSummary !== undefined &&
274
+ prop &&
275
+ prop.default !== undefined &&
276
+ normalize(at.defaultSummary) !== normalize(prop.default)) {
277
+ add({
278
+ code: "reconcile.storybook.default-mismatch",
279
+ severity: "warning",
280
+ message: `${v.name} documents the default of ${component.name}.${name} as ${at.defaultSummary}, but the code default is ${JSON.stringify(prop.default)}`,
281
+ prop: name,
282
+ storybookEvidence: at.defaultEvidence,
283
+ codeEvidence: prop.evidence.default,
284
+ claim: { storybook: normalize(at.defaultSummary), code: prop.default }
285
+ });
286
+ }
287
+ }
288
+ }
289
+ /** Props and declared values no linked story demonstrates. Only claimed where the story side is fully known. */
290
+ function coverage(component, seen, found) {
291
+ if (seen.storyCount === 0)
292
+ return;
293
+ const skipRoles = new Set(["children", "ref", "event-handler"]);
294
+ const missing = [...component.props.values()]
295
+ .filter((p) => !skipRoles.has(p.role ?? "") && !seen.names.has(p.name) && !component.variantNames.has(p.name))
296
+ .map((p) => p.name);
297
+ if (missing.length > 0) {
298
+ found.push({
299
+ code: "reconcile.storybook.prop-not-demonstrated",
300
+ severity: "info",
301
+ message: `${component.name} declares ${missing.slice(0, 10).join(", ")}${missing.length > 10 ? ", …" : ""}, which no story passes as an arg or documents as a control`,
302
+ sourceId: component.sourceId,
303
+ entityId: component.ref,
304
+ component: component.ref,
305
+ storybookEvidence: [],
306
+ codeEvidence: missing.flatMap((m) => component.props.get(m)?.evidence.declared ?? [])
307
+ });
308
+ }
309
+ for (const prop of component.props.values()) {
310
+ if (!prop.allowedValues ||
311
+ prop.allowedValues.length < 2 ||
312
+ seen.uncertain.has(prop.name) ||
313
+ !seen.names.has(prop.name))
314
+ continue;
315
+ const shown = seen.values.get(prop.name) ?? new Set();
316
+ const absent = prop.allowedValues.filter((v) => !shown.has(String(v)));
317
+ if (absent.length > 0) {
318
+ found.push({
319
+ code: "reconcile.storybook.value-not-demonstrated",
320
+ severity: "info",
321
+ message: `${component.name}.${prop.name} declares ${absent
322
+ .slice(0, 8)
323
+ .map((v) => JSON.stringify(v))
324
+ .join(", ")}${absent.length > 8 ? `, … (${absent.length} in all)` : ""}, which no story or control demonstrates`,
325
+ sourceId: component.sourceId,
326
+ entityId: component.ref,
327
+ component: component.ref,
328
+ prop: prop.name,
329
+ storybookEvidence: [],
330
+ codeEvidence: prop.evidence.allowedValues
331
+ });
332
+ }
333
+ }
334
+ }
335
+ function valuesFor(seen, name) {
336
+ const existing = seen.values.get(name);
337
+ if (existing)
338
+ return existing;
339
+ const created = new Set();
340
+ seen.values.set(name, created);
341
+ return created;
342
+ }
343
+ function isScalar(value) {
344
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
345
+ }
346
+ /** Compares a documented default (`'md'`, `"md"`, `md`, `false`) with a code default. */
347
+ function normalize(value) {
348
+ const text = String(value).trim();
349
+ return text.replace(/^(["'`])(.*)\1$/s, "$2");
350
+ }
351
+ function locationOf(index, evidenceIds) {
352
+ const location = index.location(evidenceIds[0]);
353
+ return location ? { location } : {};
354
+ }
355
+ function compare(a, b) {
356
+ const key = (d) => `${d.code}\u0000${d.sourceId}\u0000${d.entityId}\u0000${d.prop ?? ""}\u0000${d.message}`;
357
+ const order = { error: 0, warning: 1, info: 2 };
358
+ return order[a.severity] - order[b.severity] || (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
359
+ }
360
+ function toDiagnostic(d) {
361
+ return {
362
+ code: d.code,
363
+ severity: d.severity,
364
+ message: d.message,
365
+ sourceId: d.sourceId,
366
+ entityId: d.entityId,
367
+ ...(d.location ? { location: d.location } : {})
368
+ };
369
+ }
370
+ //# sourceMappingURL=storybook.js.map
@@ -0,0 +1,179 @@
1
+ import { cmp, normalizeName, sameValue, setNested, slug, unique } from "./util.js";
2
+ const STATUS = (f) => f.origin === "extracted" ? "discovered" : "documented";
3
+ const rank = (s) => (s === "discovered" ? 0 : 1);
4
+ const themeId = (mode) => `theme:${slug(mode).replace(/\./g, "-")}`;
5
+ /** Token entities from design-token adapters: the ones that state a value. Inventory entries do not. */
6
+ function sourcesOf(docs) {
7
+ const out = [];
8
+ for (const doc of docs) {
9
+ const bySubject = new Map();
10
+ for (const f of doc.facts)
11
+ bySubject.set(f.subject, [...(bySubject.get(f.subject) ?? []), f]);
12
+ for (const e of doc.entities.filter((x) => x.kind === "token")) {
13
+ const facts = bySubject.get(e.ref) ?? [];
14
+ if (!facts.some((f) => f.path === "value.value" || f.path === "value.aliasOf"))
15
+ continue;
16
+ const name = facts.find((f) => f.path === "name" && typeof f.value === "string")?.value;
17
+ out.push({ doc, ref: e.ref, name: typeof name === "string" ? name : e.name, facts });
18
+ }
19
+ }
20
+ return out.sort((a, b) => cmp(a.doc.source.id, b.doc.source.id) || cmp(a.ref, b.ref));
21
+ }
22
+ export function buildTokens(docs, ctx) {
23
+ const sources = sourcesOf(docs);
24
+ const groups = new Map();
25
+ for (const s of sources)
26
+ groups.set(normalizeName(s.name), [...(groups.get(normalizeName(s.name)) ?? []), s]);
27
+ const ids = new Map();
28
+ const used = new Set();
29
+ const names = new Map();
30
+ for (const [key, members] of [...groups.entries()].sort((a, b) => cmp(a[0], b[0]))) {
31
+ const name = members.find((m) => m.name.includes("."))?.name ?? members[0].name;
32
+ let id = `token:${slug(name)}`;
33
+ for (let n = 2; used.has(id); n++)
34
+ id = `token:${slug(name)}-${n}`;
35
+ used.add(id);
36
+ ids.set(key, id);
37
+ names.set(key, name);
38
+ }
39
+ const themes = new Map();
40
+ const identities = [];
41
+ const tokens = [];
42
+ for (const [key, members] of [...groups.entries()].sort((a, b) => cmp(a[0], b[0]))) {
43
+ const id = ids.get(key);
44
+ const spellings = unique(members.map((m) => m.name)).sort();
45
+ if (spellings.length > 1) {
46
+ identities.push({
47
+ tokenId: id,
48
+ link: {
49
+ rule: "name-match.token",
50
+ confidence: 0.8,
51
+ reasoning: `${spellings.map((s) => `"${s}"`).join(", ")} differ only in case or separators and are treated as one token`,
52
+ inputs: members.map((m) => m.ref).sort(),
53
+ evidenceIds: unique(members.flatMap((m) => m.facts.find((f) => f.path === "name")?.evidenceIds ?? [])).sort()
54
+ }
55
+ });
56
+ }
57
+ const facts = {};
58
+ const extensions = {};
59
+ const slots = new Map();
60
+ let description;
61
+ let deprecated;
62
+ for (const src of members) {
63
+ for (const f of src.facts) {
64
+ const seg = f.path.split(".");
65
+ if (f.path === "value.value" ||
66
+ f.path === "value.aliasOf" ||
67
+ (seg[0] === "modes" && seg.length === 3 && (seg[2] === "value" || seg[2] === "aliasOf"))) {
68
+ const slot = seg[0] === "modes" ? `modes.${themeId(seg[1])}` : "value";
69
+ if (seg[0] === "modes") {
70
+ const tid = themeId(seg[1]);
71
+ if (!themes.has(tid))
72
+ themes.set(tid, { id: tid, name: seg[1] });
73
+ }
74
+ let form;
75
+ if (seg[seg.length - 1] === "aliasOf") {
76
+ const target = ids.get(normalizeName(String(f.value)));
77
+ if (!target) {
78
+ ctx.diagnostics.warning("reconcile.token-alias-unresolved", `${id} aliases "${String(f.value)}", which is not a known token; its source text is kept as a literal`, { entityId: id, sourceId: src.doc.source.id });
79
+ const raw = src.facts.find((x) => /^extensions\.[^.]+\.(root\.)?rawValue$/.test(x.path))?.value;
80
+ form = { value: raw ?? String(f.value) };
81
+ }
82
+ else
83
+ form = { aliasOf: target };
84
+ }
85
+ else
86
+ form = { value: f.value };
87
+ slots.set(slot, [
88
+ ...(slots.get(slot) ?? []),
89
+ { src, form, status: STATUS(f), evidenceIds: f.evidenceIds }
90
+ ]);
91
+ }
92
+ else if (f.path === "description" && typeof f.value === "string")
93
+ description ??= { value: f.value, claim: f };
94
+ else if (f.path === "deprecated") {
95
+ deprecated ??= {
96
+ value: typeof f.value === "object" && f.value !== null && !Array.isArray(f.value)
97
+ ? f.value
98
+ : {},
99
+ claim: f
100
+ };
101
+ }
102
+ else if (seg[0] === "extensions" &&
103
+ seg.length >= 3 &&
104
+ /^[a-z0-9][a-z0-9._-]*$/.test(seg[1]) &&
105
+ seg[1] !== "invariant") {
106
+ extensions[seg[1]] ??= {};
107
+ if (setNested(extensions[seg[1]], seg.slice(2), f.value))
108
+ facts[f.path] = { status: STATUS(f), evidenceIds: f.evidenceIds };
109
+ }
110
+ }
111
+ }
112
+ const formOf = (slot) => {
113
+ const claims = slots.get(slot);
114
+ if (!claims)
115
+ return undefined;
116
+ const win = [...claims].sort((a, b) => rank(a.status) - rank(b.status) ||
117
+ cmp(a.src.doc.source.id, b.src.doc.source.id) ||
118
+ cmp(a.src.ref, b.src.ref))[0];
119
+ const agreeing = claims.filter((c) => sameValue(c.form, win.form));
120
+ const field = "aliasOf" in win.form ? "aliasOf" : "value";
121
+ facts[slot === "value" ? `value.${field}` : `${slot}.${field}`] = {
122
+ status: agreeing.some((c) => c.status === "discovered") ? "discovered" : "documented",
123
+ evidenceIds: unique(agreeing.flatMap((c) => c.evidenceIds)).sort()
124
+ };
125
+ if (agreeing.length < claims.length) {
126
+ ctx.conflicts.push({
127
+ type: "token-value-mismatch",
128
+ severity: "warning",
129
+ subject: id,
130
+ path: slot,
131
+ claims: claims
132
+ .map((c) => ({
133
+ sourceId: c.src.doc.source.id,
134
+ value: c.form,
135
+ status: c.status,
136
+ evidenceIds: c.evidenceIds
137
+ }))
138
+ .sort((a, b) => cmp(a.sourceId, b.sourceId) || cmp(JSON.stringify(a.value), JSON.stringify(b.value))),
139
+ suggestedResolution: "Decide which value is intended and remove the other definition."
140
+ });
141
+ }
142
+ return win.form;
143
+ };
144
+ const main = formOf("value");
145
+ const modes = {};
146
+ for (const slot of [...slots.keys()].filter((s) => s.startsWith("modes.")).sort()) {
147
+ const form = formOf(slot);
148
+ if (form)
149
+ modes[slot.slice("modes.".length)] = form;
150
+ }
151
+ const name = names.get(key);
152
+ facts.name = {
153
+ status: "discovered",
154
+ evidenceIds: unique(members.flatMap((m) => m.facts.find((f) => f.path === "name")?.evidenceIds ?? [])).sort()
155
+ };
156
+ if (description) {
157
+ facts.description = { status: STATUS(description.claim), evidenceIds: description.claim.evidenceIds };
158
+ }
159
+ if (deprecated)
160
+ facts.deprecated = { status: STATUS(deprecated.claim), evidenceIds: deprecated.claim.evidenceIds };
161
+ tokens.push({
162
+ id: id,
163
+ name,
164
+ category: "other",
165
+ ...(description ? { description: description.value } : {}),
166
+ value: main,
167
+ ...(Object.keys(modes).length > 0 ? { modes: modes } : {}),
168
+ ...(deprecated ? { deprecated: deprecated.value } : {}),
169
+ facts: Object.fromEntries(Object.entries(facts).sort((a, b) => cmp(a[0], b[0]))),
170
+ ...(Object.keys(extensions).length > 0 ? { extensions } : {})
171
+ });
172
+ }
173
+ return {
174
+ tokens: tokens.sort((a, b) => cmp(a.id, b.id)),
175
+ themes: [...themes.values()].sort((a, b) => cmp(a.id, b.id)),
176
+ identities
177
+ };
178
+ }
179
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1,55 @@
1
+ import { canonicalStringify } from "../shared/index.js";
2
+ export function cmp(a, b) {
3
+ return a < b ? -1 : a > b ? 1 : 0;
4
+ }
5
+ /** Lower-case id segment matching the contract's name grammar `[a-z0-9][a-z0-9._-]*`. */
6
+ export function slug(text) {
7
+ const s = text
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9._-]+/g, "-")
10
+ .replace(/^[^a-z0-9]+/, "")
11
+ .replace(/-{2,}/g, "-")
12
+ .replace(/[-.]+$/, "");
13
+ return s === "" ? "x" : s;
14
+ }
15
+ /** Equality key for names: case and separators are ignored. */
16
+ export function normalizeName(text) {
17
+ return text.toLowerCase().replace(/[^a-z0-9]/g, "");
18
+ }
19
+ export function sameValue(a, b) {
20
+ return (canonicalStringify(a ?? null) === canonicalStringify(b ?? null) && (a === undefined) === (b === undefined));
21
+ }
22
+ /** Order-insensitive equality for arrays of scalars (value domains); other values compare exactly. */
23
+ export function sameDomain(a, b) {
24
+ if (Array.isArray(a) && Array.isArray(b)) {
25
+ const key = (v) => canonicalStringify(v);
26
+ return canonicalStringify([...a].map(key).sort()) === canonicalStringify([...b].map(key).sort());
27
+ }
28
+ return sameValue(a, b);
29
+ }
30
+ /** Sets `value` at a nested path. Returns false when the path collides with an existing leaf or branch. */
31
+ export function setNested(target, segments, value) {
32
+ let node = target;
33
+ for (let i = 0; i < segments.length - 1; i++) {
34
+ const key = segments[i];
35
+ const next = node[key];
36
+ if (next === undefined) {
37
+ const created = {};
38
+ node[key] = created;
39
+ node = created;
40
+ }
41
+ else if (next !== null && typeof next === "object" && !Array.isArray(next))
42
+ node = next;
43
+ else
44
+ return false;
45
+ }
46
+ const last = segments[segments.length - 1];
47
+ if (last in node)
48
+ return false;
49
+ node[last] = value;
50
+ return true;
51
+ }
52
+ export function unique(items) {
53
+ return [...new Set(items)];
54
+ }
55
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1,51 @@
1
+ import { canonicalStringify } from "../shared/index.js";
2
+ const same = (a, b) => canonicalStringify(a ?? null) === canonicalStringify(b ?? null);
3
+ function entityViolations(kind, before, after, ignore) {
4
+ const out = [];
5
+ const afterById = new Map(after.map((e) => [e.id, e]));
6
+ for (const b of before) {
7
+ const a = afterById.get(b.id);
8
+ if (!a) {
9
+ out.push(`${kind} ${b.id} was removed`);
10
+ continue;
11
+ }
12
+ for (const key of new Set([...Object.keys(b), ...Object.keys(a)])) {
13
+ if (key === "facts" || ignore(key, b.id))
14
+ continue;
15
+ if (!same(b[key], a[key]))
16
+ out.push(`${kind} ${b.id}: ${key} changed`);
17
+ }
18
+ for (const [path, fact] of Object.entries(b.facts))
19
+ if (!same(fact, a.facts[path]))
20
+ out.push(`${kind} ${b.id}: fact ${path} changed`);
21
+ }
22
+ return out;
23
+ }
24
+ /**
25
+ * Enrichment may add entities, facts, and relationships. It may not change or remove anything reconciliation
26
+ * produced, except the token `category` placeholder (and only while that has no fact). Returns violations.
27
+ */
28
+ export function assertAdditive(before, after) {
29
+ const out = [];
30
+ for (const key of ["contractVersion", "metadata", "sources", "themes", "conflicts"])
31
+ if (!same(before[key], after[key]))
32
+ out.push(`${key} changed`);
33
+ if (after.policies.length !== 0 || before.policies.length !== 0) {
34
+ if (!same(before.policies, after.policies))
35
+ out.push("policies changed: the engine never produces policies");
36
+ }
37
+ out.push(...entityViolations("guideline", before.guidelines, after.guidelines, () => false));
38
+ out.push(...entityViolations("pattern", before.patterns, after.patterns, () => false));
39
+ out.push(...entityViolations("constraint", before.constraints, after.constraints, () => false));
40
+ const placeholderOnly = (id) => before.tokens.find((t) => t.id === id)?.facts.category === undefined;
41
+ out.push(...entityViolations("token", before.tokens, after.tokens, (key, id) => key === "category" && placeholderOnly(id)));
42
+ const beforeComponents = new Map(before.components.map((c) => [c.id, c]));
43
+ out.push(...entityViolations("component", before.components, after.components, (key) => key === "composition"));
44
+ for (const a of after.components) {
45
+ const b = beforeComponents.get(a.id);
46
+ if (b && !same(b.composition, a.composition.slice(0, b.composition.length)))
47
+ out.push(`component ${a.id}: existing composition entries changed`);
48
+ }
49
+ return out;
50
+ }
51
+ //# sourceMappingURL=additive.js.map