@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,608 @@
1
+ import { constraintDetailsSchemas, effectiveParts, isPatternConstraintKind } from "../contract/index.js";
2
+ import { globToRegExp } from "../shared/index.js";
3
+ import { analyzeInteractions } from "./interaction-workflows.js";
4
+ const CODE = {
5
+ "required-child": "pattern-required-child-missing",
6
+ "forbidden-child": "pattern-child-forbidden",
7
+ "requires-parent": "pattern-parent-required",
8
+ "required-region": "pattern-region-missing",
9
+ ordering: "pattern-order-violated",
10
+ cardinality: "pattern-cardinality-violated",
11
+ "required-state": "pattern-state-missing"
12
+ };
13
+ export const PATTERN_RULE_CODES = Object.values(CODE);
14
+ export const patternRuleCode = (kind) => CODE[kind];
15
+ const capped = (severity, enforcement) => enforcement === "report-only"
16
+ ? "info"
17
+ : enforcement === "warn" && severity === "error"
18
+ ? "warning"
19
+ : severity;
20
+ /**
21
+ * The structural rules approved policies carry. A policy carries one when it is based on a constraint of a
22
+ * pattern kind; its `details` are parsed here and a constraint that does not parse is reported, not enforced.
23
+ */
24
+ export function patternRules(ds) {
25
+ const rules = [];
26
+ const problems = [];
27
+ for (const policy of ds.contract.policies)
28
+ for (const id of policy.basedOn) {
29
+ const constraint = ds.contract.constraints.find((c) => c.id === id);
30
+ if (!constraint || !isPatternConstraintKind(constraint.kind))
31
+ continue;
32
+ const owner = ds.contract.patterns.find((p) => p.constraints?.includes(constraint.id));
33
+ const parsed = constraintDetailsSchemas[constraint.kind].safeParse(constraint.details);
34
+ if (!owner || !parsed.success) {
35
+ problems.push(`policy ${policy.id}: ${constraint.id} ${owner ? "has details that do not fit its kind" : "belongs to no pattern"}`);
36
+ continue;
37
+ }
38
+ const source = ds.location(constraint.facts.description?.evidenceIds ?? []);
39
+ rules.push({
40
+ policy: policy.id,
41
+ pattern: owner.id,
42
+ constraint: constraint.id,
43
+ kind: constraint.kind,
44
+ details: parsed.data,
45
+ severity: capped(policy.severity, policy.enforcement),
46
+ paths: policy.scope.paths ?? [],
47
+ frameworks: policy.scope.frameworks ?? [],
48
+ ...(source ? { source } : {})
49
+ });
50
+ }
51
+ return { rules: [...rules, ...inheritedRules(ds, rules)], problems };
52
+ }
53
+ /** Every pattern a pattern composes, in the base blueprint and every variation, followed through patterns it uses. */
54
+ function composedPatterns(ds, root) {
55
+ const byId = new Map(ds.contract.patterns.map((p) => [p.id, p]));
56
+ const seen = new Set();
57
+ const queue = [root];
58
+ for (let next = queue.shift(); next; next = queue.shift()) {
59
+ const ids = [undefined, ...(next.variations ?? []).map((v) => v.name)];
60
+ for (const v of ids)
61
+ for (const part of effectiveParts(next, v)) {
62
+ const target = byId.get(part.ref);
63
+ if (!target || seen.has(target.id) || target.id === root.id)
64
+ continue;
65
+ seen.add(target.id);
66
+ queue.push(target);
67
+ }
68
+ }
69
+ return seen;
70
+ }
71
+ /**
72
+ * Constraint inheritance is opt in and needs approval. A page pattern that sets `inheritsConstraints` and names
73
+ * `appliesTo.paths` puts the rules of everything it composes onto those paths. Both settings must be approved facts,
74
+ * so a definition nobody approved enforces nothing, and no rule is created that an approved policy did not already carry.
75
+ */
76
+ function inheritedRules(ds, base) {
77
+ const out = [];
78
+ for (const page of ds.contract.patterns) {
79
+ const paths = page.appliesTo?.paths;
80
+ if (page.inheritsConstraints !== true || !paths)
81
+ continue;
82
+ if (page.facts.appliesTo?.status !== "approved" || page.facts.inheritsConstraints?.status !== "approved")
83
+ continue;
84
+ const composed = composedPatterns(ds, page);
85
+ for (const rule of base) {
86
+ if (!composed.has(rule.pattern))
87
+ continue;
88
+ out.push({
89
+ ...rule,
90
+ paths,
91
+ frameworks: page.appliesTo?.frameworks ?? rule.frameworks,
92
+ inheritedFrom: page.id
93
+ });
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ const PRECEDENCE = ["fail", "warning", "not-verifiable", "pass", "skipped"];
99
+ const depthOf = (t, i) => t.uses[i]?.exprDepth ?? 0;
100
+ const loopOf = (t, i) => t.uses[i]?.loopDepth ?? 0;
101
+ const nameOf = (ctx, ref) => ref.startsWith("region:") ? ref : (ctx.ds.component(ref)?.name ?? ref.replace(/^[a-z]+:/, ""));
102
+ function matcher(ctx, ref) {
103
+ if (ref.startsWith("pattern:"))
104
+ return {
105
+ ok: false,
106
+ reason: `${ref} is a pattern, and occurrences of a pattern are not detected statically yet`
107
+ };
108
+ if (ref.startsWith("region:")) {
109
+ const name = ref.slice("region:".length);
110
+ const parts = (ctx.owner?.composition?.parts ?? []).filter((p) => p.region === name && p.role !== "prohibited");
111
+ const ids = new Set(parts.filter((p) => p.ref.startsWith("component:")).map((p) => p.ref));
112
+ if (ids.size === 0)
113
+ return {
114
+ ok: false,
115
+ reason: `region ${name} has no component parts to recognize it by; give a composition part that region`
116
+ };
117
+ return {
118
+ ok: true,
119
+ describe: `region ${name}`,
120
+ test: (i) => {
121
+ const c = ctx.tree.components[i];
122
+ return c !== undefined && ids.has(c.id);
123
+ }
124
+ };
125
+ }
126
+ return {
127
+ ok: true,
128
+ describe: nameOf(ctx, ref),
129
+ test: (i) => ctx.tree.components[i]?.id === ref
130
+ };
131
+ }
132
+ function descendants(ctx, root) {
133
+ const out = [];
134
+ const walk = (i) => {
135
+ for (const c of ctx.children[i] ?? []) {
136
+ out.push(c);
137
+ walk(c);
138
+ }
139
+ };
140
+ walk(root);
141
+ return out.sort((a, b) => a - b);
142
+ }
143
+ /** True when nothing in the scope can hide elements: no other-file component and no opaque children. */
144
+ function closed(ctx, scope) {
145
+ for (const i of scope) {
146
+ const use = ctx.tree.uses[i];
147
+ const kind = ctx.tree.kinds[i];
148
+ if (kind === "foreign" || kind === "unknown" || kind === "ambiguous")
149
+ return {
150
+ ok: false,
151
+ why: `<${use.tag}> at line ${use.line} comes from outside the design system and may render more`
152
+ };
153
+ if (use.opaque)
154
+ return {
155
+ ok: false,
156
+ why: `<${use.tag}> at line ${use.line} gets content from outside what this file shows (children, a spread or an expression)`
157
+ };
158
+ }
159
+ return { ok: true };
160
+ }
161
+ function record(ctx, status, reason, at, suggestion, extra) {
162
+ const { rule } = ctx;
163
+ const code = CODE[rule.kind];
164
+ ctx.out.evaluations.push({
165
+ rule: code,
166
+ status,
167
+ reason,
168
+ pattern: rule.pattern,
169
+ constraint: rule.constraint,
170
+ policy: rule.policy,
171
+ ...(ctx.filename ? { file: ctx.filename } : {}),
172
+ ...(at?.line === undefined ? {} : { line: at.line }),
173
+ ...(at?.column === undefined ? {} : { column: at.column }),
174
+ ...(extra?.level ? { level: extra.level } : {}),
175
+ ...(extra?.evidence && extra.evidence.length > 0 ? { evidence: extra.evidence } : {}),
176
+ ...(rule.inheritedFrom ? { inheritedFrom: rule.inheritedFrom } : {})
177
+ });
178
+ if (status !== "fail" && status !== "warning")
179
+ return;
180
+ ctx.out.findings.push({
181
+ code,
182
+ severity: status === "warning" && rule.severity === "error" ? "warning" : rule.severity,
183
+ message: `${reason} (pattern ${rule.pattern}${rule.inheritedFrom ? `, inherited through ${rule.inheritedFrom}` : ""})`,
184
+ subject: rule.pattern,
185
+ ...(at?.line === undefined ? {} : { line: at.line }),
186
+ ...(at?.column === undefined ? {} : { column: at.column }),
187
+ basis: "approved",
188
+ policy: rule.policy,
189
+ ...(rule.source ? { source: rule.source } : {}),
190
+ ...(suggestion ? { suggestion } : {})
191
+ });
192
+ }
193
+ const at = (ctx, i) => ({
194
+ line: ctx.tree.uses[i].line,
195
+ column: ctx.tree.uses[i].column
196
+ });
197
+ /** A scope is the whole file, or the subtree of one element. Returns the indices inside it. */
198
+ function scopeOf(ctx, within) {
199
+ return within === undefined ? ctx.tree.uses.map((_, i) => i) : descendants(ctx, within);
200
+ }
201
+ /**
202
+ * Matches in a scope. `certain` ones render exactly once relative to the scope's root; `repeated` ones sit in a
203
+ * callback (a list, a render prop) and may render any number of times, so they can never prove an upper bound.
204
+ */
205
+ function counts(ctx, scope, test, baseDepth, baseLoop = 0) {
206
+ const matched = scope.filter(test);
207
+ return {
208
+ all: matched,
209
+ certain: matched.filter((i) => depthOf(ctx.tree, i) <= baseDepth),
210
+ repeated: matched.filter((i) => loopOf(ctx.tree, i) > baseLoop)
211
+ };
212
+ }
213
+ function evaluateRequiredChild(ctx, d) {
214
+ const parent = matcher(ctx, d.parent);
215
+ const child = matcher(ctx, d.child);
216
+ if (!parent.ok || !child.ok) {
217
+ record(ctx, "not-verifiable", (!parent.ok ? parent : child).reason, undefined);
218
+ return;
219
+ }
220
+ const min = d.min ?? 1;
221
+ const each = (p) => {
222
+ if (!parent.test(p))
223
+ return;
224
+ const scope = descendants(ctx, p);
225
+ const { all, certain } = counts(ctx, scope, child.test, depthOf(ctx.tree, p));
226
+ const label = `${parent.describe} at line ${at(ctx, p).line}`;
227
+ if (certain.length >= min)
228
+ return record(ctx, "pass", `${label} contains ${child.describe}`, at(ctx, p));
229
+ if (all.length >= min)
230
+ return record(ctx, "warning", `${label} contains ${child.describe} only conditionally, so it may be missing when rendered`, at(ctx, p), `Render ${child.describe} unconditionally inside ${parent.describe}.`);
231
+ const c = closed(ctx, [p, ...scope]);
232
+ if (!c.ok)
233
+ return record(ctx, "not-verifiable", `${label}: cannot tell whether it contains ${child.describe}; ${c.why}`, at(ctx, p));
234
+ record(ctx, "fail", `${label} has no ${child.describe}${min > 1 ? ` (needs ${min})` : ""}`, at(ctx, p), `Add ${child.describe} inside ${parent.describe}.`);
235
+ };
236
+ for (const p of ctx.tree.uses.keys())
237
+ each(p);
238
+ }
239
+ function evaluateForbiddenChild(ctx, d) {
240
+ const parent = matcher(ctx, d.parent);
241
+ const child = matcher(ctx, d.child);
242
+ if (!parent.ok || !child.ok) {
243
+ record(ctx, "not-verifiable", (!parent.ok ? parent : child).reason, undefined);
244
+ return;
245
+ }
246
+ const each = (p) => {
247
+ if (!parent.test(p))
248
+ return;
249
+ const scope = descendants(ctx, p);
250
+ const label = `${parent.describe} at line ${at(ctx, p).line}`;
251
+ const found = scope.find(child.test);
252
+ if (found !== undefined)
253
+ return record(ctx, "fail", `${label} contains ${child.describe} at line ${at(ctx, found).line}, which is not allowed there`, at(ctx, found), `Remove ${child.describe} from inside ${parent.describe}.`);
254
+ const c = closed(ctx, [p, ...scope]);
255
+ if (!c.ok)
256
+ return record(ctx, "not-verifiable", `${label}: cannot tell whether it avoids ${child.describe}; ${c.why}`, at(ctx, p));
257
+ record(ctx, "pass", `${label} does not contain ${child.describe}`, at(ctx, p));
258
+ };
259
+ for (const p of ctx.tree.uses.keys())
260
+ each(p);
261
+ }
262
+ function evaluateRequiresParent(ctx, d) {
263
+ const child = matcher(ctx, d.child);
264
+ const parents = d.parents.map((p) => matcher(ctx, p));
265
+ const bad = [child, ...parents].find((m) => !m.ok);
266
+ if (bad && !bad.ok) {
267
+ record(ctx, "not-verifiable", bad.reason, undefined);
268
+ return;
269
+ }
270
+ const okParents = parents;
271
+ const okChild = child;
272
+ const names = okParents.map((m) => m.describe).join(" or ");
273
+ const each = (i) => {
274
+ const use = ctx.tree.uses[i];
275
+ if (!okChild.test(i))
276
+ return;
277
+ const chain = [];
278
+ for (let p = use.parent; p !== undefined; p = ctx.tree.uses[p]?.parent)
279
+ chain.push(p);
280
+ const label = `${okChild.describe} at line ${use.line}`;
281
+ if (chain.some((a) => okParents.some((m) => m.test(a))))
282
+ return record(ctx, "pass", `${label} is inside ${names}`, at(ctx, i));
283
+ const designSystemAncestors = chain.filter((a) => ctx.tree.components[a] !== undefined);
284
+ if (designSystemAncestors.length === 0)
285
+ return record(ctx, "not-verifiable", `${label}: no design-system element encloses it in this file, so its parent may be in another file`, at(ctx, i));
286
+ record(ctx, "fail", `${label} must be inside ${names}, but sits inside ${nameOf(ctx, ctx.tree.components[designSystemAncestors[0]]?.id ?? "")} instead`, at(ctx, i), `Wrap it in ${names}.`);
287
+ };
288
+ for (const i of ctx.tree.uses.keys())
289
+ each(i);
290
+ }
291
+ /** A file-level scope needs the policy to say which files are instances of the pattern. */
292
+ function fileLevel(ctx) {
293
+ if (ctx.rule.paths.length > 0)
294
+ return true;
295
+ record(ctx, "skipped", "this rule is about a whole file, and the policy names no paths (appliesTo.paths), so there is no way to know this file is an instance of the pattern", undefined);
296
+ return false;
297
+ }
298
+ function evaluateRequiredRegion(ctx, d) {
299
+ const region = matcher(ctx, d.region);
300
+ if (!region.ok)
301
+ return record(ctx, "not-verifiable", region.reason, undefined);
302
+ if (!fileLevel(ctx))
303
+ return;
304
+ const all = ctx.tree.uses.map((_, i) => i);
305
+ const { all: found, certain } = counts(ctx, all, region.test, 0);
306
+ if (certain.length > 0)
307
+ return record(ctx, "pass", `${region.describe} is present`, at(ctx, certain[0]));
308
+ if (found.length > 0)
309
+ return record(ctx, "warning", `${region.describe} appears only conditionally, so it may be missing when rendered`, at(ctx, found[0]), `Render ${region.describe} unconditionally.`);
310
+ const c = closed(ctx, all);
311
+ if (!c.ok)
312
+ return record(ctx, "not-verifiable", `cannot tell whether ${region.describe} is present; ${c.why}`, {
313
+ line: 1,
314
+ column: 1
315
+ });
316
+ record(ctx, "fail", `${region.describe} is missing`, { line: 1, column: 1 }, `Add the components that make up ${region.describe}.`);
317
+ }
318
+ function evaluateOrdering(ctx, d) {
319
+ const before = matcher(ctx, d.before);
320
+ const after = matcher(ctx, d.after);
321
+ const within = d.within === undefined ? undefined : matcher(ctx, d.within);
322
+ const bad = [before, after, within].find((m) => m !== undefined && !m.ok);
323
+ if (bad && !bad.ok)
324
+ return record(ctx, "not-verifiable", bad.reason, undefined);
325
+ if (d.within?.startsWith("region:"))
326
+ return record(ctx, "not-verifiable", "ordering within a region is not evaluated yet", undefined);
327
+ const b = before;
328
+ const a = after;
329
+ const scopes = within === undefined
330
+ ? [{ root: undefined, label: "this file" }]
331
+ : ctx.tree.uses.flatMap((_, i) => within.test(i)
332
+ ? [
333
+ {
334
+ root: i,
335
+ label: `${within.describe} at line ${at(ctx, i).line}`
336
+ }
337
+ ]
338
+ : []);
339
+ if (within === undefined && !fileLevel(ctx))
340
+ return;
341
+ for (const { root, label } of scopes) {
342
+ const scope = scopeOf(ctx, root);
343
+ const bs = scope.filter(b.test);
344
+ const as = scope.filter(a.test);
345
+ const where = root === undefined ? undefined : at(ctx, root);
346
+ if (bs.length === 0 || as.length === 0) {
347
+ record(ctx, "skipped", `${label}: no ${bs.length === 0 ? b.describe : a.describe} to order`, where);
348
+ continue;
349
+ }
350
+ const early = as.find((x) => bs.some((y) => y > x && !isAncestor(ctx, x, y) && !isAncestor(ctx, y, x)));
351
+ if (early !== undefined) {
352
+ const late = bs.find((y) => y > early);
353
+ record(ctx, "fail", `${label}: ${a.describe} (line ${at(ctx, early).line}) comes before ${b.describe} (line ${at(ctx, late).line}); ${b.describe} must come first`, at(ctx, early), `Move ${b.describe} before ${a.describe}.`);
354
+ continue;
355
+ }
356
+ const c = closed(ctx, root === undefined ? scope : [root, ...scope]);
357
+ if (!c.ok)
358
+ record(ctx, "not-verifiable", `${label}: order looks right, but ${c.why}`, where);
359
+ else
360
+ record(ctx, "pass", `${label}: ${b.describe} comes before ${a.describe}`, where);
361
+ }
362
+ }
363
+ function isAncestor(ctx, ancestor, of) {
364
+ for (let p = ctx.tree.uses[of]?.parent; p !== undefined; p = ctx.tree.uses[p]?.parent)
365
+ if (p === ancestor)
366
+ return true;
367
+ return false;
368
+ }
369
+ function evaluateCardinality(ctx, d) {
370
+ const target = matcher(ctx, d.target);
371
+ const within = d.within === undefined ? undefined : matcher(ctx, d.within);
372
+ const bad = [target, within].find((m) => m !== undefined && !m.ok);
373
+ if (bad && !bad.ok)
374
+ return record(ctx, "not-verifiable", bad.reason, undefined);
375
+ if (d.within?.startsWith("region:"))
376
+ return record(ctx, "not-verifiable", "counting within a region is not evaluated yet", undefined);
377
+ const t = target;
378
+ const w = within;
379
+ if (w === undefined && !fileLevel(ctx))
380
+ return;
381
+ const scopes = w === undefined
382
+ ? [{ root: undefined, label: "this file" }]
383
+ : ctx.tree.uses.flatMap((_, i) => w.test(i)
384
+ ? [{ root: i, label: `${w.describe} at line ${at(ctx, i).line}` }]
385
+ : []);
386
+ for (const { root, label } of scopes) {
387
+ const scope = scopeOf(ctx, root);
388
+ const base = root === undefined ? 0 : depthOf(ctx.tree, root);
389
+ const { all, certain, repeated } = counts(ctx, scope, t.test, base, root === undefined ? 0 : loopOf(ctx.tree, root));
390
+ const where = root === undefined ? undefined : at(ctx, root);
391
+ const range = `${d.min !== undefined ? `at least ${d.min}` : ""}${d.min !== undefined && d.max !== undefined ? " and " : ""}${d.max !== undefined ? `at most ${d.max}` : ""}`;
392
+ const seal = closed(ctx, root === undefined ? scope : [root, ...scope]);
393
+ const outcomes = [];
394
+ if (d.max !== undefined) {
395
+ if (certain.length > d.max)
396
+ outcomes.push({
397
+ status: "fail",
398
+ reason: `${label} has ${certain.length} ${t.describe}, but ${range} are allowed`,
399
+ at: at(ctx, certain[d.max]),
400
+ fix: `Keep at most ${d.max} ${t.describe}.`
401
+ });
402
+ else if (repeated.length > 0)
403
+ outcomes.push({
404
+ status: "warning",
405
+ reason: `${label} renders ${t.describe} in a list or callback, so there may be more than ${d.max}`,
406
+ at: at(ctx, repeated[0])
407
+ });
408
+ else if (all.length > d.max)
409
+ outcomes.push({
410
+ status: "warning",
411
+ reason: `${label} may have more than ${d.max} ${t.describe} when conditional ones render`,
412
+ ...(all[0] === undefined ? {} : { at: at(ctx, all[0]) })
413
+ });
414
+ else if (!seal.ok)
415
+ outcomes.push({
416
+ status: "not-verifiable",
417
+ reason: `${label}: ${all.length} ${t.describe} seen (at most ${d.max} allowed), but ${seal.why}`
418
+ });
419
+ else
420
+ outcomes.push({
421
+ status: "pass",
422
+ reason: `${label} has ${all.length} ${t.describe}, within the limit of ${d.max}`
423
+ });
424
+ }
425
+ if (d.min !== undefined) {
426
+ if (certain.length >= d.min)
427
+ outcomes.push({
428
+ status: "pass",
429
+ reason: `${label} has ${certain.length} ${t.describe}, at least ${d.min}`
430
+ });
431
+ else if (all.length >= d.min)
432
+ outcomes.push({
433
+ status: "warning",
434
+ reason: `${label} reaches ${d.min} ${t.describe} only with conditionally rendered ones`,
435
+ ...(all[0] === undefined ? {} : { at: at(ctx, all[0]) }),
436
+ fix: `Render ${t.describe} unconditionally.`
437
+ });
438
+ else if (!seal.ok)
439
+ outcomes.push({
440
+ status: "not-verifiable",
441
+ reason: `${label}: ${all.length} ${t.describe} seen (needs ${d.min}), but ${seal.why}`
442
+ });
443
+ else
444
+ outcomes.push({
445
+ status: "fail",
446
+ reason: `${label} has ${all.length} ${t.describe}, but needs at least ${d.min}`,
447
+ at: where ?? { line: 1, column: 1 },
448
+ fix: `Add ${t.describe}.`
449
+ });
450
+ }
451
+ const worst = PRECEDENCE.find((s) => outcomes.some((o) => o.status === s));
452
+ const picked = outcomes.filter((o) => o.status === worst);
453
+ record(ctx, worst, picked.map((o) => o.reason).join("; "), picked[0]?.at ?? where, picked.find((o) => o.fix)?.fix);
454
+ }
455
+ }
456
+ const STATE_FIX = {
457
+ "form-submission:submitting": "Show that the form is submitting, for example disable the submit button and show progress.",
458
+ "form-submission:error": "Show an error message, and a way to try again, when the submission fails.",
459
+ "form-submission:success": "Confirm the result, for example with a message or by navigating on success.",
460
+ "form-submission:guarded": "Disable the submit button, or return early in the handler, while the form is pending.",
461
+ "destructive-confirmation:confirming": "Ask for confirmation, for example in a dialog, before deleting.",
462
+ "destructive-confirmation:cancellable": "Give the confirmation a Cancel button that closes it without deleting.",
463
+ "search-results:connected": "Use the search text to filter or fetch the results.",
464
+ "search-results:loading": "Show a loading state while the results load.",
465
+ "search-results:empty": "Show an empty state when the search finds nothing.",
466
+ "search-results:error": "Show an error state when the search fails.",
467
+ "search-results:clearable": "Give the user a way to clear the search."
468
+ };
469
+ const LEVEL_RANK = { structural: 1, "static-inferred": 2 };
470
+ /**
471
+ * A workflow state the code must represent. The result says how it was established. A state found only
472
+ * structurally when the rule asks for more is a warning, and one the file hides is not verifiable.
473
+ */
474
+ function evaluateRequiredState(ctx, d) {
475
+ const minimum = d.minimum ?? "structural";
476
+ for (const site of ctx.interactions().filter((s) => s.workflow === d.workflow)) {
477
+ const check = site.states[d.state];
478
+ const where = { line: site.line, column: site.column };
479
+ if (!check) {
480
+ record(ctx, "not-verifiable", `${site.label}: ${d.state} is not a state this analysis looks for`, where);
481
+ }
482
+ else if (check.level) {
483
+ const extra = { level: check.level, evidence: check.evidence };
484
+ if (LEVEL_RANK[check.level] >= LEVEL_RANK[minimum])
485
+ record(ctx, "pass", `${site.label}: ${d.state} is represented (${check.level})`, where, undefined, extra);
486
+ else
487
+ record(ctx, "warning", `${site.label}: ${d.state} is represented only structurally; the rule asks for evidence that the code makes it happen (${minimum})`, where, STATE_FIX[`${d.workflow}:${d.state}`], extra);
488
+ }
489
+ else if (check.open)
490
+ record(ctx, "not-verifiable", `${site.label}: cannot tell whether ${d.state} is represented; ${check.open}`, where);
491
+ else
492
+ record(ctx, "fail", `${site.label}: ${d.state} is not represented; ${check.absent ?? "nothing shows it"}`, where, STATE_FIX[`${d.workflow}:${d.state}`]);
493
+ }
494
+ return undefined;
495
+ }
496
+ const framework = (rule, name) => rule.frameworks.length === 0 || rule.frameworks.includes(name);
497
+ /**
498
+ * Evaluates the structural rules that apply to one file. A rule applies when the policy's frameworks include
499
+ * the file's framework and, if the policy names paths, the file matches one. Rules for other files are not
500
+ * recorded: they are not part of this file's result.
501
+ */
502
+ /**
503
+ * One policy and constraint is evaluated once per file. When inheritance gives a rule a second route to the file,
504
+ * the route that says most wins: the policy's own paths, then inherited paths, then no paths at all.
505
+ */
506
+ function shadowedRules(rules, frameworkName, filename) {
507
+ const shadowed = new Set();
508
+ if (!rules.some((r) => r.inheritedFrom))
509
+ return shadowed;
510
+ const name = filename?.replace(/^\.\//, "");
511
+ const applies = (r) => framework(r, frameworkName) &&
512
+ (r.paths.length === 0 || (name !== undefined && r.paths.some((g) => globToRegExp(g).test(name))));
513
+ const rank = (r) => (r.inheritedFrom ? 1 : r.paths.length > 0 ? 0 : 2);
514
+ const best = new Map();
515
+ for (const r of rules) {
516
+ if (!applies(r))
517
+ continue;
518
+ const key = `${r.policy}|${r.constraint}`;
519
+ const held = best.get(key);
520
+ if (!held || rank(r) < rank(held))
521
+ best.set(key, r);
522
+ }
523
+ for (const r of rules)
524
+ if (applies(r) && best.get(`${r.policy}|${r.constraint}`) !== r)
525
+ shadowed.add(r);
526
+ return shadowed;
527
+ }
528
+ export function evaluatePatternRules(input) {
529
+ const out = { evaluations: [], findings: [] };
530
+ const children = input.tree.uses.map(() => []);
531
+ input.tree.uses.forEach((u, i) => {
532
+ if (u.parent !== undefined)
533
+ children[u.parent]?.push(i);
534
+ });
535
+ let model;
536
+ const interactions = () => {
537
+ if (model)
538
+ return model;
539
+ const components = new Set(input.tree.uses.filter((_, i) => input.tree.kinds[i] === "component").map((u) => u.tag));
540
+ model =
541
+ input.code === undefined
542
+ ? []
543
+ : analyzeInteractions(input.code, input.filename ?? "snippet.tsx", components);
544
+ return model;
545
+ };
546
+ const shadowed = shadowedRules(input.rules, input.framework, input.filename);
547
+ for (const rule of input.rules) {
548
+ if (shadowed.has(rule))
549
+ continue;
550
+ if (!framework(rule, input.framework))
551
+ continue;
552
+ if (rule.paths.length > 0) {
553
+ if (input.filename === undefined) {
554
+ out.evaluations.push({
555
+ rule: CODE[rule.kind],
556
+ status: "skipped",
557
+ reason: "the policy applies to certain paths and no file name was given, so it was not checked",
558
+ pattern: rule.pattern,
559
+ constraint: rule.constraint,
560
+ policy: rule.policy
561
+ });
562
+ continue;
563
+ }
564
+ const name = input.filename.replace(/^\.\//, "");
565
+ if (!rule.paths.some((g) => globToRegExp(g).test(name)))
566
+ continue;
567
+ }
568
+ const ctx = {
569
+ ds: input.ds,
570
+ tree: input.tree,
571
+ rule,
572
+ owner: input.ds.contract.patterns.find((p) => p.id === rule.pattern),
573
+ filename: input.filename,
574
+ children,
575
+ out,
576
+ interactions
577
+ };
578
+ if (input.framework !== "react") {
579
+ record(ctx, "skipped", `pattern rules are evaluated for React JSX only; ${input.framework} templates are not analyzed for patterns yet`, undefined);
580
+ continue;
581
+ }
582
+ switch (rule.kind) {
583
+ case "required-child":
584
+ evaluateRequiredChild(ctx, rule.details);
585
+ break;
586
+ case "forbidden-child":
587
+ evaluateForbiddenChild(ctx, rule.details);
588
+ break;
589
+ case "requires-parent":
590
+ evaluateRequiresParent(ctx, rule.details);
591
+ break;
592
+ case "required-region":
593
+ evaluateRequiredRegion(ctx, rule.details);
594
+ break;
595
+ case "ordering":
596
+ evaluateOrdering(ctx, rule.details);
597
+ break;
598
+ case "cardinality":
599
+ evaluateCardinality(ctx, rule.details);
600
+ break;
601
+ case "required-state":
602
+ evaluateRequiredState(ctx, rule.details);
603
+ break;
604
+ }
605
+ }
606
+ return out;
607
+ }
608
+ //# sourceMappingURL=pattern-rules.js.map