@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1002.1

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 (147) hide show
  1. package/LICENSE.md +55 -0
  2. package/README.md +166 -0
  3. package/dist/branding.d.ts +25 -0
  4. package/dist/branding.js +27 -0
  5. package/dist/claudeCompat.d.ts +78 -0
  6. package/dist/claudeCompat.js +198 -0
  7. package/dist/cli.d.ts +55 -0
  8. package/dist/cli.js +326 -0
  9. package/dist/config.d.ts +19 -0
  10. package/dist/config.js +28 -0
  11. package/dist/credentials.d.ts +41 -0
  12. package/dist/credentials.js +75 -0
  13. package/dist/distribution.d.ts +15 -0
  14. package/dist/distribution.js +45 -0
  15. package/dist/doctor.d.ts +94 -0
  16. package/dist/doctor.js +343 -0
  17. package/dist/extension/advisor.d.ts +84 -0
  18. package/dist/extension/advisor.js +101 -0
  19. package/dist/extension/askAdvisorTool.d.ts +83 -0
  20. package/dist/extension/askAdvisorTool.js +188 -0
  21. package/dist/extension/askYagniTool.d.ts +39 -0
  22. package/dist/extension/askYagniTool.js +63 -0
  23. package/dist/extension/bless.d.ts +45 -0
  24. package/dist/extension/bless.js +74 -0
  25. package/dist/extension/branding.d.ts +48 -0
  26. package/dist/extension/branding.js +119 -0
  27. package/dist/extension/chipEditor.d.ts +117 -0
  28. package/dist/extension/chipEditor.js +369 -0
  29. package/dist/extension/config.d.ts +112 -0
  30. package/dist/extension/config.js +108 -0
  31. package/dist/extension/costHud.d.ts +73 -0
  32. package/dist/extension/costHud.js +123 -0
  33. package/dist/extension/decisionCapture.d.ts +52 -0
  34. package/dist/extension/decisionCapture.js +66 -0
  35. package/dist/extension/decisions.d.ts +83 -0
  36. package/dist/extension/decisions.js +200 -0
  37. package/dist/extension/diagnostics.d.ts +41 -0
  38. package/dist/extension/diagnostics.js +110 -0
  39. package/dist/extension/index.d.ts +116 -0
  40. package/dist/extension/index.js +388 -0
  41. package/dist/extension/initDone.d.ts +28 -0
  42. package/dist/extension/initDone.js +66 -0
  43. package/dist/extension/initPass.d.ts +152 -0
  44. package/dist/extension/initPass.js +394 -0
  45. package/dist/extension/nextWorkTool.d.ts +51 -0
  46. package/dist/extension/nextWorkTool.js +80 -0
  47. package/dist/extension/permission.d.ts +91 -0
  48. package/dist/extension/permission.js +236 -0
  49. package/dist/extension/pipeline/activity.d.ts +37 -0
  50. package/dist/extension/pipeline/activity.js +151 -0
  51. package/dist/extension/pipeline/activityFeed.d.ts +59 -0
  52. package/dist/extension/pipeline/activityFeed.js +175 -0
  53. package/dist/extension/pipeline/budget.d.ts +48 -0
  54. package/dist/extension/pipeline/budget.js +68 -0
  55. package/dist/extension/pipeline/checkpoint.d.ts +31 -0
  56. package/dist/extension/pipeline/checkpoint.js +176 -0
  57. package/dist/extension/pipeline/eval.d.ts +168 -0
  58. package/dist/extension/pipeline/eval.js +182 -0
  59. package/dist/extension/pipeline/events.d.ts +56 -0
  60. package/dist/extension/pipeline/events.js +147 -0
  61. package/dist/extension/pipeline/findings.d.ts +42 -0
  62. package/dist/extension/pipeline/findings.js +144 -0
  63. package/dist/extension/pipeline/finish.d.ts +128 -0
  64. package/dist/extension/pipeline/finish.js +307 -0
  65. package/dist/extension/pipeline/goCommand.d.ts +128 -0
  66. package/dist/extension/pipeline/goCommand.js +972 -0
  67. package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
  68. package/dist/extension/pipeline/goCompareCommand.js +184 -0
  69. package/dist/extension/pipeline/goFlags.d.ts +40 -0
  70. package/dist/extension/pipeline/goFlags.js +46 -0
  71. package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
  72. package/dist/extension/pipeline/goStatusCommands.js +188 -0
  73. package/dist/extension/pipeline/invocation.d.ts +45 -0
  74. package/dist/extension/pipeline/invocation.js +64 -0
  75. package/dist/extension/pipeline/orchestrator.d.ts +131 -0
  76. package/dist/extension/pipeline/orchestrator.js +636 -0
  77. package/dist/extension/pipeline/personas.d.ts +44 -0
  78. package/dist/extension/pipeline/personas.js +248 -0
  79. package/dist/extension/pipeline/resilience.d.ts +85 -0
  80. package/dist/extension/pipeline/resilience.js +166 -0
  81. package/dist/extension/pipeline/resume.d.ts +18 -0
  82. package/dist/extension/pipeline/resume.js +106 -0
  83. package/dist/extension/pipeline/runRegistry.d.ts +112 -0
  84. package/dist/extension/pipeline/runRegistry.js +202 -0
  85. package/dist/extension/pipeline/runSession.d.ts +152 -0
  86. package/dist/extension/pipeline/runSession.js +167 -0
  87. package/dist/extension/pipeline/runState.d.ts +158 -0
  88. package/dist/extension/pipeline/runState.js +264 -0
  89. package/dist/extension/pipeline/runner.d.ts +79 -0
  90. package/dist/extension/pipeline/runner.js +298 -0
  91. package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
  92. package/dist/extension/pipeline/scrubSecrets.js +32 -0
  93. package/dist/extension/pipeline/stages.d.ts +51 -0
  94. package/dist/extension/pipeline/stages.js +113 -0
  95. package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
  96. package/dist/extension/pipeline/ticketResolution.js +75 -0
  97. package/dist/extension/pipeline/types.d.ts +436 -0
  98. package/dist/extension/pipeline/types.js +48 -0
  99. package/dist/extension/pipeline/verify.d.ts +277 -0
  100. package/dist/extension/pipeline/verify.js +758 -0
  101. package/dist/extension/pipeline/workspace.d.ts +32 -0
  102. package/dist/extension/pipeline/workspace.js +73 -0
  103. package/dist/extension/pipeline/worktree.d.ts +107 -0
  104. package/dist/extension/pipeline/worktree.js +200 -0
  105. package/dist/extension/provider.d.ts +11 -0
  106. package/dist/extension/provider.js +26 -0
  107. package/dist/extension/recall.d.ts +93 -0
  108. package/dist/extension/recall.js +187 -0
  109. package/dist/extension/recordContextTool.d.ts +38 -0
  110. package/dist/extension/recordContextTool.js +85 -0
  111. package/dist/extension/recordDecisionTool.d.ts +52 -0
  112. package/dist/extension/recordDecisionTool.js +102 -0
  113. package/dist/extension/repoDocs.d.ts +81 -0
  114. package/dist/extension/repoDocs.js +260 -0
  115. package/dist/extension/resilientFetch.d.ts +60 -0
  116. package/dist/extension/resilientFetch.js +133 -0
  117. package/dist/extension/reviewTool.d.ts +34 -0
  118. package/dist/extension/reviewTool.js +81 -0
  119. package/dist/extension/spool.d.ts +92 -0
  120. package/dist/extension/spool.js +266 -0
  121. package/dist/extension/stateHome.d.ts +2 -0
  122. package/dist/extension/stateHome.js +6 -0
  123. package/dist/extension/subagents.d.ts +135 -0
  124. package/dist/extension/subagents.js +281 -0
  125. package/dist/extension/surface.d.ts +10 -0
  126. package/dist/extension/surface.js +12 -0
  127. package/dist/extension/todos.d.ts +110 -0
  128. package/dist/extension/todos.js +217 -0
  129. package/dist/extension/tokenProvider.d.ts +93 -0
  130. package/dist/extension/tokenProvider.js +234 -0
  131. package/dist/launch.d.ts +76 -0
  132. package/dist/launch.js +111 -0
  133. package/dist/login.d.ts +45 -0
  134. package/dist/login.js +142 -0
  135. package/dist/logout.d.ts +14 -0
  136. package/dist/logout.js +34 -0
  137. package/dist/paths.d.ts +31 -0
  138. package/dist/paths.js +87 -0
  139. package/dist/piPackage.d.ts +33 -0
  140. package/dist/piPackage.js +71 -0
  141. package/dist/profiles.d.ts +80 -0
  142. package/dist/profiles.js +222 -0
  143. package/dist/refresh.d.ts +70 -0
  144. package/dist/refresh.js +117 -0
  145. package/dist/upgrade.d.ts +97 -0
  146. package/dist/upgrade.js +284 -0
  147. package/package.json +39 -3
