@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,633 @@
1
+ import ts from "typescript";
2
+ import { analyzeFunction, bindingNames, buildScope, candidates, chainHandler, chainStates, chainUnknownHooks, enclosingFunctions, isDeleteLike, lineOf, shown, unwrap } from "./interaction-facts.js";
3
+ const PENDING = /^(?:is|has)?(?:submitting|saving|sending|loading|pending|busy|processing|posting|updating)$/i;
4
+ const LOADING = /^(?:is)?(?:loading|fetching|searching|pending|busy)$/i;
5
+ const ERROR = /error|fail/i;
6
+ const SUCCESS = /success|succeeded|submitted|saved|sent|completed|done/i;
7
+ const OPEN_ATTR = /^(?:open|isOpen|visible|show|opened)$/i;
8
+ const CONFIRM_ATTR = /^on(?:Confirm|Ok|Accept|Submit|Delete|Remove|Yes)$/i;
9
+ const CANCEL_ATTR = /^on(?:Cancel|Close|Dismiss|OpenChange)$/i;
10
+ const DIALOG_TAG = /(?:modal|dialog|drawer|popup|popover|sheet|confirm)/i;
11
+ const SEARCH_TAG = /^(?:search|searchbox|searchfield|searchinput|searchbar)$/i;
12
+ const NAVIGATION_OR_FEEDBACK = /^(?:navigate|push|replace|redirect|toast|notify|showToast|success|enqueueSnackbar|onSuccess|onSubmitted|onDone|onSaved|onComplete|alert)$/i;
13
+ const notCallback = (c) => !/^on[A-Z]/.test(c.name);
14
+ function elementOf(sf, node) {
15
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
16
+ const attrs = new Map();
17
+ let spread = false;
18
+ for (const a of opening.attributes.properties) {
19
+ if (ts.isJsxSpreadAttribute(a)) {
20
+ spread = true;
21
+ continue;
22
+ }
23
+ const name = a.name.getText(sf);
24
+ const init = a.initializer;
25
+ if (!init)
26
+ attrs.set(name, true);
27
+ else if (ts.isStringLiteral(init))
28
+ attrs.set(name, init.text);
29
+ else if (ts.isJsxExpression(init) && init.expression)
30
+ attrs.set(name, init.expression);
31
+ }
32
+ const pos = sf.getLineAndCharacterOfPosition(node.getStart(sf));
33
+ return {
34
+ node,
35
+ tag: opening.tagName.getText(sf),
36
+ attrs,
37
+ spread,
38
+ line: pos.line + 1,
39
+ column: pos.character + 1
40
+ };
41
+ }
42
+ const identifiersIn = (n) => {
43
+ const out = [];
44
+ const walk = (c) => {
45
+ if (ts.isIdentifier(c))
46
+ out.push(c.text);
47
+ ts.forEachChild(c, walk);
48
+ };
49
+ walk(n);
50
+ return out;
51
+ };
52
+ const attrIds = (el, name) => {
53
+ const v = el.attrs.get(name);
54
+ return v !== undefined && typeof v === "object" ? identifiersIn(v) : [];
55
+ };
56
+ const isFn = (n) => ts.isArrowFunction(n) || ts.isFunctionExpression(n);
57
+ /** Reads one file and finds every place an interaction occurs. */
58
+ export function analyzeInteractions(code, filename,
59
+ /** Tags of this file that are design-system components: their content is known to the contract. */
60
+ transparentTags) {
61
+ const sf = ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true, /\.jsx?$/i.test(filename) ? ts.ScriptKind.JSX : ts.ScriptKind.TSX);
62
+ const scopes = new Map();
63
+ const scopeFor = (fn) => {
64
+ const hit = scopes.get(fn);
65
+ if (hit)
66
+ return hit;
67
+ const outer = enclosingFunctions(fn)[0];
68
+ const s = buildScope(sf, fn, outer ? scopeFor(outer) : undefined);
69
+ scopes.set(fn, s);
70
+ return s;
71
+ };
72
+ const chainOf = (node) => ({ scopes: enclosingFunctions(node).map(scopeFor) });
73
+ const elements = [];
74
+ const collect = (n) => {
75
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n))
76
+ elements.push(elementOf(sf, n));
77
+ ts.forEachChild(n, collect);
78
+ };
79
+ collect(sf);
80
+ const isNative = (tag) => /^[a-z][a-z0-9]*$/.test(tag);
81
+ const isForeign = (tag) => !isNative(tag) && tag !== "Fragment" && !transparentTags.has(tag) && !DIALOG_TAG.test(tag);
82
+ const within = (outer, inner) => inner.node !== outer.node &&
83
+ inner.node.getStart(sf) >= outer.node.getStart(sf) &&
84
+ inner.node.getEnd() <= outer.node.getEnd();
85
+ const below = (el) => elements.filter((e) => within(el, e));
86
+ const textOf = (el) => ts.isJsxElement(el.node)
87
+ ? el.node.children
88
+ .filter(ts.isJsxText)
89
+ .map((t) => t.text.trim())
90
+ .join(" ")
91
+ .trim()
92
+ : "";
93
+ const labelOf = (el) => `<${el.tag}> at line ${el.line}`;
94
+ /** Reasons the answer may be somewhere this file does not show: custom hooks, other-file components, spreads. */
95
+ const hidden = (chain, subtree) => {
96
+ const hooks = chainUnknownHooks(chain);
97
+ if (hooks.length > 0)
98
+ return `${[...new Set(hooks)].join(", ")} may supply it, and Invariant cannot see inside`;
99
+ const foreign = subtree.find((e) => isForeign(e.tag));
100
+ if (foreign)
101
+ return `<${foreign.tag}> at line ${foreign.line} comes from another file and may supply it`;
102
+ return undefined;
103
+ };
104
+ /** The function a handler expression means, when this file shows it. */
105
+ const resolveHandler = (value, chain) => {
106
+ if (value === undefined || value === true || typeof value === "string")
107
+ return { open: "no handler is attached here" };
108
+ const e = unwrap(value);
109
+ if (ts.isIdentifier(e)) {
110
+ const info = chainHandler(chain, e.text);
111
+ return info ? { info } : { open: `${e.text} is not defined in this file` };
112
+ }
113
+ if (isFn(e)) {
114
+ const inline = analyzeFunction(sf, e);
115
+ // `() => submit()`: the inline function only hands over to a handler this file defines.
116
+ const delegate = inline.calls.map((c) => chainHandler(chain, c.name)).find((h) => h !== undefined);
117
+ return { info: inline.setters.length === 0 && delegate ? delegate : inline };
118
+ }
119
+ if (ts.isCallExpression(e) && e.arguments[0])
120
+ return resolveHandler(e.arguments[0], chain);
121
+ return { open: `the handler comes from outside this file (${value.getText(sf).slice(0, 40)})` };
122
+ };
123
+ const setsAround = (info, state) => {
124
+ if (info.awaitPos === undefined)
125
+ return false;
126
+ const before = info.setters.some((s) => s.setter === state.setter && s.arg === "true" && s.pos < info.awaitPos);
127
+ const after = info.setters.some((s) => s.setter === state.setter && s.arg === "false" && (s.pos > info.awaitPos || s.inFinally));
128
+ return before && after;
129
+ };
130
+ const sites = [];
131
+ // ---------------------------------------------------------------- form submission
132
+ for (const form of elements.filter((e) => e.attrs.has("onSubmit"))) {
133
+ const chain = chainOf(form.node);
134
+ const inside = below(form);
135
+ const handler = resolveHandler(form.attrs.get("onSubmit"), chain);
136
+ const info = handler.info;
137
+ const hiddenReason = hidden(chain, inside) ??
138
+ (form.spread ? "props are spread onto the form and may carry more" : undefined) ??
139
+ (form.attrs.has("action")
140
+ ? "the form has an action, so a child may read its status with useFormStatus"
141
+ : undefined);
142
+ const states = {};
143
+ const pend = candidates(chain, PENDING).filter(notCallback);
144
+ const pendShown = pend.filter((c) => shown(chain, c.name));
145
+ const flags = chainStates(chain).filter((st) => st.init === "false" || st.init === "other");
146
+ const inferredFlag = info ? flags.find((st) => setsAround(info, st) && shown(chain, st.name)) : undefined;
147
+ {
148
+ const check = { evidence: [] };
149
+ if (pendShown[0]) {
150
+ check.level = "structural";
151
+ check.evidence.push(`${pendShown[0].name} (${pendShown[0].source === "hook" ? `from ${pendShown[0].hook}` : pendShown[0].source}) is read in the interface and is named like a pending flag`);
152
+ }
153
+ if (inferredFlag) {
154
+ check.level = "static-inferred";
155
+ check.evidence.push(`the submit handler (line ${info?.line}) sets ${inferredFlag.name} to true before its await and back to false after, and the interface reads it`);
156
+ }
157
+ if (!check.level) {
158
+ const unused = pend.find((c) => c.source === "state");
159
+ if (unused)
160
+ check.absent = `${unused.name} exists but nothing in the interface reads it`;
161
+ else if (hiddenReason ?? handler.open)
162
+ check.open = hiddenReason ?? `the submit handler cannot be read: ${handler.open}`;
163
+ else
164
+ check.absent = "nothing shows that the form is submitting";
165
+ }
166
+ states.submitting = check;
167
+ }
168
+ {
169
+ const check = { evidence: [] };
170
+ const errs = candidates(chain, ERROR).filter(notCallback);
171
+ const errShown = errs.filter((c) => shown(chain, c.name));
172
+ const alert = elements.find((e) => (e.attrs.get("role") === "alert" || e.attrs.has("aria-live")) &&
173
+ enclosingFunctions(e.node)[0] === enclosingFunctions(form.node)[0]);
174
+ if (errShown[0]) {
175
+ check.level = "structural";
176
+ check.evidence.push(`${errShown[0].name} is read in the interface and is named like an error`);
177
+ }
178
+ else if (alert) {
179
+ check.level = "structural";
180
+ check.evidence.push(`an element with ${alert.attrs.has("role") ? 'role="alert"' : "aria-live"} is on line ${alert.line}`);
181
+ }
182
+ const caught = info
183
+ ? errs.find((c) => c.setter !== undefined && info.setters.some((s) => s.setter === c.setter && s.inCatch))
184
+ : undefined;
185
+ if (caught && (errShown.length > 0 || alert)) {
186
+ check.level = "static-inferred";
187
+ check.evidence.push(`the submit handler sets ${caught.name} when it fails (catch, line ${info?.line})`);
188
+ }
189
+ if (!check.level) {
190
+ if (hiddenReason ?? handler.open)
191
+ check.open = hiddenReason ?? `the submit handler cannot be read: ${handler.open}`;
192
+ else
193
+ check.absent =
194
+ info && !info.hasCatch
195
+ ? "the submit handler has no catch and nothing shows an error"
196
+ : "nothing shows an error";
197
+ }
198
+ states.error = check;
199
+ }
200
+ {
201
+ const check = { evidence: [] };
202
+ const succ = candidates(chain, SUCCESS).filter(notCallback);
203
+ const succShown = succ.filter((c) => shown(chain, c.name));
204
+ if (succShown[0]) {
205
+ check.level = "structural";
206
+ check.evidence.push(`${succShown[0].name} is read in the interface and is named like a success flag`);
207
+ }
208
+ const after = info?.awaitPos;
209
+ const feedback = info && after !== undefined
210
+ ? info.calls.find((c) => c.pos > after && !c.inCatch && NAVIGATION_OR_FEEDBACK.test(c.name))
211
+ : undefined;
212
+ const set = info && after !== undefined
213
+ ? succShown.find((c) => c.setter !== undefined &&
214
+ info.setters.some((s) => s.setter === c.setter && s.pos > after && !s.inCatch))
215
+ : undefined;
216
+ if (feedback) {
217
+ check.level = "static-inferred";
218
+ check.evidence.push(`the submit handler calls ${feedback.full} after its await (line ${feedback.line})`);
219
+ }
220
+ else if (set) {
221
+ check.level = "static-inferred";
222
+ check.evidence.push(`the submit handler sets ${set.name} after its await`);
223
+ }
224
+ if (!check.level) {
225
+ if (hiddenReason ?? handler.open)
226
+ check.open = hiddenReason ?? `the submit handler cannot be read: ${handler.open}`;
227
+ else
228
+ check.absent = "nothing shows that the submission succeeded";
229
+ }
230
+ states.success = check;
231
+ }
232
+ {
233
+ const check = { evidence: [] };
234
+ const names = new Set(pend.map((c) => c.name));
235
+ const control = inside.find((e) => ["disabled", "loading", "isLoading", "isDisabled"].some((a) => attrIds(e, a).some((id) => names.has(id))));
236
+ if (control) {
237
+ check.level = "structural";
238
+ check.evidence.push(`${labelOf(control)} is disabled or loading while ${[...names].find((n) => attrIds(control, "disabled").concat(attrIds(control, "loading"), attrIds(control, "isLoading"), attrIds(control, "isDisabled")).includes(n))} is set`);
239
+ }
240
+ const guarded = info?.guards.find((g) => names.has(g));
241
+ if (guarded) {
242
+ check.level = "static-inferred";
243
+ check.evidence.push(`the submit handler returns early when ${guarded} is set`);
244
+ }
245
+ else if (control &&
246
+ inferredFlag &&
247
+ attrIds(control, "disabled").concat(attrIds(control, "loading")).includes(inferredFlag.name)) {
248
+ check.level = "static-inferred";
249
+ check.evidence.push(`${inferredFlag.name} is set by the handler around its await`);
250
+ }
251
+ if (!check.level) {
252
+ if (hiddenReason ?? handler.open)
253
+ check.open = hiddenReason ?? `the submit handler cannot be read: ${handler.open}`;
254
+ else
255
+ check.absent = "nothing stops the form being submitted again while it is pending";
256
+ }
257
+ states.guarded = check;
258
+ }
259
+ sites.push({
260
+ workflow: "form-submission",
261
+ line: form.line,
262
+ column: form.column,
263
+ label: `form ${labelOf(form)}`,
264
+ states
265
+ });
266
+ }
267
+ // ---------------------------------------------------------------- destructive confirmation
268
+ const dialogs = elements.filter((e) => (isNative(e.tag) ? e.tag === "dialog" : DIALOG_TAG.test(e.tag)));
269
+ const insideDialog = (el) => dialogs.some((d) => within(d, el));
270
+ for (const trigger of elements.filter((e) => (e.attrs.has("onClick") || e.attrs.has("onPress")) && !insideDialog(e))) {
271
+ const chain = chainOf(trigger.node);
272
+ const handler = resolveHandler(trigger.attrs.get("onClick") ?? trigger.attrs.get("onPress"), chain);
273
+ const info = handler.info;
274
+ const deletes = info?.calls.filter((c) => isDeleteLike(c.name)) ?? [];
275
+ const labelled = /^(?:delete|remove|destroy|discard)\b/i.test(textOf(trigger));
276
+ const named = info?.name !== undefined && isDeleteLike(info.name);
277
+ if (!(deletes.length > 0 || named || labelled))
278
+ continue;
279
+ const states = {};
280
+ let dialog;
281
+ let gate;
282
+ const confirming = { evidence: [] };
283
+ const cancellable = { evidence: [] };
284
+ if (!info) {
285
+ confirming.open = `the delete handler cannot be read (${handler.open}), so any confirmation may live in the caller`;
286
+ cancellable.open = confirming.open;
287
+ }
288
+ else if (deletes.length > 0) {
289
+ const first = deletes[0];
290
+ const native = info.calls.find((c) => c.name === "confirm" && c.pos < first.pos);
291
+ if (native) {
292
+ confirming.level = "static-inferred";
293
+ confirming.evidence.push(`the handler calls ${native.full} (line ${native.line}) before ${first.full}`);
294
+ cancellable.level = "structural";
295
+ cancellable.evidence.push(`${native.full} offers a cancel choice`);
296
+ }
297
+ else {
298
+ confirming.absent = `the handler calls ${first.full} (line ${first.line}) directly, with no confirmation step`;
299
+ cancellable.absent = "there is no confirmation to cancel";
300
+ }
301
+ }
302
+ else {
303
+ const states_ = chainStates(chain);
304
+ const opened = info.setters
305
+ .filter((s) => s.arg === "true" || s.arg === "other")
306
+ .map((s) => states_.find((st) => st.setter === s.setter))
307
+ .filter((st) => st !== undefined);
308
+ for (const st of opened) {
309
+ const found = dialogs.find((d) => [...d.attrs.keys()].some((k) => OPEN_ATTR.test(k) && attrIds(d, k).includes(st.name)) ||
310
+ gatedBy(d, st.name));
311
+ if (found) {
312
+ dialog = found;
313
+ gate = st;
314
+ break;
315
+ }
316
+ }
317
+ if (!dialog) {
318
+ const reason = opened[0]
319
+ ? `${opened[0].name} is set, but no dialog in this file reacts to it (it may be rendered elsewhere)`
320
+ : "the handler neither deletes nor opens anything this file shows";
321
+ confirming.open = reason;
322
+ cancellable.open = reason;
323
+ }
324
+ else {
325
+ const confirmEls = [dialog, ...below(dialog)];
326
+ const confirmHandler = confirmEls
327
+ .flatMap((e) => [...e.attrs.entries()]
328
+ .filter(([k]) => CONFIRM_ATTR.test(k) || k === "onClick")
329
+ .map(([, v]) => resolveHandler(v, chainOf(e.node)).info))
330
+ .find((h) => h?.calls.some((c) => isDeleteLike(c.name)));
331
+ confirming.level = confirmHandler ? "static-inferred" : "structural";
332
+ confirming.evidence.push(`${labelOf(trigger)} opens ${labelOf(dialog)} through ${gate?.name}`, confirmHandler
333
+ ? `the delete call (line ${confirmHandler.calls.find((c) => isDeleteLike(c.name))?.line}) is reached from the dialog's confirm action`
334
+ : "the dialog's confirm action was not traced to the delete call");
335
+ const cancel = confirmEls.find((e) => e.node !== dialog?.node && /^(?:cancel|no|keep|dismiss|close|back)\b/i.test(textOf(e)));
336
+ const cancelAttr = [...dialog.attrs.entries()].find(([k]) => CANCEL_ATTR.test(k));
337
+ if (cancel || cancelAttr) {
338
+ cancellable.level = "structural";
339
+ cancellable.evidence.push(cancel ? `${labelOf(cancel)} reads "${textOf(cancel)}"` : `the dialog has ${cancelAttr?.[0]}`);
340
+ const closer = [cancel, dialog]
341
+ .flatMap((e) => e
342
+ ? [...e.attrs.entries()]
343
+ .filter(([k]) => CANCEL_ATTR.test(k) || k === "onClick")
344
+ .map(([, v]) => resolveHandler(v, chainOf(e.node)).info)
345
+ : [])
346
+ .find((h) => h?.setters.some((s) => s.setter === gate?.setter && s.arg === "false"));
347
+ if (closer) {
348
+ cancellable.level = "static-inferred";
349
+ cancellable.evidence.push(`cancelling sets ${gate?.name} back to false`);
350
+ }
351
+ }
352
+ else
353
+ cancellable.absent = "the dialog has no cancel button or close handler";
354
+ }
355
+ }
356
+ states.confirming = confirming;
357
+ states.cancellable = cancellable;
358
+ sites.push({
359
+ workflow: "destructive-confirmation",
360
+ line: trigger.line,
361
+ column: trigger.column,
362
+ label: `destructive action ${labelOf(trigger)}`,
363
+ states
364
+ });
365
+ }
366
+ function gatedBy(dialog, name) {
367
+ for (let p = dialog.node.parent; p; p = p.parent) {
368
+ if (ts.isJsxExpression(p) && p.expression && identifiersIn(p.expression).includes(name))
369
+ return true;
370
+ if (isFn(p) || ts.isFunctionDeclaration(p))
371
+ return false;
372
+ }
373
+ return false;
374
+ }
375
+ // ---------------------------------------------------------------- search and results
376
+ for (const input of elements.filter((e) => (isNative(e.tag) && e.tag === "input" && e.attrs.get("type") === "search") ||
377
+ SEARCH_TAG.test(e.tag) ||
378
+ (isNative(e.tag) &&
379
+ e.tag === "input" &&
380
+ attrIds(e, "value").some((id) => /^(?:q|query|term|keyword|search\w*)$/i.test(id))))) {
381
+ const chain = chainOf(input.node);
382
+ const top = enclosingFunctions(input.node)[enclosingFunctions(input.node).length - 1];
383
+ const all = top
384
+ ? elements.filter((e) => within({ ...input, node: top }, e) || e.node === input.node)
385
+ : elements;
386
+ const stateList = chainStates(chain);
387
+ const bound = attrIds(input, "value")
388
+ .map((id) => stateList.find((s) => s.name === id))
389
+ .find((s) => s !== undefined);
390
+ const changeSetter = (() => {
391
+ const c = input.attrs.get("onChange");
392
+ if (!c || typeof c !== "object")
393
+ return undefined;
394
+ const info = resolveHandler(c, chain).info;
395
+ return info?.setters[0]?.setter;
396
+ })();
397
+ const query = bound ?? stateList.find((s) => s.setter === changeSetter);
398
+ const states = {};
399
+ const hiddenReason = hidden(chain, all);
400
+ const effects = chain.scopes.flatMap((s) => [...s.effects, ...s.handlers.values()]);
401
+ {
402
+ const check = { evidence: [] };
403
+ if (query) {
404
+ const feeding = referencesOutside(chain, query.name, input);
405
+ const strong = feeding.find((r) => r.kind === "filter" || r.kind === "request");
406
+ if (strong) {
407
+ check.level = "static-inferred";
408
+ check.evidence.push(`${query.name} feeds ${strong.kind === "filter" ? "a filter" : "a request"} (line ${strong.line})`);
409
+ }
410
+ else if (feeding[0]) {
411
+ check.level = "structural";
412
+ check.evidence.push(`${query.name} is also used at line ${feeding[0].line}`);
413
+ }
414
+ }
415
+ if (!check.level) {
416
+ if (!query)
417
+ check.open = "the input is not tied to state this file can see";
418
+ else if (hiddenReason)
419
+ check.open = hiddenReason;
420
+ else
421
+ check.absent = `${query.name} is set by the input but nothing else uses it`;
422
+ }
423
+ states.connected = check;
424
+ }
425
+ {
426
+ const check = { evidence: [] };
427
+ const loading = candidates(chain, LOADING).filter(notCallback);
428
+ const loadShown = loading.filter((c) => shown(chain, c.name));
429
+ if (loadShown[0]) {
430
+ check.level = "structural";
431
+ check.evidence.push(`${loadShown[0].name} is read in the interface and is named like a loading flag`);
432
+ }
433
+ const flag = stateList.find((s) => (s.init === "false" || s.init === "other") &&
434
+ shown(chain, s.name) &&
435
+ effects.some((f) => setsAround(f, s)));
436
+ if (flag) {
437
+ check.level = "static-inferred";
438
+ check.evidence.push(`${flag.name} is set to true before an await and back to false after, and the interface reads it`);
439
+ }
440
+ if (!check.level) {
441
+ if (hiddenReason)
442
+ check.open = hiddenReason;
443
+ else
444
+ check.absent = "nothing shows that results are loading";
445
+ }
446
+ states.loading = check;
447
+ }
448
+ {
449
+ const check = { evidence: [] };
450
+ const derived = new Set();
451
+ if (query)
452
+ for (const s of chain.scopes) {
453
+ const body = s.fn.body;
454
+ if (body && ts.isBlock(body))
455
+ for (const stmt of body.statements)
456
+ if (ts.isVariableStatement(stmt))
457
+ for (const d of stmt.declarationList.declarations)
458
+ if (d.initializer && identifiersIn(d.initializer).includes(query.name))
459
+ for (const n of bindingNames(d.name))
460
+ derived.add(n);
461
+ }
462
+ const empties = emptinessChecks(top ?? sf);
463
+ const text = jsxTextMatch(top ?? sf, /no (?:results|matches|items|data)|nothing (?:found|matches)|no .{0,20} found/i);
464
+ const emptyEl = all.find((e) => /empty/i.test(e.tag));
465
+ const fromQuery = empties.find((e) => derived.has(e.id));
466
+ if (fromQuery) {
467
+ check.level = "static-inferred";
468
+ check.evidence.push(`${fromQuery.id}.length is checked in the interface, and ${fromQuery.id} is derived from ${query?.name} (line ${fromQuery.line})`);
469
+ }
470
+ else if (empties[0] || text || emptyEl) {
471
+ check.level = "structural";
472
+ check.evidence.push(empties[0]
473
+ ? `${empties[0].id}.length is checked in the interface (line ${empties[0].line})`
474
+ : text
475
+ ? `the interface says "${text.text}" (line ${text.line})`
476
+ : `${labelOf(emptyEl)} is rendered`);
477
+ }
478
+ if (!check.level) {
479
+ if (hiddenReason)
480
+ check.open = hiddenReason;
481
+ else
482
+ check.absent = "nothing tells the user there are no results";
483
+ }
484
+ states.empty = check;
485
+ }
486
+ {
487
+ const check = { evidence: [] };
488
+ const errs = candidates(chain, ERROR).filter(notCallback);
489
+ const errShown = errs.filter((c) => shown(chain, c.name));
490
+ if (errShown[0]) {
491
+ check.level = "structural";
492
+ check.evidence.push(`${errShown[0].name} is read in the interface and is named like an error`);
493
+ }
494
+ const caught = errShown.find((c) => c.setter !== undefined &&
495
+ effects.some((f) => f.setters.some((s) => s.setter === c.setter && s.inCatch)));
496
+ if (caught) {
497
+ check.level = "static-inferred";
498
+ check.evidence.push(`${caught.name} is set when the search fails (catch)`);
499
+ }
500
+ if (!check.level) {
501
+ if (hiddenReason)
502
+ check.open = hiddenReason;
503
+ else
504
+ check.absent = "nothing shows that a search failed";
505
+ }
506
+ states.error = check;
507
+ }
508
+ {
509
+ const check = { evidence: [] };
510
+ if (input.attrs.get("type") === "search") {
511
+ check.level = "structural";
512
+ check.evidence.push('the input has type="search", which browsers give a clear control');
513
+ }
514
+ if (input.attrs.has("onClear") || input.attrs.has("clearable")) {
515
+ check.level = "structural";
516
+ check.evidence.push(`${labelOf(input)} has ${input.attrs.has("onClear") ? "onClear" : "clearable"}`);
517
+ }
518
+ if (query) {
519
+ const clears = (el) => ["onClick", "onPress", "onClear"].some((a) => {
520
+ const h = el.attrs.get(a);
521
+ return (h !== undefined &&
522
+ resolveHandler(h, chainOf(el.node)).info?.setters.some((s) => s.setter === query.setter && s.arg === "empty") === true);
523
+ });
524
+ const control = all.find(clears);
525
+ if (control) {
526
+ check.level = "static-inferred";
527
+ check.evidence.push(`${labelOf(control)} sets ${query.name} to an empty string`);
528
+ }
529
+ }
530
+ if (!check.level) {
531
+ if (hiddenReason)
532
+ check.open = hiddenReason;
533
+ else
534
+ check.absent = "there is no way to clear the search";
535
+ }
536
+ states.clearable = check;
537
+ }
538
+ sites.push({
539
+ workflow: "search-results",
540
+ line: input.line,
541
+ column: input.column,
542
+ label: `search ${labelOf(input)}`,
543
+ states
544
+ });
545
+ }
546
+ function referencesOutside(chain, name, site) {
547
+ const out = [];
548
+ const start = site.node.getStart(sf);
549
+ const end = site.node.getEnd();
550
+ const root = chain.scopes[chain.scopes.length - 1]?.fn;
551
+ if (!root)
552
+ return out;
553
+ const walk = (n) => {
554
+ if (ts.isIdentifier(n) && n.text === name && !(n.getStart(sf) >= start && n.getEnd() <= end)) {
555
+ const p = n.parent;
556
+ if (ts.isVariableDeclaration(p) || ts.isBindingElement(p) || ts.isArrayBindingPattern(p))
557
+ return;
558
+ out.push({ kind: classify(n), line: lineOf(sf, n) });
559
+ }
560
+ ts.forEachChild(n, walk);
561
+ };
562
+ walk(root);
563
+ return out;
564
+ }
565
+ function classify(id) {
566
+ for (let p = id.parent; p; p = p.parent) {
567
+ if (ts.isCallExpression(p)) {
568
+ const callee = ts.isPropertyAccessExpression(p.expression)
569
+ ? p.expression.name.text
570
+ : ts.isIdentifier(p.expression)
571
+ ? p.expression.text
572
+ : "";
573
+ if (/^(?:filter|find|some|every|includes|search)$/.test(callee))
574
+ return "filter";
575
+ if (/^(?:fetch|get|post|search|query|load\w*|fetch\w*|request)$/i.test(callee))
576
+ return "request";
577
+ }
578
+ if (ts.isFunctionDeclaration(p))
579
+ break;
580
+ }
581
+ return "other";
582
+ }
583
+ function emptinessChecks(root) {
584
+ const out = [];
585
+ const lengthOf = (e) => {
586
+ const x = unwrap(e);
587
+ return ts.isPropertyAccessExpression(x) &&
588
+ x.name.text === "length" &&
589
+ ts.isIdentifier(unwrap(x.expression))
590
+ ? unwrap(x.expression).text
591
+ : undefined;
592
+ };
593
+ const visit = (n, inJsx) => {
594
+ const now = inJsx || ts.isJsxExpression(n);
595
+ if (now) {
596
+ if (ts.isBinaryExpression(n)) {
597
+ const l = lengthOf(n.left) ?? lengthOf(n.right);
598
+ if (l)
599
+ out.push({ id: l, line: lineOf(sf, n) });
600
+ }
601
+ if (ts.isPrefixUnaryExpression(n) && n.operator === ts.SyntaxKind.ExclamationToken) {
602
+ const l = lengthOf(n.operand);
603
+ if (l)
604
+ out.push({ id: l, line: lineOf(sf, n) });
605
+ }
606
+ if (ts.isConditionalExpression(n)) {
607
+ const l = lengthOf(n.condition);
608
+ if (l)
609
+ out.push({ id: l, line: lineOf(sf, n) });
610
+ }
611
+ }
612
+ ts.forEachChild(n, (c) => visit(c, now));
613
+ };
614
+ visit(root, false);
615
+ return out;
616
+ }
617
+ function jsxTextMatch(root, re) {
618
+ let found;
619
+ const visit = (n) => {
620
+ if (found)
621
+ return;
622
+ if (ts.isJsxText(n) && re.test(n.text))
623
+ found = { text: n.text.trim(), line: lineOf(sf, n) };
624
+ if (ts.isStringLiteral(n) && ts.isJsxExpression(n.parent) && re.test(n.text))
625
+ found = { text: n.text, line: lineOf(sf, n) };
626
+ ts.forEachChild(n, visit);
627
+ };
628
+ visit(root);
629
+ return found;
630
+ }
631
+ return sites.sort((a, b) => a.line - b.line || a.column - b.column);
632
+ }
633
+ //# sourceMappingURL=interaction-workflows.js.map