@@ -0,0 +1,758 @@
1
+ /**
2
+ * P4 - the deterministic, non-LLM verify gate so "clean" means "builds".
3
+ *
4
+ * The /go review half is judgment (LLM lenses). Judgment can like a change that
5
+ * does not compile: the recorded false-clean defect. This module runs each changed
6
+ * package's own secret-independent build check (`typecheck`/`lint`) via `execFile`
7
+ * and turns a non-zero exit into `critical` Findings that flow through the SAME
8
+ * review -> fix loop, so the loop cannot stop `clean` while the build is broken and
9
+ * the fix stage gets the real errors.
10
+ *
11
+ * `detectVerifyCommands`, `detectVerifyTestCommands`, `parseVerifyFailures`,
12
+ * `classifyTestFailure`, `isFannedOutScript`, `parseChangedPaths` and
13
+ * `resolvePackageDirs` are PURE (unit-tested directly). `makeRunVerify` is the
14
+ * thin impure runner; it shells out (pi's ExtensionContext exposes no exec,
15
+ * mirroring workspace.ts) and is wrapped with R1's timeout composition
16
+ * (`composeAbortSignal`) so a slow or flaky suite is bounded.
17
+ *
18
+ * Scoped-by-default: in a monorepo the gate runs EVERY changed package's own
19
+ * check in that package's dir, never the repo-root recursive runner (`pnpm -r
20
+ * test`, `turbo`, an e2e suite). The changed packages are found by reusing
21
+ * workspace.ts's git seam MINUS the tree's pre-run dirt, so judgment stays
22
+ * scoped to THIS run's real diff and an unrelated dirty package never retargets
23
+ * the gate.
24
+ *
25
+ * TWO HALVES (spec §3d). The TYPECHECK half runs each changed package's
26
+ * secret-independent build check (`typecheck`/`lint`, never `test`) under the
27
+ * scrubbed {@link buildVerifyEnv}; its behavior is frozen (invariant §0.2). The
28
+ * TEST half then runs each changed package's own `test` script under the user's
29
+ * real environment MINUS every `YAGNI_*` var and NODE_OPTIONS
30
+ * ({@link buildTestEnv}) — tests legitimately need the user's DB / local infra,
31
+ * but repo-defined commands never see YAGNI tokens (§0.9). Test output is
32
+ * classified by the pure {@link classifyTestFailure}: parseable test failures
33
+ * become `critical` findings through the same review->fix loop; infra/env
34
+ * failures (connection refused, missing env, DB/auth, command not found) and
35
+ * anything UNCERTAIN fail OPEN with an honest note, never a never-clearable
36
+ * critical.
37
+ *
38
+ * Safe-by-default and FAIL-OPEN: if no scoped, non-fanned-out command is
39
+ * detectable, or the command cannot be spawned, or it times out, the gate
40
+ * produces no findings and records an honest note rather than blocking a real
41
+ * change on its own inability to run. Repo-local `.yagni-code/verify` overrides
42
+ * are opt-in only and, when active, ARE the whole gate (they cover the test half
43
+ * too — no detected `test` script runs alongside an override). All captured
44
+ * command output passes through {@link scrubSecrets} before it can enter
45
+ * findings, handoffs, or run records (§0.9).
46
+ */
47
+ import { execFile } from "node:child_process";
48
+ import { readFileSync } from "node:fs";
49
+ import { basename, dirname, join } from "node:path";
50
+ import { composeAbortSignal } from "./resilience.js";
51
+ import { scrubSecrets } from "./scrubSecrets.js";
52
+ import { snapshotWorkspace } from "./workspace.js";
53
+ /** Total wall-clock budget for a single verify run before it is abandoned (fail-open). */
54
+ export const VERIFY_TIMEOUT_MS = 10 * 60_000;
55
+ /** Per-PACKAGE wall-clock budget for the test half (spec §3d); overrun fails open. */
56
+ export const VERIFY_TEST_TIMEOUT_MS = 10 * 60_000;
57
+ /** Cap on located findings parsed from one failing run, and on the opaque-tail message. */
58
+ const MAX_LOCATED_FINDINGS = 25;
59
+ const MAX_TAIL_CHARS = 1000;
60
+ /** The verify lens reuses `does_it_hold` (does the change build/test) to avoid widening ReviewLens. */
61
+ const VERIFY_LENS = "does_it_hold";
62
+ /**
63
+ * The priority order of package.json scripts we treat as a verify command.
64
+ * Deliberately a SECRET-INDEPENDENT build check (`typecheck`/`lint`), NEVER the
65
+ * package `test`. The verify env is scrubbed of secrets (see {@link buildVerifyEnv}),
66
+ * so a package whose tests need infra (a DB, an e2e secret) can never exit 0 under
67
+ * it: selecting `test` would reintroduce the deterministic, never-clearable
68
+ * `critical` that forces round_cap on every run (the exact defect this gate exists
69
+ * to avoid). The gate's job is "the change compiles"; the worker stages and CI run
70
+ * the tests. `build` is also excluded (it writes dist/.next/ artifacts into the
71
+ * tree). A repo that wants a different check opts in via `.yagni-code/verify`.
72
+ */
73
+ const SCRIPT_PRIORITY = ["verify", "typecheck", "lint"];
74
+ function tryRead(readFile, path) {
75
+ try {
76
+ return readFile(path);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Detect the package manager from a lockfile (default npm). Exported for the
84
+ * worktree bootstrap (spec §3b), which must install with the same pm the verify
85
+ * gate will later run scripts through.
86
+ */
87
+ export function detectPackageManager(cwd, readFile) {
88
+ if (tryRead(readFile, join(cwd, "pnpm-lock.yaml")) != null)
89
+ return "pnpm";
90
+ if (tryRead(readFile, join(cwd, "yarn.lock")) != null)
91
+ return "yarn";
92
+ return "npm";
93
+ }
94
+ /**
95
+ * True for a script body that fans out across packages or shells a secret-gated
96
+ * e2e runner: `pnpm` with `-r`/`--recursive`/`--filter`, or `turbo` / `lerna` /
97
+ * `nx run-many` / `playwright`. Such a script can never exit 0 from a
98
+ * single-package change under the scrubbed verify env (it drags in unrelated
99
+ * packages and missing secrets), so it must NEVER be chosen as the gate command:
100
+ * choosing it is the deterministic false-`critical` this gate exists to avoid.
101
+ * Applied to BOTH the scoped package script and the repo-root fallback. The `-r`
102
+ * flag is matched at a flag boundary so it never hits an innocent substring.
103
+ */
104
+ export function isFannedOutScript(body) {
105
+ if (/\bpnpm\b/.test(body) && /(?:^|\s)(?:-r|--recursive|--filter)(?:[\s=]|$)/.test(body))
106
+ return true;
107
+ if (/\b(?:turbo|lerna)\b/.test(body))
108
+ return true;
109
+ if (/\bnx\s+run-many\b/.test(body))
110
+ return true;
111
+ if (/\bplaywright\b/.test(body))
112
+ return true;
113
+ return false;
114
+ }
115
+ /**
116
+ * Parse `git status --porcelain` into repo-relative changed paths. Each line is
117
+ * `XY <path>`; we drop the 2-char XY status + its separating space, and for a
118
+ * rename/copy (`R old -> new`) take the destination. Pure; the impure git
119
+ * shell-out is {@link snapshotWorkspace}.
120
+ */
121
+ export function parseChangedPaths(status) {
122
+ const paths = [];
123
+ for (const raw of status.split("\n")) {
124
+ if (raw.length < 4)
125
+ continue; // need at least "XY p"
126
+ const entry = raw.slice(3); // drop the 2-char XY status + its separating space
127
+ const arrow = entry.indexOf(" -> ");
128
+ const path = (arrow >= 0 ? entry.slice(arrow + 4) : entry).trim();
129
+ if (path)
130
+ paths.push(path);
131
+ }
132
+ return paths;
133
+ }
134
+ /** Walk up from a changed path's dir to repoRoot; return the nearest dir holding a package.json (or null). */
135
+ function nearestPackageDir(repoRoot, rel, readFile) {
136
+ let dir = dirname(join(repoRoot, rel));
137
+ while (true) {
138
+ if (tryRead(readFile, join(dir, "package.json")) != null)
139
+ return dir;
140
+ if (dir === repoRoot)
141
+ return null;
142
+ const parent = dirname(dir);
143
+ if (parent === dir)
144
+ return null; // reached the filesystem root without a package.json
145
+ dir = parent;
146
+ }
147
+ }
148
+ /**
149
+ * Map each repo-relative changed path to its nearest enclosing package dir (the
150
+ * one with a package.json, repoRoot inclusive), deduped + deterministically
151
+ * sorted. `readFile` is injected so this stays pure. Used to SCOPE the verify gate
152
+ * to the package(s) a change actually touched instead of the whole monorepo.
153
+ */
154
+ export function resolvePackageDirs(repoRoot, relPaths, readFile) {
155
+ const dirs = new Set();
156
+ for (const rel of relPaths) {
157
+ const dir = nearestPackageDir(repoRoot, rel, readFile);
158
+ if (dir)
159
+ dirs.add(dir);
160
+ }
161
+ return [...dirs].sort();
162
+ }
163
+ /**
164
+ * This run's OWN changed package dirs: the porcelain `status`'s packages MINUS the
165
+ * packages touched only by `baselinePaths` (the tree's pre-run dirt). A package with
166
+ * a NEW change stays in scope even if it also had pre-run dirt; a package whose every
167
+ * changed path predates the run drops out. Pure (the `readFile` seam keeps it so), so
168
+ * an unrelated dirty package can never retarget the gate off this run's real diff.
169
+ */
170
+ export function ownChangedDirs(repoRoot, status, baselinePaths, readFile) {
171
+ const baseline = new Set(baselinePaths);
172
+ const own = parseChangedPaths(status).filter((p) => !baseline.has(p));
173
+ return resolvePackageDirs(repoRoot, own, readFile);
174
+ }
175
+ /**
176
+ * The highest-priority package.json script at `pkgDir` that is a string AND not a
177
+ * fanned-out/e2e runner, as a {@link VerifyCommand} that runs IN `pkgDir`. `pm` is
178
+ * the repo's package manager (detected once at the repo root, since a monorepo's
179
+ * lockfile lives there, not in each package). Null when the dir has no usable
180
+ * script, so the caller tries the next scope or fails open.
181
+ */
182
+ function pickScript(pkgDir, readFile, pm) {
183
+ const pkgRaw = tryRead(readFile, join(pkgDir, "package.json"));
184
+ if (!pkgRaw)
185
+ return null;
186
+ let scripts = {};
187
+ try {
188
+ scripts = JSON.parse(pkgRaw).scripts ?? {};
189
+ }
190
+ catch {
191
+ scripts = {};
192
+ }
193
+ for (const name of SCRIPT_PRIORITY) {
194
+ const body = scripts[name];
195
+ if (typeof body === "string" && !isFannedOutScript(body)) {
196
+ return { argv: [pm, "run", name], label: name, source: "script", cwd: pkgDir };
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ /**
202
+ * Resolve the verify command(s) for a repo. An explicitly allowed `.yagni-code/verify`
203
+ * override wins (first non-comment, non-empty line, whitespace-split into argv; run
204
+ * at the repo root) as a single command. Otherwise the gate is SCOPED to the change:
205
+ * EVERY changed package's highest-priority, non-fanned-out script is returned (each
206
+ * run in its own package), falling back to the repo-root script (still rejecting
207
+ * fanned-out runners). Covering all changed packages (not just the first) stops the
208
+ * gate giving false confidence by checking the wrong package. Returns [] when nothing
209
+ * is usable, so the caller fails open rather than running the repo-root recursive
210
+ * runner and emitting a false `critical`.
211
+ */
212
+ export function detectVerifyCommands(cwd, opts = {}) {
213
+ const readFile = opts.readFile ?? ((p) => readFileSync(p, "utf8"));
214
+ const override = opts.allowRepoOverride ? tryRead(readFile, join(cwd, ".yagni-code", "verify")) : null;
215
+ if (override) {
216
+ for (const raw of override.split("\n")) {
217
+ const line = raw.trim();
218
+ if (!line || line.startsWith("#"))
219
+ continue;
220
+ const argv = line.split(/\s+/);
221
+ if (argv.length > 0)
222
+ return [{ argv, label: line, source: "override", cwd }];
223
+ }
224
+ }
225
+ // Detect the package manager ONCE at the repo root: a monorepo keeps a single
226
+ // lockfile there, so detecting it inside a changed package dir would mis-fall to npm.
227
+ const pm = detectPackageManager(cwd, readFile);
228
+ // Scope to the change: run the build check of EVERY changed package (deterministic
229
+ // order from resolvePackageDirs), each in its own dir. This stays off the repo-root
230
+ // recursive runner AND never silently checks only the first (lexicographically
231
+ // smallest) package while leaving the package the run actually targeted unverified.
232
+ const commands = [];
233
+ const seen = new Set();
234
+ for (const dir of opts.changedDirs ?? []) {
235
+ if (seen.has(dir))
236
+ continue;
237
+ seen.add(dir);
238
+ const scoped = pickScript(dir, readFile, pm);
239
+ if (scoped)
240
+ commands.push(scoped);
241
+ }
242
+ if (commands.length > 0)
243
+ return commands;
244
+ // Fallback: the repo-root script, still rejecting fanned-out runners so a
245
+ // recursive `pnpm -r test` is never selected anywhere. Empty => fail open.
246
+ const root = pickScript(cwd, readFile, pm);
247
+ return root ? [root] : [];
248
+ }
249
+ /** Back-compat singular: the first detected verify command, or null. */
250
+ export function detectVerifyCommand(cwd, opts = {}) {
251
+ return detectVerifyCommands(cwd, opts)[0] ?? null;
252
+ }
253
+ /** The package.json `test` script at `pkgDir` as a VerifyCommand, when usable. */
254
+ function pickTestScript(pkgDir, readFile, pm) {
255
+ const pkgRaw = tryRead(readFile, join(pkgDir, "package.json"));
256
+ if (!pkgRaw)
257
+ return null;
258
+ let scripts = {};
259
+ try {
260
+ scripts = JSON.parse(pkgRaw).scripts ?? {};
261
+ }
262
+ catch {
263
+ scripts = {};
264
+ }
265
+ const body = scripts.test;
266
+ if (typeof body === "string" && !isFannedOutScript(body)) {
267
+ return { argv: [pm, "run", "test"], label: "test", source: "script", cwd: pkgDir };
268
+ }
269
+ return null;
270
+ }
271
+ /**
272
+ * Resolve the TEST-half command(s) (spec §3d): every changed package's own
273
+ * `test` script (each run in its own dir), falling back to the repo-root `test`
274
+ * script; a fanned-out script (`pnpm -r test`, turbo, playwright) is rejected
275
+ * everywhere (the same false-critical guard as the typecheck half). Returns []
276
+ * when nothing is usable so the caller fails open. The `.yagni-code/verify`
277
+ * override is deliberately NOT consulted here: an active override IS the whole
278
+ * gate and the caller skips the test half entirely.
279
+ */
280
+ export function detectVerifyTestCommands(cwd, opts = {}) {
281
+ const readFile = opts.readFile ?? ((p) => readFileSync(p, "utf8"));
282
+ const pm = detectPackageManager(cwd, readFile);
283
+ const commands = [];
284
+ const seen = new Set();
285
+ for (const dir of opts.changedDirs ?? []) {
286
+ if (seen.has(dir))
287
+ continue;
288
+ seen.add(dir);
289
+ const scoped = pickTestScript(dir, readFile, pm);
290
+ if (scoped)
291
+ commands.push(scoped);
292
+ }
293
+ if (commands.length > 0)
294
+ return commands;
295
+ const root = pickTestScript(cwd, readFile, pm);
296
+ return root ? [root] : [];
297
+ }
298
+ /**
299
+ * Infra/environment failure classification for the typecheck half (spec §3b,
300
+ * invariant §0.2). A fresh run worktree has no node_modules until the bootstrap
301
+ * finishes (or when it fail-opened), so a verify command can exit non-zero not
302
+ * because the CHANGE is broken but because the ENVIRONMENT is: `tsc: command
303
+ * not found` (exit 127), a Node loader `ERR_MODULE_NOT_FOUND`, or a spawn-shaped
304
+ * ENOENT surfaced through the runner's own output. Those must become an honest
305
+ * fail-open `verifyNote` ("verify could not run"), NEVER critical findings — a
306
+ * finding the fix stage can't clear would force round_cap on every run.
307
+ *
308
+ * Deliberately conservative (when uncertain, treat it as a REAL failure and let
309
+ * {@link parseVerifyFailures} report it): tsc's own `error TS2307: Cannot find
310
+ * module` is a legitimate compile diagnostic a change can cause, so a bare
311
+ * "Cannot find module" only classifies as infra when it carries the Node
312
+ * loader's `Require stack`/`ERR_MODULE_NOT_FOUND` signature, not the TS code.
313
+ * Returns a short human cause for the note, or null when it looks like a real
314
+ * verify failure.
315
+ */
316
+ export function classifyVerifyInfraFailure(output, exitCode) {
317
+ if (exitCode === 127)
318
+ return "command not found (exit 127; missing node_modules?)";
319
+ if (/command not found/i.test(output))
320
+ return "command not found (missing node_modules?)";
321
+ if (/not recognized as an internal or external command/i.test(output)) {
322
+ return "command not found (missing node_modules?)";
323
+ }
324
+ if (/\bERR_MODULE_NOT_FOUND\b/.test(output))
325
+ return "module resolution failed (ERR_MODULE_NOT_FOUND)";
326
+ if (/Cannot find module/.test(output) && /Require stack:/.test(output)) {
327
+ return "module resolution failed (Cannot find module)";
328
+ }
329
+ if (/\bspawn(?:Sync)?\b.*\bENOENT\b/.test(output))
330
+ return "spawn failed (ENOENT)";
331
+ return null;
332
+ }
333
+ /**
334
+ * Test-half infra/environment patterns (spec §3d): failure shapes that mean the
335
+ * ENVIRONMENT could not run the suite — a DB that is not up, a missing env var,
336
+ * auth against local infra, a connect timeout — not that the CHANGE broke a
337
+ * test. Each is deliberately specific (a bare "timed out" or "401" would eat
338
+ * real assertion failures); when none match, classification falls through to
339
+ * the failure parsers and, past those, to fail-open UNCERTAIN.
340
+ */
341
+ const TEST_INFRA_PATTERNS = [
342
+ [/\bECONNREFUSED\b|\bconnection refused\b/i, "connection refused (is the test's DB/service running?)"],
343
+ [/\bECONNRESET\b/, "connection reset by the test's DB/service"],
344
+ [/\bENOTFOUND\b|\bEAI_AGAIN\b/, "DNS/host resolution failed for a test dependency"],
345
+ [/\bEADDRINUSE\b/, "a port the tests need is already in use"],
346
+ [/\bETIMEDOUT\b|\bconnect(?:ion)? timed out\b/i, "connection to a test dependency timed out"],
347
+ [/\bmissing (?:required )?env(?:ironment)? (?:var(?:iable)?s?)\b/i, "missing environment variable"],
348
+ [/\benvironment variable\b[^\n]{0,80}\b(?:is )?(?:not set|missing|required|must be set)\b/i, "missing environment variable"],
349
+ [/\bprocess\.env\.[A-Z0-9_]+ is (?:undefined|not set)\b/, "missing environment variable"],
350
+ [/\bSASL\b|\bSCRAM-SERVER\b/, "database auth failed (missing DB credentials?)"],
351
+ [/\bpassword authentication failed\b/i, "database auth failed"],
352
+ [/\brole "[^"]*" does not exist\b/i, "database role does not exist"],
353
+ [/\bdatabase "[^"]*" does not exist\b/i, "database does not exist"],
354
+ [/\brelation "[^"]*" does not exist\b/i, "database schema missing (migrations not run?)"],
355
+ [/\bauthentication (?:failed|error)\b/i, "authentication against a test dependency failed"],
356
+ [/\b401 Unauthorized\b|\b403 Forbidden\b/i, "authorization against a test dependency failed"],
357
+ ];
358
+ /** Strip a trailing `(4 ms)` / `(0.82ms)` duration off a failing-test name. */
359
+ function stripDuration(name) {
360
+ return name.replace(/\s*\(\d+(?:\.\d+)?\s*m?s\)\s*$/, "").trim();
361
+ }
362
+ /** A failing-test bullet: jest `✕ name`, vitest `×/✗ name`, node spec `✖ name`. */
363
+ const FAIL_BULLET = /^[✕✖✗×]\s+(.+)$/;
364
+ /** node:test spec-reporter location line printed right above the footer's ✖ line. */
365
+ const NODE_TEST_AT = /^test at (\S+?):(\d+)(?::\d+)?$/;
366
+ /** TAP failing line (node:test tap reporter): `not ok 1 - name`. */
367
+ const TAP_NOT_OK = /^not ok\s+\d+\s+-\s+(.+)$/;
368
+ /** vitest per-test failure header: ` FAIL file > suite > name` (jest's has no `>`). */
369
+ const RUNNER_FAIL = /^FAIL\s+(\S+)(?:\s+>\s+(.+))?/;
370
+ /** Evidence that a test RUN happened and failed, even when no per-test line parsed. */
371
+ const FAILURE_SUMMARY = [
372
+ /(?:^|\n)\s*# fail\s+[1-9]\d*\b/, // node:test TAP footer
373
+ /\bTests:\s+(?:\d+\s+\w+,\s*)*[1-9]\d*\s+failed\b/, // jest: "Tests: 1 failed, 3 passed, 4 total"
374
+ /\bTests\s+[1-9]\d*\s+failed\b/, // vitest: "Tests 2 failed | 30 passed (32)"
375
+ /\b[1-9]\d*\s+failing\b/, // mocha / node:test "N failing"
376
+ /[✕✖✗×]\s+failing tests:/u, // node:test spec footer header
377
+ ];
378
+ /**
379
+ * PURE classification of one test-script run (spec §3d, invariant §0.2).
380
+ * Deliberately asymmetric with {@link parseVerifyFailures} (which always emits
381
+ * a finding on non-zero exit): a test suite fails for environmental reasons far
382
+ * more often than a typecheck does, and a finding the fix stage cannot clear
383
+ * forces round_cap on every run. So:
384
+ *
385
+ * - exit 0 → `clean`.
386
+ * - infra/env shapes (command not found, module resolution, connection
387
+ * refused, missing env var, DB/auth, connect timeout) → `infra` with a short
388
+ * cause; checked FIRST so a suite whose assertions failed only because its
389
+ * DB was down fails open rather than blocking.
390
+ * - recognisable failing-test lines (pnpm-wrapped vitest / jest / node:test
391
+ * TAP + spec reporters) → `failures` with located `critical` findings
392
+ * (capped at 25, deduped, locations attached when the runner printed them).
393
+ * - a failure summary with no parseable per-test lines → one capped tail
394
+ * finding (still real evidence tests ran and failed).
395
+ * - anything else → `uncertain`, and the CALLER FAILS OPEN (an honest note,
396
+ * never a guessed finding).
397
+ *
398
+ * Callers scrub the output through {@link scrubSecrets} BEFORE classification
399
+ * so findings/notes never carry secrets (§0.9).
400
+ */
401
+ export function classifyTestFailure(output, exitCode) {
402
+ if (exitCode === 0)
403
+ return { kind: "clean" };
404
+ const spawnInfra = classifyVerifyInfraFailure(output, exitCode);
405
+ if (spawnInfra)
406
+ return { kind: "infra", reason: spawnInfra };
407
+ for (const [re, reason] of TEST_INFRA_PATTERNS) {
408
+ if (re.test(output))
409
+ return { kind: "infra", reason };
410
+ }
411
+ // Per-test failure lines, deduped by test name; a later duplicate that DOES
412
+ // carry a location upgrades the located-less first sighting (node:test prints
413
+ // the body ✖ first and the located footer ✖ later). `pendingLocation` is the
414
+ // one-shot `test at file:line` node:test prints directly above a footer ✖;
415
+ // `currentFile` is jest's sticky `FAIL <file>` header covering its ✕ bullets.
416
+ const byMessage = new Map();
417
+ let pendingLocation;
418
+ let currentFile;
419
+ for (const raw of output.split("\n")) {
420
+ const line = raw.trim();
421
+ if (!line)
422
+ continue;
423
+ const at = line.match(NODE_TEST_AT);
424
+ if (at) {
425
+ pendingLocation = { file: at[1], line: Number(at[2]) };
426
+ continue;
427
+ }
428
+ let parsed;
429
+ const bullet = line.match(FAIL_BULLET) ?? line.match(TAP_NOT_OK);
430
+ if (bullet) {
431
+ const name = stripDuration(bullet[1]);
432
+ if (name && name.length <= 200 && !/^failing tests:?$/i.test(name)) {
433
+ parsed = { message: name };
434
+ if (pendingLocation) {
435
+ parsed.file = pendingLocation.file;
436
+ parsed.line = pendingLocation.line;
437
+ }
438
+ else if (currentFile) {
439
+ parsed.file = currentFile;
440
+ }
441
+ }
442
+ }
443
+ else {
444
+ const fail = line.match(RUNNER_FAIL);
445
+ if (fail && /\.[cm]?[jt]sx?$/.test(fail[1])) {
446
+ // vitest's `FAIL file > suite > name` is a test failure in itself; jest's
447
+ // bare `FAIL file` only names the file — its ✕ bullets that follow are
448
+ // located against it via currentFile.
449
+ if (fail[2])
450
+ parsed = { message: stripDuration(fail[2]), file: fail[1] };
451
+ else
452
+ currentFile = fail[1];
453
+ }
454
+ }
455
+ if (!parsed)
456
+ continue;
457
+ pendingLocation = undefined;
458
+ const existing = byMessage.get(parsed.message);
459
+ if (!existing)
460
+ byMessage.set(parsed.message, parsed);
461
+ else if (!existing.file && parsed.file)
462
+ byMessage.set(parsed.message, parsed);
463
+ }
464
+ const failures = [...byMessage.values()].slice(0, MAX_LOCATED_FINDINGS);
465
+ if (failures.length > 0) {
466
+ return {
467
+ kind: "failures",
468
+ findings: failures.map((f) => {
469
+ const finding = { severity: "critical", lens: VERIFY_LENS, message: `verify: test failed: ${f.message}` };
470
+ if (f.file)
471
+ finding.file = f.file;
472
+ if (f.line)
473
+ finding.line = f.line;
474
+ return finding;
475
+ }),
476
+ };
477
+ }
478
+ if (FAILURE_SUMMARY.some((re) => re.test(output))) {
479
+ const tail = output.length > MAX_TAIL_CHARS ? output.slice(output.length - MAX_TAIL_CHARS) : output;
480
+ return {
481
+ kind: "failures",
482
+ findings: [
483
+ {
484
+ severity: "critical",
485
+ lens: VERIFY_LENS,
486
+ message: `verify (test) failed (exit ${exitCode}). Output tail:\n${tail.trim()}`,
487
+ },
488
+ ],
489
+ };
490
+ }
491
+ return { kind: "uncertain" };
492
+ }
493
+ /** Match a TypeScript diagnostic line: `path(line,col): error TSxxxx: message`. */
494
+ const TS_DIAGNOSTIC = /^(.+?)\((\d+),\d+\):\s*error\s+TS\d+:\s*(.+)$/;
495
+ /** Match a generic `path:line:col: error message` (eslint/tsc-pretty-ish). */
496
+ const GENERIC_DIAGNOSTIC = /^(.+?):(\d+):\d+:?\s*(?:error\b[:\s]*)?(.+)$/;
497
+ /**
498
+ * Turn a failing verify run into `critical` Findings. A clean (exit 0) run yields
499
+ * none. Located diagnostics (TypeScript first, then a generic file:line form) are
500
+ * parsed into per-error findings; if none parse, a single capped finding carries
501
+ * the tail of the output so the fix stage still gets actionable signal. Never
502
+ * throws; always emits at least one finding on a non-zero exit.
503
+ */
504
+ export function parseVerifyFailures(output, exitCode, label) {
505
+ if (exitCode === 0)
506
+ return [];
507
+ const located = [];
508
+ const seen = new Set();
509
+ for (const raw of output.split("\n")) {
510
+ const line = raw.trim();
511
+ if (!line)
512
+ continue;
513
+ const m = line.match(TS_DIAGNOSTIC) ?? line.match(GENERIC_DIAGNOSTIC);
514
+ if (!m)
515
+ continue;
516
+ const file = m[1].trim();
517
+ const lineNo = Number(m[2]);
518
+ const message = m[3].trim();
519
+ // Guard the generic matcher against noise: require a plausible file token.
520
+ if (!file || /\s/.test(file) || !Number.isFinite(lineNo))
521
+ continue;
522
+ const key = `${file}:${lineNo}:${message}`;
523
+ if (seen.has(key))
524
+ continue;
525
+ seen.add(key);
526
+ located.push({ severity: "critical", lens: VERIFY_LENS, message: `verify: ${message}`, file, line: lineNo });
527
+ if (located.length >= MAX_LOCATED_FINDINGS)
528
+ break;
529
+ }
530
+ if (located.length > 0)
531
+ return located;
532
+ const tail = output.length > MAX_TAIL_CHARS ? output.slice(output.length - MAX_TAIL_CHARS) : output;
533
+ return [
534
+ {
535
+ severity: "critical",
536
+ lens: VERIFY_LENS,
537
+ message: `verify (${label}) failed (exit ${exitCode}). Output tail:\n${tail.trim()}`,
538
+ },
539
+ ];
540
+ }
541
+ const SAFE_ENV_KEYS = [
542
+ "PATH",
543
+ "HOME",
544
+ "USER",
545
+ "USERNAME",
546
+ "SHELL",
547
+ "TMPDIR",
548
+ "TMP",
549
+ "TEMP",
550
+ "SystemRoot",
551
+ "WINDIR",
552
+ "PNPM_HOME",
553
+ "COREPACK_HOME",
554
+ ];
555
+ /** Build the environment used for repo verify commands, intentionally excluding secrets and Node injection hooks. */
556
+ export function buildVerifyEnv(source = process.env) {
557
+ const env = { CI: "true" };
558
+ for (const key of SAFE_ENV_KEYS) {
559
+ const value = source[key];
560
+ if (value !== undefined)
561
+ env[key] = value;
562
+ }
563
+ return env;
564
+ }
565
+ /**
566
+ * The TEST-half environment (spec §3d): the user's FULL environment — tests
567
+ * legitimately need the local DB url, service creds, PATH extensions — MINUS
568
+ * every `YAGNI_*` var (repo-defined commands never inherit YAGNI tokens, §0.9)
569
+ * and MINUS NODE_OPTIONS (the same injection-hook stripping as the typecheck
570
+ * half). CI is forced on so a bare `vitest`/`jest --watch` test script runs
571
+ * once instead of parking in watch mode until the 10-min cap kills it.
572
+ */
573
+ export function buildTestEnv(source = process.env) {
574
+ const env = {};
575
+ for (const [key, value] of Object.entries(source)) {
576
+ if (value === undefined)
577
+ continue;
578
+ if (key.startsWith("YAGNI_"))
579
+ continue;
580
+ if (key === "NODE_OPTIONS")
581
+ continue;
582
+ env[key] = value;
583
+ }
584
+ env.CI = "true";
585
+ return env;
586
+ }
587
+ // `signal` is deliberately NOT passed into execFile: Node's built-in abort
588
+ // handling kills via the child's pid, and when the spawn itself failed (missing
589
+ // command, missing cwd) an abort landing in the same tick resolves that to pid 0
590
+ // — signalling the WHOLE process group and killing the parent. Abort is wired
591
+ // manually with an explicit pid guard; a killed child reaches the callback with
592
+ // a code-less error, which stays the same fail-open rejection as before.
593
+ const defaultExec = (argv, cwd, signal, env) => new Promise((resolve, reject) => {
594
+ if (signal.aborted)
595
+ return reject(new Error("verify aborted before start"));
596
+ const child = execFile(argv[0], argv.slice(1), { cwd, env: env ?? buildVerifyEnv(), maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
597
+ signal.removeEventListener("abort", onAbort);
598
+ const output = `${stdout ?? ""}${stderr ?? ""}`;
599
+ if (err) {
600
+ // A non-zero EXIT is a verify failure (carries a numeric code); a spawn
601
+ // error (ENOENT) or an abort has no numeric code and is a fail-open.
602
+ const code = err.code;
603
+ if (typeof code === "number")
604
+ return resolve({ code, output });
605
+ return reject(err);
606
+ }
607
+ resolve({ code: 0, output });
608
+ });
609
+ const onAbort = () => {
610
+ try {
611
+ if (child.pid)
612
+ child.kill("SIGTERM");
613
+ }
614
+ catch {
615
+ /* ignore */
616
+ }
617
+ };
618
+ signal.addEventListener("abort", onAbort, { once: true });
619
+ });
620
+ /**
621
+ * Build the runVerify function injected into the orchestrator. It discovers the
622
+ * changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
623
+ * build check for EACH, runs them (in their package dirs) under a composed (caller +
624
+ * timeout) abort signal, then runs each changed package's own `test` script (the
625
+ * §3d test half), and unions everything onto a single {@link VerifyOutcome}.
626
+ * Fail-open on every path it cannot turn into a clean verdict, so the gate never
627
+ * blocks a real change on its own failure.
628
+ */
629
+ export function makeRunVerify(opts = {}) {
630
+ const detect = opts.detect ??
631
+ ((cwd, changedDirs) => detectVerifyCommands(cwd, {
632
+ allowRepoOverride: process.env.YAGNI_CODE_ALLOW_VERIFY_OVERRIDE === "1",
633
+ changedDirs,
634
+ }));
635
+ const detectTests = opts.detectTests ?? ((cwd, changedDirs) => detectVerifyTestCommands(cwd, { changedDirs }));
636
+ const changedDirs = opts.changedDirs ??
637
+ (async (cwd, signal, baselinePaths) => {
638
+ const snapshot = await snapshotWorkspace(cwd, signal);
639
+ if (!snapshot.tracked)
640
+ return [];
641
+ // This run's OWN changed packages = the current diff minus the pre-run dirt, so
642
+ // a stray pre-existing file (or an unrelated dirty package) never pulls its dir
643
+ // into scope and the gate verifies what THIS run actually touched.
644
+ return ownChangedDirs(cwd, snapshot.status, baselinePaths ?? [], (p) => readFileSync(p, "utf8"));
645
+ });
646
+ const exec = opts.exec ?? defaultExec;
647
+ const timeoutMs = opts.timeoutMs ?? VERIFY_TIMEOUT_MS;
648
+ const testTimeoutMs = opts.testTimeoutMs ?? VERIFY_TEST_TIMEOUT_MS;
649
+ return async function runVerify(cwd, signal, baselinePaths) {
650
+ const dirs = await changedDirs(cwd, signal, baselinePaths);
651
+ const commands = detect(cwd, dirs);
652
+ // An explicitly-allowed .yagni-code/verify override IS the whole gate (spec
653
+ // §3d: it "covers the test half too"), so no detected test script runs
654
+ // alongside it — the repo already told us exactly how to verify.
655
+ const overridden = commands.some((c) => c.source === "override");
656
+ const testCommands = overridden ? [] : detectTests(cwd, dirs);
657
+ if (commands.length === 0 && testCommands.length === 0) {
658
+ return { ran: false, ok: true, reason: "verify skipped: no verify command detected", findings: [] };
659
+ }
660
+ const findings = [];
661
+ const reasons = [];
662
+ let anyRan = false;
663
+ // TYPECHECK HALF (frozen behavior, §0.2): run every changed package's build
664
+ // check; union the findings. One package that cannot spawn fails OPEN (a
665
+ // recorded reason, no finding) without sinking the verdict for the packages
666
+ // that did run. Output is scrubbed before it can enter a finding (§0.9).
667
+ const typecheckLabels = [];
668
+ for (const command of commands) {
669
+ const composed = composeAbortSignal(signal, AbortSignal.timeout(timeoutMs));
670
+ try {
671
+ const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed);
672
+ const output = scrubSecrets(rawOutput);
673
+ if (code !== 0) {
674
+ // Infra/env failure (command not found in a fresh worktree, Node loader
675
+ // resolution): the ENVIRONMENT could not run the check, so it is the same
676
+ // fail-open as a spawn error — an honest reason, never a critical finding
677
+ // the fix stage can't clear (invariant §0.2).
678
+ const infra = classifyVerifyInfraFailure(output, code);
679
+ if (infra) {
680
+ reasons.push(`verify (${command.label}) could not run: ${infra}`);
681
+ continue;
682
+ }
683
+ }
684
+ anyRan = true;
685
+ typecheckLabels.push(command.label);
686
+ if (code !== 0)
687
+ findings.push(...parseVerifyFailures(output, code, command.label));
688
+ }
689
+ catch (err) {
690
+ const msg = err instanceof Error ? err.message : String(err);
691
+ reasons.push(`verify (${command.label}) could not run: ${scrubSecrets(msg)}`);
692
+ }
693
+ }
694
+ // TEST HALF (spec §3d): each changed package's own `test` script, run AFTER
695
+ // the typecheck half under the user's env minus YAGNI_*/NODE_OPTIONS. When
696
+ // the typecheck half already produced findings the tests are skipped with an
697
+ // honest note — a tree that doesn't compile fails its suite for the same
698
+ // root cause, and the loop must fix the compile errors first anyway; the
699
+ // round where typecheck goes green runs the tests, so `clean` is never
700
+ // reached without the tests having run (or their absence honestly noted).
701
+ const testLabels = [];
702
+ if (testCommands.length > 0) {
703
+ if (findings.length > 0) {
704
+ reasons.push("tests skipped: the typecheck half failed; 'clean' still requires a later round's tests");
705
+ }
706
+ else {
707
+ const testEnv = opts.testEnv ?? buildTestEnv();
708
+ let testFindingCount = 0;
709
+ for (const command of testCommands) {
710
+ const where = basename(command.cwd) || command.cwd;
711
+ const composed = composeAbortSignal(signal, AbortSignal.timeout(testTimeoutMs));
712
+ try {
713
+ const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, testEnv);
714
+ const output = scrubSecrets(rawOutput);
715
+ const cls = classifyTestFailure(output, code);
716
+ if (cls.kind === "infra") {
717
+ reasons.push(`tests (${where}) could not run: ${cls.reason}; 'clean' reflects typecheck + review only`);
718
+ continue;
719
+ }
720
+ if (cls.kind === "uncertain") {
721
+ // Failing OPEN on an uninterpretable failure (§3d): an honest note,
722
+ // never a guessed finding the fix stage cannot clear.
723
+ reasons.push(`tests (${where}) exited ${code} but the output was not interpretable as test failures; failing open. 'clean' reflects typecheck + review only`);
724
+ continue;
725
+ }
726
+ anyRan = true;
727
+ testLabels.push(command.label);
728
+ if (cls.kind === "failures") {
729
+ const room = Math.max(0, MAX_LOCATED_FINDINGS - testFindingCount);
730
+ const take = cls.findings.slice(0, room);
731
+ testFindingCount += take.length;
732
+ findings.push(...take);
733
+ }
734
+ }
735
+ catch (err) {
736
+ const msg = err instanceof Error ? err.message : String(err);
737
+ reasons.push(`tests (${where}) could not run: ${scrubSecrets(msg)}`);
738
+ }
739
+ }
740
+ }
741
+ }
742
+ // The command labels list only what produced a verdict — on BOTH the
743
+ // typecheck and test halves — so the FINISH trailer's `passed (<cmds>)`
744
+ // never claims a check that was infra-skipped or threw (invariant §0.1).
745
+ const command = [...new Set(typecheckLabels), ...new Set(testLabels)].join(", ");
746
+ if (!anyRan) {
747
+ return { ran: false, ok: true, command, reason: reasons.join("; ") || "verify skipped", findings: [] };
748
+ }
749
+ return {
750
+ ran: true,
751
+ ok: findings.length === 0,
752
+ command,
753
+ findings,
754
+ ...(reasons.length ? { reason: reasons.join("; ") } : {}),
755
+ };
756
+ };
757
+ }
758
+ //# sourceMappingURL=verify.js.map