truecourse 0.6.1 → 0.6.6-next.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 (4) hide show
  1. package/README.md +3 -1
  2. package/cli.mjs +1599 -920
  3. package/package.json +1 -1
  4. package/server.mjs +3088 -2507
package/cli.mjs CHANGED
@@ -973,7 +973,7 @@ var require_command = __commonJS({
973
973
  var EventEmitter2 = __require("node:events").EventEmitter;
974
974
  var childProcess = __require("node:child_process");
975
975
  var path60 = __require("node:path");
976
- var fs50 = __require("node:fs");
976
+ var fs51 = __require("node:fs");
977
977
  var process2 = __require("node:process");
978
978
  var { Argument: Argument2, humanReadableArgName } = require_argument();
979
979
  var { CommanderError: CommanderError2 } = require_error();
@@ -1906,10 +1906,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1906
1906
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1907
1907
  function findFile(baseDir, baseName) {
1908
1908
  const localBin = path60.resolve(baseDir, baseName);
1909
- if (fs50.existsSync(localBin)) return localBin;
1909
+ if (fs51.existsSync(localBin)) return localBin;
1910
1910
  if (sourceExt.includes(path60.extname(baseName))) return void 0;
1911
1911
  const foundExt = sourceExt.find(
1912
- (ext2) => fs50.existsSync(`${localBin}${ext2}`)
1912
+ (ext2) => fs51.existsSync(`${localBin}${ext2}`)
1913
1913
  );
1914
1914
  if (foundExt) return `${localBin}${foundExt}`;
1915
1915
  return void 0;
@@ -1921,7 +1921,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1921
1921
  if (this._scriptPath) {
1922
1922
  let resolvedScriptPath;
1923
1923
  try {
1924
- resolvedScriptPath = fs50.realpathSync(this._scriptPath);
1924
+ resolvedScriptPath = fs51.realpathSync(this._scriptPath);
1925
1925
  } catch (err) {
1926
1926
  resolvedScriptPath = this._scriptPath;
1927
1927
  }
@@ -3492,7 +3492,9 @@ var require_src = __commonJS({
3492
3492
 
3493
3493
  // node_modules/.pnpm/@clack+core@1.2.0/node_modules/@clack/core/dist/index.mjs
3494
3494
  import { stdout as S, stdin as $ } from "node:process";
3495
+ import * as _ from "node:readline";
3495
3496
  import P from "node:readline";
3497
+ import { ReadStream as D } from "node:tty";
3496
3498
  function d(r, t2, e) {
3497
3499
  if (!e.some((o) => !o.disabled)) return r;
3498
3500
  const s = r + t2, i = Math.max(e.length - 1, 0), n = s < 0 ? i : s > i ? 0 : s;
@@ -3518,6 +3520,27 @@ function w(r, t2) {
3518
3520
  const e = r;
3519
3521
  e.isTTY && e.setRawMode(t2);
3520
3522
  }
3523
+ function z({ input: r = $, output: t2 = S, overwrite: e = true, hideCursor: s = true } = {}) {
3524
+ const i = _.createInterface({ input: r, output: t2, prompt: "", tabSize: 1 });
3525
+ _.emitKeypressEvents(r, i), r instanceof D && r.isTTY && r.setRawMode(true);
3526
+ const n = (o, { name: a, sequence: h }) => {
3527
+ const l = String(o);
3528
+ if (V([l, a, h], "cancel")) {
3529
+ s && t2.write(import_sisteransi.cursor.show), process.exit(0);
3530
+ return;
3531
+ }
3532
+ if (!e) return;
3533
+ const f2 = a === "return" ? 0 : -1, v = a === "return" ? -1 : 0;
3534
+ _.moveCursor(t2, f2, v, () => {
3535
+ _.clearLine(t2, 1, () => {
3536
+ r.once("keypress", n);
3537
+ });
3538
+ });
3539
+ };
3540
+ return s && t2.write(import_sisteransi.cursor.hide), r.once("keypress", n), () => {
3541
+ r.off("keypress", n), s && t2.write(import_sisteransi.cursor.show), r instanceof D && r.isTTY && !Y && r.setRawMode(false), i.terminal = false, i.close();
3542
+ };
3543
+ }
3521
3544
  function R(r, t2, e, s = e) {
3522
3545
  const i = O(r ?? S);
3523
3546
  return wrapAnsi(t2, i - e.length, { hard: true, trim: false }).split(`
@@ -3711,7 +3734,7 @@ import P2 from "node:process";
3711
3734
  function Ze() {
3712
3735
  return P2.platform !== "win32" ? P2.env.TERM !== "linux" : !!P2.env.CI || !!P2.env.WT_SESSION || !!P2.env.TERMINUS_SUBLIME || P2.env.ConEmuTask === "{cmd::Cmder}" || P2.env.TERM_PROGRAM === "Terminus-Sublime" || P2.env.TERM_PROGRAM === "vscode" || P2.env.TERM === "xterm-256color" || P2.env.TERM === "alacritty" || P2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
3713
3736
  }
3714
- var import_sisteransi2, ee, w2, _e, oe, ue, F, le, d2, E2, Ie, Ee, z2, H2, te, U, J, xe, se, ce, Ge, $e, de, Oe, he, pe, me, ge, V2, ye, et2, Y2, ot2, O2, pt, mt, gt, Ve, re, _t, je;
3737
+ var import_sisteransi2, ee, ae, w2, _e, oe, ue, F, le, d2, E2, Ie, Ee, z2, H2, te, U, J, xe, se, ce, Ge, $e, de, Oe, he, pe, me, ge, V2, ye, et2, Y2, ot2, O2, pt, mt, gt, Ct, fe, Ve, re, _t, je;
3715
3738
  var init_dist4 = __esm({
3716
3739
  "node_modules/.pnpm/@clack+prompts@1.2.0/node_modules/@clack/prompts/dist/index.mjs"() {
3717
3740
  init_dist3();
@@ -3720,6 +3743,7 @@ var init_dist4 = __esm({
3720
3743
  init_dist2();
3721
3744
  import_sisteransi2 = __toESM(require_src(), 1);
3722
3745
  ee = Ze();
3746
+ ae = () => process.env.CI === "true";
3723
3747
  w2 = (e, i) => ee ? e : i;
3724
3748
  _e = w2("\u25C6", "*");
3725
3749
  oe = w2("\u25A0", "x");
@@ -3796,7 +3820,7 @@ var init_dist4 = __esm({
3796
3820
  }
3797
3821
  if (f2 > $2) {
3798
3822
  let b = 0, x = 0, G2 = f2;
3799
- const M2 = e - v, R2 = (j2, D) => et2(h, G2, j2, D, $2);
3823
+ const M2 = e - v, R2 = (j2, D2) => et2(h, G2, j2, D2, $2);
3800
3824
  m ? ({ lineCount: G2, removals: b } = R2(0, M2), G2 > $2 && ({ lineCount: G2, removals: x } = R2(M2 + 1, h.length))) : ({ lineCount: G2, removals: x } = R2(M2 + 1, h.length), G2 > $2 && ({ lineCount: G2, removals: b } = R2(0, M2))), b > 0 && (m = true, h.splice(0, b)), x > 0 && (g = true, h.splice(h.length - x, x));
3801
3825
  }
3802
3826
  const C3 = [];
@@ -3875,6 +3899,59 @@ ${t("gray", E2)} ` : "";
3875
3899
 
3876
3900
  `);
3877
3901
  };
3902
+ Ct = (e) => t("magenta", e);
3903
+ fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: o = ee ? 80 : 120, signal: c2, ...a } = {}) => {
3904
+ const l = ae();
3905
+ let $2, y, p2 = false, m = false, g = "", S2, h = performance.now();
3906
+ const f2 = O(s), v = a?.styleFrame ?? Ct, T = (_2) => {
3907
+ const A2 = _2 > 1 ? u2 ?? u.messages.error : r ?? u.messages.cancel;
3908
+ m = _2 === 1, p2 && (W(A2, _2), m && typeof i == "function" && i());
3909
+ }, C3 = () => T(2), b = () => T(1), x = () => {
3910
+ process.on("uncaughtExceptionMonitor", C3), process.on("unhandledRejection", C3), process.on("SIGINT", b), process.on("SIGTERM", b), process.on("exit", T), c2 && c2.addEventListener("abort", b);
3911
+ }, G2 = () => {
3912
+ process.removeListener("uncaughtExceptionMonitor", C3), process.removeListener("unhandledRejection", C3), process.removeListener("SIGINT", b), process.removeListener("SIGTERM", b), process.removeListener("exit", T), c2 && c2.removeEventListener("abort", b);
3913
+ }, M2 = () => {
3914
+ if (S2 === void 0) return;
3915
+ l && s.write(`
3916
+ `);
3917
+ const _2 = wrapAnsi(S2, f2, { hard: true, trim: false }).split(`
3918
+ `);
3919
+ _2.length > 1 && s.write(import_sisteransi2.cursor.up(_2.length - 1)), s.write(import_sisteransi2.cursor.to(0)), s.write(import_sisteransi2.erase.down());
3920
+ }, R2 = (_2) => _2.replace(/\.+$/, ""), j2 = (_2) => {
3921
+ const A2 = (performance.now() - _2) / 1e3, k = Math.floor(A2 / 60), L = Math.floor(A2 % 60);
3922
+ return k > 0 ? `[${k}m ${L}s]` : `[${L}s]`;
3923
+ }, D2 = a.withGuide ?? u.withGuide, ie = (_2 = "") => {
3924
+ p2 = true, $2 = z({ output: s }), g = R2(_2), h = performance.now(), D2 && s.write(`${t("gray", d2)}
3925
+ `);
3926
+ let A2 = 0, k = 0;
3927
+ x(), y = setInterval(() => {
3928
+ if (l && g === S2) return;
3929
+ M2(), S2 = g;
3930
+ const L = v(n[A2]);
3931
+ let Z;
3932
+ if (l) Z = `${L} ${g}...`;
3933
+ else if (e === "timer") Z = `${L} ${g} ${j2(h)}`;
3934
+ else {
3935
+ const Be = ".".repeat(Math.floor(k)).slice(0, 3);
3936
+ Z = `${L} ${g}${Be}`;
3937
+ }
3938
+ const Ne = wrapAnsi(Z, f2, { hard: true, trim: false });
3939
+ s.write(Ne), A2 = A2 + 1 < n.length ? A2 + 1 : 0, k = k < 4 ? k + 0.125 : 0;
3940
+ }, o);
3941
+ }, W = (_2 = "", A2 = 0, k = false) => {
3942
+ if (!p2) return;
3943
+ p2 = false, clearInterval(y), M2();
3944
+ const L = A2 === 0 ? t("green", F) : A2 === 1 ? t("red", oe) : t("red", ue);
3945
+ g = _2 ?? g, k || (e === "timer" ? s.write(`${L} ${g} ${j2(h)}
3946
+ `) : s.write(`${L} ${g}
3947
+ `)), G2(), $2();
3948
+ };
3949
+ return { start: ie, stop: (_2 = "") => W(_2, 0), message: (_2 = "") => {
3950
+ g = R2(_2 ?? g);
3951
+ }, cancel: (_2 = "") => W(_2, 1), error: (_2 = "") => W(_2, 2), clear: () => W("", 0, true), get isCancelled() {
3952
+ return m;
3953
+ } };
3954
+ };
3878
3955
  Ve = { light: w2("\u2500", "-"), heavy: w2("\u2501", "="), block: w2("\u2588", "#") };
3879
3956
  re = (e, i) => e.includes(`
3880
3957
  `) ? e.split(`
@@ -3976,7 +4053,20 @@ var init_paths = __esm({
3976
4053
  "packages/core/dist/config/paths.js"() {
3977
4054
  "use strict";
3978
4055
  TRUECOURSE_DIR = ".truecourse";
3979
- GITIGNORE_CONTENTS = "analyses/\nhistory.json\ndiff.json\nui-state.json\nlogs/\n.analyze.lock\n";
4056
+ GITIGNORE_CONTENTS = [
4057
+ "analyses/",
4058
+ "history.json",
4059
+ "diff.json",
4060
+ "ui-state.json",
4061
+ "logs/",
4062
+ ".analyze.lock",
4063
+ "contracts/",
4064
+ "verifier/runs/",
4065
+ "verifier/history.json",
4066
+ "verifier/diff.json",
4067
+ ".cache/",
4068
+ ""
4069
+ ].join("\n");
3980
4070
  }
3981
4071
  });
3982
4072
 
@@ -4566,6 +4656,16 @@ var init_helpers = __esm({
4566
4656
  }
4567
4657
  });
4568
4658
 
4659
+ // packages/shared/dist/claude-binary.js
4660
+ function resolveClaudeBinary() {
4661
+ return process.env.CLAUDE_CODE_BINARY || process.env.CLAUDE_CODE_BIN || "claude";
4662
+ }
4663
+ var init_claude_binary = __esm({
4664
+ "packages/shared/dist/claude-binary.js"() {
4665
+ "use strict";
4666
+ }
4667
+ });
4668
+
4569
4669
  // packages/core/dist/lib/atomic-write.js
4570
4670
  import fs5 from "node:fs";
4571
4671
  import path5 from "node:path";
@@ -5411,7 +5511,7 @@ var require_node = __commonJS({
5411
5511
  exports.inspectOpts = Object.keys(process.env).filter((key2) => {
5412
5512
  return /^debug_/i.test(key2);
5413
5513
  }).reduce((obj, key2) => {
5414
- const prop = key2.substring(6).toLowerCase().replace(/_([a-z])/g, (_, k) => {
5514
+ const prop = key2.substring(6).toLowerCase().replace(/_([a-z])/g, (_2, k) => {
5415
5515
  return k.toUpperCase();
5416
5516
  });
5417
5517
  let val = process.env[key2];
@@ -6887,7 +6987,7 @@ function deleteBranchTask(branch, forceDelete = false) {
6887
6987
  parser(stdOut, stdErr) {
6888
6988
  return parseBranchDeletions(stdOut, stdErr).branches[branch];
6889
6989
  },
6890
- onError({ exitCode, stdErr, stdOut }, error, _, fail4) {
6990
+ onError({ exitCode, stdErr, stdOut }, error, _2, fail4) {
6891
6991
  if (!hasBranchDeletionError(String(error), exitCode)) {
6892
6992
  return fail4(error);
6893
6993
  }
@@ -10066,6 +10166,7 @@ var init_errors = __esm({
10066
10166
  });
10067
10167
 
10068
10168
  // packages/core/dist/lib/git.js
10169
+ import fs8 from "node:fs";
10069
10170
  async function isGitRepo(repoPath) {
10070
10171
  try {
10071
10172
  return await simpleGit(repoPath).checkIsRepo();
@@ -10081,31 +10182,74 @@ async function getGit(repoPath) {
10081
10182
  }
10082
10183
  return git;
10083
10184
  }
10185
+ async function runWithStash(repoRoot6, options, fn) {
10186
+ let didStash = false;
10187
+ let stashGit;
10188
+ if (!options.skipStash) {
10189
+ try {
10190
+ stashGit = await getGit(repoRoot6);
10191
+ const status = await stashGit.status();
10192
+ if (!status.isClean()) {
10193
+ const gitRoot = (await stashGit.revparse(["--show-toplevel"])).trim();
10194
+ if (fs8.realpathSync(repoRoot6) === fs8.realpathSync(gitRoot)) {
10195
+ options.onStashStart?.();
10196
+ const res = await stashGit.stash([
10197
+ "push",
10198
+ "--include-untracked",
10199
+ "-m",
10200
+ options.message,
10201
+ // Stash everything under the repo root EXCEPT `.truecourse/`, so the
10202
+ // tool's own working dir (contracts, specs, baselines) survives.
10203
+ "--",
10204
+ ".",
10205
+ `:(exclude,top)${TRUECOURSE_DIR}`
10206
+ ]);
10207
+ didStash = !res.includes("No local changes");
10208
+ }
10209
+ }
10210
+ } catch (error) {
10211
+ options.onStashError?.(error instanceof Error ? error : new Error(String(error)));
10212
+ }
10213
+ }
10214
+ try {
10215
+ return await fn();
10216
+ } finally {
10217
+ if (didStash && stashGit) {
10218
+ options.onRestoreStart?.();
10219
+ try {
10220
+ await stashGit.stash(["pop"]);
10221
+ } catch (error) {
10222
+ options.onRestoreError?.(error instanceof Error ? error : new Error(String(error)));
10223
+ }
10224
+ }
10225
+ }
10226
+ }
10084
10227
  var NOT_A_GIT_REPO_MESSAGE;
10085
10228
  var init_git = __esm({
10086
10229
  "packages/core/dist/lib/git.js"() {
10087
10230
  "use strict";
10088
10231
  init_esm();
10089
10232
  init_errors();
10233
+ init_paths();
10090
10234
  NOT_A_GIT_REPO_MESSAGE = "The selected folder is not a git repository. Please select a folder that has been initialized with git.";
10091
10235
  }
10092
10236
  });
10093
10237
 
10094
10238
  // packages/core/dist/config/project-config.js
10095
- import fs8 from "node:fs";
10239
+ import fs9 from "node:fs";
10096
10240
  function readProjectConfig(repoDir) {
10097
10241
  const file = getRepoConfigPath(repoDir);
10098
- if (!fs8.existsSync(file))
10242
+ if (!fs9.existsSync(file))
10099
10243
  return { ...EMPTY };
10100
10244
  try {
10101
- return JSON.parse(fs8.readFileSync(file, "utf-8"));
10245
+ return JSON.parse(fs9.readFileSync(file, "utf-8"));
10102
10246
  } catch {
10103
10247
  return { ...EMPTY };
10104
10248
  }
10105
10249
  }
10106
10250
  function writeProjectConfig(repoDir, config2) {
10107
10251
  ensureRepoTruecourseDir(repoDir);
10108
- fs8.writeFileSync(getRepoConfigPath(repoDir), JSON.stringify(config2, null, 2), "utf-8");
10252
+ fs9.writeFileSync(getRepoConfigPath(repoDir), JSON.stringify(config2, null, 2), "utf-8");
10109
10253
  }
10110
10254
  function updateProjectConfig(repoDir, patch) {
10111
10255
  const current = readProjectConfig(repoDir);
@@ -10174,7 +10318,7 @@ var require_ignore = __commonJS({
10174
10318
  // (a ) -> (a)
10175
10319
  // (a \ ) -> (a )
10176
10320
  /((?:\\\\)*?)(\\?\s+)$/,
10177
- (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY2)
10321
+ (_2, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY2)
10178
10322
  ],
10179
10323
  // Replace (\ ) with ' '
10180
10324
  // (\ ) -> ' '
@@ -10182,7 +10326,7 @@ var require_ignore = __commonJS({
10182
10326
  // (\\\ ) -> '\\ '
10183
10327
  [
10184
10328
  /(\\+?)\s/g,
10185
- (_, m1) => {
10329
+ (_2, m1) => {
10186
10330
  const { length } = m1;
10187
10331
  return m1.slice(0, length - length % 2) + SPACE;
10188
10332
  }
@@ -10253,7 +10397,7 @@ var require_ignore = __commonJS({
10253
10397
  // Zero, one or several directories
10254
10398
  // should not use '*', or it will be replaced by the next replacer
10255
10399
  // Check if it is not the last `'/**'`
10256
- (_, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
10400
+ (_2, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
10257
10401
  ],
10258
10402
  // normal intermediate wildcards
10259
10403
  [
@@ -10265,7 +10409,7 @@ var require_ignore = __commonJS({
10265
10409
  /(^|[^\\]+)(\\\*)+(?=.+)/g,
10266
10410
  // '*.js' matches '.js'
10267
10411
  // '*.js' doesn't match 'abc'
10268
- (_, p1, p2) => {
10412
+ (_2, p1, p2) => {
10269
10413
  const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
10270
10414
  return p1 + unescaped;
10271
10415
  }
@@ -10312,11 +10456,11 @@ var require_ignore = __commonJS({
10312
10456
  var MODE_CHECK_IGNORE = "checkRegex";
10313
10457
  var UNDERSCORE = "_";
10314
10458
  var TRAILING_WILD_CARD_REPLACERS = {
10315
- [MODE_IGNORE](_, p1) {
10459
+ [MODE_IGNORE](_2, p1) {
10316
10460
  const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
10317
10461
  return `${prefix}(?=$|\\/$)`;
10318
10462
  },
10319
- [MODE_CHECK_IGNORE](_, p1) {
10463
+ [MODE_CHECK_IGNORE](_2, p1) {
10320
10464
  const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
10321
10465
  return `${prefix}(?=$|\\/$)`;
10322
10466
  }
@@ -10999,10 +11143,10 @@ import { Parser, Language } from "web-tree-sitter";
10999
11143
  import { createRequire as _createRequire } from "node:module";
11000
11144
  import { fileURLToPath as fileURLToPath2 } from "node:url";
11001
11145
  import path8 from "node:path";
11002
- import fs9 from "node:fs";
11146
+ import fs10 from "node:fs";
11003
11147
  function resolveWasmPath(subpath) {
11004
11148
  const bundled = path8.join(BUNDLED_WASM_DIR, path8.basename(subpath));
11005
- if (fs9.existsSync(bundled))
11149
+ if (fs10.existsSync(bundled))
11006
11150
  return bundled;
11007
11151
  return _require.resolve(subpath);
11008
11152
  }
@@ -15507,7 +15651,7 @@ var init_ast = __esm({
15507
15651
  if (!isExtglobAST(this)) {
15508
15652
  const noEmpty = this.isStart() && this.isEnd() && !this.#parts.some((s) => typeof s !== "string");
15509
15653
  const src = this.#parts.map((p2) => {
15510
- const [re2, _, hasMagic, uflag] = typeof p2 === "string" ? _a2.#parseGlob(p2, this.#hasMagic, noEmpty) : p2.toRegExpSource(allowDot);
15654
+ const [re2, _2, hasMagic, uflag] = typeof p2 === "string" ? _a2.#parseGlob(p2, this.#hasMagic, noEmpty) : p2.toRegExpSource(allowDot);
15511
15655
  this.#hasMagic = this.#hasMagic || hasMagic;
15512
15656
  this.#uflag = this.#uflag || uflag;
15513
15657
  return re2;
@@ -15613,7 +15757,7 @@ var init_ast = __esm({
15613
15757
  if (typeof p2 === "string") {
15614
15758
  throw new Error("string type in extglob ast??");
15615
15759
  }
15616
- const [re2, _, _hasMagic, uflag] = p2.toRegExpSource(dot);
15760
+ const [re2, _2, _hasMagic, uflag] = p2.toRegExpSource(dot);
15617
15761
  this.#uflag = this.#uflag || uflag;
15618
15762
  return re2;
15619
15763
  }).filter((p2) => !(this.isStart() && this.isEnd()) || !!p2).join("|");
@@ -15886,7 +16030,7 @@ var init_esm4 = __esm({
15886
16030
  }
15887
16031
  return false;
15888
16032
  }
15889
- debug(..._) {
16033
+ debug(..._2) {
15890
16034
  }
15891
16035
  make() {
15892
16036
  const pattern = this.pattern;
@@ -15908,7 +16052,7 @@ var init_esm4 = __esm({
15908
16052
  const rawGlobParts = this.globSet.map((s) => this.slashSplit(s));
15909
16053
  this.globParts = this.preprocess(rawGlobParts);
15910
16054
  this.debug(this.pattern, this.globParts);
15911
- let set2 = this.globParts.map((s, _, __) => {
16055
+ let set2 = this.globParts.map((s, _2, __) => {
15912
16056
  if (this.isWindows && this.windowsNoMagicRoot) {
15913
16057
  const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]);
15914
16058
  const isDrive = /^[a-z]:/i.test(s[0]);
@@ -61925,7 +62069,7 @@ function pythonRegexToJs(pattern) {
61925
62069
  collectFlag(f2);
61926
62070
  pattern = pattern.slice(prefix[0].length);
61927
62071
  }
61928
- pattern = pattern.replace(/\(\?([aiLmsux]+):/g, (_, flagSet) => {
62072
+ pattern = pattern.replace(/\(\?([aiLmsux]+):/g, (_2, flagSet) => {
61929
62073
  for (const f2 of flagSet)
61930
62074
  collectFlag(f2);
61931
62075
  return "(?:";
@@ -74612,8 +74756,8 @@ var init_test_empty_file = __esm({
74612
74756
  nodeTypes: ["program"],
74613
74757
  visit(node2, filePath, sourceCode) {
74614
74758
  const lowerPath = filePath.toLowerCase();
74615
- const isTestFile2 = lowerPath.includes(".test.") || lowerPath.includes(".spec.") || lowerPath.includes("__tests__");
74616
- if (!isTestFile2)
74759
+ const isTestFile3 = lowerPath.includes(".test.") || lowerPath.includes(".spec.") || lowerPath.includes("__tests__");
74760
+ if (!isTestFile3)
74617
74761
  return null;
74618
74762
  if (hasTestFunction(node2))
74619
74763
  return null;
@@ -88426,8 +88570,8 @@ var init_test_not_discoverable = __esm({
88426
88570
  return null;
88427
88571
  const className = classNameNode.text;
88428
88572
  const isTestClass = className.startsWith("Test") || className.endsWith("Test") || className.endsWith("Tests");
88429
- const isTestFile2 = isPythonTestFile(filePath);
88430
- if (!isTestClass && !isTestFile2)
88573
+ const isTestFile3 = isPythonTestFile(filePath);
88574
+ if (!isTestClass && !isTestFile3)
88431
88575
  return null;
88432
88576
  const body = node2.childForFieldName("body");
88433
88577
  if (!body)
@@ -90915,7 +91059,7 @@ var init_js_naming_convention = __esm({
90915
91059
  return null;
90916
91060
  }
90917
91061
  if (funcName.includes("_") && !funcName.startsWith("_")) {
90918
- return makeViolation(this.ruleKey, node2, filePath, "low", "Function uses snake_case naming", `Function '${funcName}' uses snake_case. JavaScript convention is camelCase.`, sourceCode, `Rename to camelCase: ${funcName.replace(/_([a-z])/g, (_, c2) => c2.toUpperCase())}.`);
91062
+ return makeViolation(this.ruleKey, node2, filePath, "low", "Function uses snake_case naming", `Function '${funcName}' uses snake_case. JavaScript convention is camelCase.`, sourceCode, `Rename to camelCase: ${funcName.replace(/_([a-z])/g, (_2, c2) => c2.toUpperCase())}.`);
90919
91063
  }
90920
91064
  return null;
90921
91065
  }
@@ -99324,7 +99468,7 @@ var util, objectUtil, ZodParsedType, getParsedType;
99324
99468
  var init_util3 = __esm({
99325
99469
  "node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js"() {
99326
99470
  (function(util2) {
99327
- util2.assertEqual = (_) => {
99471
+ util2.assertEqual = (_2) => {
99328
99472
  };
99329
99473
  function assertIs(_arg) {
99330
99474
  }
@@ -99374,7 +99518,7 @@ var init_util3 = __esm({
99374
99518
  return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
99375
99519
  }
99376
99520
  util2.joinValues = joinValues;
99377
- util2.jsonStringifyReplacer = (_, value) => {
99521
+ util2.jsonStringifyReplacer = (_2, value) => {
99378
99522
  if (typeof value === "bigint") {
99379
99523
  return value.toString();
99380
99524
  }
@@ -104025,13 +104169,13 @@ var init_schemas = __esm({
104025
104169
  });
104026
104170
 
104027
104171
  // packages/shared/dist/fs/tcignore.js
104028
- import fs10 from "node:fs";
104172
+ import fs11 from "node:fs";
104029
104173
  import path12 from "node:path";
104030
104174
  function findRepoRoot(startDir) {
104031
104175
  const start = path12.resolve(startDir);
104032
104176
  let dir = start;
104033
104177
  for (; ; ) {
104034
- if (ROOT_MARKERS.some((m) => fs10.existsSync(path12.join(dir, m))))
104178
+ if (ROOT_MARKERS.some((m) => fs11.existsSync(path12.join(dir, m))))
104035
104179
  return dir;
104036
104180
  const parent = path12.dirname(dir);
104037
104181
  if (parent === dir)
@@ -104043,7 +104187,7 @@ function loadTcIgnore(startDir) {
104043
104187
  const root = findRepoRoot(startDir);
104044
104188
  const ig = (0, import_ignore2.default)();
104045
104189
  try {
104046
- ig.add(fs10.readFileSync(path12.join(root, ".truecourseignore"), "utf8"));
104190
+ ig.add(fs11.readFileSync(path12.join(root, ".truecourseignore"), "utf8"));
104047
104191
  } catch {
104048
104192
  }
104049
104193
  return {
@@ -104072,6 +104216,7 @@ var init_dist7 = __esm({
104072
104216
  init_types3();
104073
104217
  init_schemas();
104074
104218
  init_tcignore();
104219
+ init_claude_binary();
104075
104220
  }
104076
104221
  });
104077
104222
 
@@ -104200,7 +104345,7 @@ var require_windows = __commonJS({
104200
104345
  "node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/windows.js"(exports, module) {
104201
104346
  module.exports = isexe;
104202
104347
  isexe.sync = sync;
104203
- var fs50 = __require("fs");
104348
+ var fs51 = __require("fs");
104204
104349
  function checkPathExt(path60, options) {
104205
104350
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
104206
104351
  if (!pathext) {
@@ -104225,12 +104370,12 @@ var require_windows = __commonJS({
104225
104370
  return checkPathExt(path60, options);
104226
104371
  }
104227
104372
  function isexe(path60, options, cb) {
104228
- fs50.stat(path60, function(er, stat) {
104373
+ fs51.stat(path60, function(er, stat) {
104229
104374
  cb(er, er ? false : checkStat(stat, path60, options));
104230
104375
  });
104231
104376
  }
104232
104377
  function sync(path60, options) {
104233
- return checkStat(fs50.statSync(path60), path60, options);
104378
+ return checkStat(fs51.statSync(path60), path60, options);
104234
104379
  }
104235
104380
  }
104236
104381
  });
@@ -104240,14 +104385,14 @@ var require_mode = __commonJS({
104240
104385
  "node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/mode.js"(exports, module) {
104241
104386
  module.exports = isexe;
104242
104387
  isexe.sync = sync;
104243
- var fs50 = __require("fs");
104388
+ var fs51 = __require("fs");
104244
104389
  function isexe(path60, options, cb) {
104245
- fs50.stat(path60, function(er, stat) {
104390
+ fs51.stat(path60, function(er, stat) {
104246
104391
  cb(er, er ? false : checkStat(stat, options));
104247
104392
  });
104248
104393
  }
104249
104394
  function sync(path60, options) {
104250
- return checkStat(fs50.statSync(path60), options);
104395
+ return checkStat(fs51.statSync(path60), options);
104251
104396
  }
104252
104397
  function checkStat(stat, options) {
104253
104398
  return stat.isFile() && checkMode(stat, options);
@@ -104271,7 +104416,7 @@ var require_mode = __commonJS({
104271
104416
  // node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/index.js
104272
104417
  var require_isexe = __commonJS({
104273
104418
  "node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/index.js"(exports, module) {
104274
- var fs50 = __require("fs");
104419
+ var fs51 = __require("fs");
104275
104420
  var core2;
104276
104421
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
104277
104422
  core2 = require_windows();
@@ -104535,16 +104680,16 @@ var require_shebang_command = __commonJS({
104535
104680
  var require_readShebang = __commonJS({
104536
104681
  "node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
104537
104682
  "use strict";
104538
- var fs50 = __require("fs");
104683
+ var fs51 = __require("fs");
104539
104684
  var shebangCommand = require_shebang_command();
104540
104685
  function readShebang(command) {
104541
104686
  const size = 150;
104542
104687
  const buffer = Buffer.alloc(size);
104543
104688
  let fd;
104544
104689
  try {
104545
- fd = fs50.openSync(command, "r");
104546
- fs50.readSync(fd, buffer, 0, size, 0);
104547
- fs50.closeSync(fd);
104690
+ fd = fs51.openSync(command, "r");
104691
+ fs51.readSync(fd, buffer, 0, size, 0);
104692
+ fs51.closeSync(fd);
104548
104693
  } catch (e) {
104549
104694
  }
104550
104695
  return shebangCommand(buffer.toString());
@@ -104672,7 +104817,7 @@ var require_cross_spawn = __commonJS({
104672
104817
  var cp = __require("child_process");
104673
104818
  var parse2 = require_parse();
104674
104819
  var enoent = require_enoent();
104675
- function spawn6(command, args, options) {
104820
+ function spawn7(command, args, options) {
104676
104821
  const parsed = parse2(command, args, options);
104677
104822
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
104678
104823
  enoent.hookChildProcess(spawned, parsed);
@@ -104684,8 +104829,8 @@ var require_cross_spawn = __commonJS({
104684
104829
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
104685
104830
  return result;
104686
104831
  }
104687
- module.exports = spawn6;
104688
- module.exports.spawn = spawn6;
104832
+ module.exports = spawn7;
104833
+ module.exports.spawn = spawn7;
104689
104834
  module.exports.sync = spawnSync2;
104690
104835
  module.exports._parse = parse2;
104691
104836
  module.exports._enoent = enoent;
@@ -105417,7 +105562,7 @@ function parseStringDef(def, refs) {
105417
105562
  case "trim":
105418
105563
  break;
105419
105564
  default:
105420
- /* @__PURE__ */ ((_) => {
105565
+ /* @__PURE__ */ ((_2) => {
105421
105566
  })(check);
105422
105567
  }
105423
105568
  }
@@ -106265,7 +106410,7 @@ var init_selectParser = __esm({
106265
106410
  case ZodFirstPartyTypeKind.ZodSymbol:
106266
106411
  return void 0;
106267
106412
  default:
106268
- return /* @__PURE__ */ ((_) => void 0)(typeName);
106413
+ return /* @__PURE__ */ ((_2) => void 0)(typeName);
106269
106414
  }
106270
106415
  };
106271
106416
  }
@@ -106528,7 +106673,7 @@ var require_package = __commonJS({
106528
106673
  // node_modules/.pnpm/dotenv@16.6.1/node_modules/dotenv/lib/main.js
106529
106674
  var require_main = __commonJS({
106530
106675
  "node_modules/.pnpm/dotenv@16.6.1/node_modules/dotenv/lib/main.js"(exports, module) {
106531
- var fs50 = __require("fs");
106676
+ var fs51 = __require("fs");
106532
106677
  var path60 = __require("path");
106533
106678
  var os11 = __require("os");
106534
106679
  var crypto4 = __require("crypto");
@@ -106637,7 +106782,7 @@ var require_main = __commonJS({
106637
106782
  if (options && options.path && options.path.length > 0) {
106638
106783
  if (Array.isArray(options.path)) {
106639
106784
  for (const filepath of options.path) {
106640
- if (fs50.existsSync(filepath)) {
106785
+ if (fs51.existsSync(filepath)) {
106641
106786
  possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
106642
106787
  }
106643
106788
  }
@@ -106647,7 +106792,7 @@ var require_main = __commonJS({
106647
106792
  } else {
106648
106793
  possibleVaultPath = path60.resolve(process.cwd(), ".env.vault");
106649
106794
  }
106650
- if (fs50.existsSync(possibleVaultPath)) {
106795
+ if (fs51.existsSync(possibleVaultPath)) {
106651
106796
  return possibleVaultPath;
106652
106797
  }
106653
106798
  return null;
@@ -106696,7 +106841,7 @@ var require_main = __commonJS({
106696
106841
  const parsedAll = {};
106697
106842
  for (const path61 of optionPaths) {
106698
106843
  try {
106699
- const parsed = DotenvModule.parse(fs50.readFileSync(path61, { encoding }));
106844
+ const parsed = DotenvModule.parse(fs51.readFileSync(path61, { encoding }));
106700
106845
  DotenvModule.populate(parsedAll, parsed, options);
106701
106846
  } catch (e) {
106702
106847
  if (debug2) {
@@ -106818,11 +106963,11 @@ var require_main = __commonJS({
106818
106963
  // packages/core/dist/config/env.js
106819
106964
  import path13 from "node:path";
106820
106965
  import os3 from "node:os";
106821
- import fs11 from "node:fs";
106966
+ import fs12 from "node:fs";
106822
106967
  function findRepoRootEnv() {
106823
106968
  let dir = process.cwd();
106824
106969
  for (let i = 0; i < 10; i++) {
106825
- if (fs11.existsSync(path13.join(dir, "pnpm-workspace.yaml"))) {
106970
+ if (fs12.existsSync(path13.join(dir, "pnpm-workspace.yaml"))) {
106826
106971
  return path13.join(dir, ".env");
106827
106972
  }
106828
106973
  const parent = path13.dirname(dir);
@@ -106859,10 +107004,13 @@ var init_config2 = __esm({
106859
107004
  "packages/core/dist/config/index.js"() {
106860
107005
  "use strict";
106861
107006
  init_env();
107007
+ init_dist7();
106862
107008
  config = {
106863
107009
  llmProvider: "claude-code",
106864
- // Claude Code CLI provider settings
106865
- claudeCodeBinary: process.env.CLAUDE_CODE_BINARY || "claude",
107010
+ // Claude Code CLI provider settings. Binary resolution is centralized in
107011
+ // `resolveClaudeBinary` (@truecourse/shared) so every command, the LLM
107012
+ // provider, and the extraction runners agree on the same target.
107013
+ claudeCodeBinary: resolveClaudeBinary(),
106866
107014
  claudeCodeModel: process.env.CLAUDE_CODE_MODEL || "",
106867
107015
  claudeCodeTimeoutMs: parseInt(process.env.CLAUDE_CODE_TIMEOUT_MS || "120000", 10),
106868
107016
  claudeCodeMaxRetries: parseInt(process.env.CLAUDE_CODE_MAX_RETRIES || "2", 10),
@@ -109100,7 +109248,7 @@ var init_violation_lifecycle_service = __esm({
109100
109248
 
109101
109249
  // packages/core/dist/services/violation-pipeline.service.js
109102
109250
  import { randomUUID as randomUUID6 } from "node:crypto";
109103
- import fs12 from "node:fs";
109251
+ import fs13 from "node:fs";
109104
109252
  import path14 from "node:path";
109105
109253
  function throwIfAborted(signal) {
109106
109254
  if (signal?.aborted)
@@ -109218,9 +109366,9 @@ async function runViolationPipeline(input) {
109218
109366
  if (!lang)
109219
109367
  continue;
109220
109368
  const absPath = resolve9 ? path14.resolve(repoPath, filePath) : path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
109221
- if (!fs12.existsSync(absPath))
109369
+ if (!fs13.existsSync(absPath))
109222
109370
  continue;
109223
- const content = fs12.readFileSync(absPath, "utf-8");
109371
+ const content = fs13.readFileSync(absPath, "utf-8");
109224
109372
  const lineCount = content.split("\n").length;
109225
109373
  fileContents.set(changedFileSet ? absPath : filePath, { content, lineCount });
109226
109374
  } catch {
@@ -109359,10 +109507,10 @@ async function runViolationPipeline(input) {
109359
109507
  log.info(`[Pipeline] Code scan: ${allCodeViolations.length} violations from ${filesToScan.length} files (${enabledCodeRules.length} det rules, ${enabledLlmCodeRules.length} LLM rules)`);
109360
109508
  if (enabledCodeRules.some((r) => r.key === "bugs/deterministic/invalid-pyproject-toml")) {
109361
109509
  const pyprojectPath = path14.join(repoPath, "pyproject.toml");
109362
- if (fs12.existsSync(pyprojectPath)) {
109510
+ if (fs13.existsSync(pyprojectPath)) {
109363
109511
  try {
109364
109512
  const { checkPyprojectToml: checkPyprojectToml2 } = await Promise.resolve().then(() => (init_dist6(), dist_exports2));
109365
- const content = fs12.readFileSync(pyprojectPath, "utf-8");
109513
+ const content = fs13.readFileSync(pyprojectPath, "utf-8");
109366
109514
  const tomlViolations = checkPyprojectToml2(pyprojectPath, content);
109367
109515
  allCodeViolations.push(...tomlViolations);
109368
109516
  } catch {
@@ -110269,7 +110417,6 @@ var init_usage_service = __esm({
110269
110417
 
110270
110418
  // packages/core/dist/commands/analyze-core.js
110271
110419
  import { randomUUID as randomUUID7 } from "node:crypto";
110272
- import path15 from "node:path";
110273
110420
  async function analyzeCore(project, options) {
110274
110421
  try {
110275
110422
  acquireAnalyzeLock(project.path);
@@ -110311,32 +110458,20 @@ async function analyzeCore(project, options) {
110311
110458
  const start = Date.now();
110312
110459
  const effectiveCategories = options.enabledCategoriesOverride?.length ? options.enabledCategoriesOverride : projectConfig.enabledCategories ?? void 0;
110313
110460
  const effectiveLlmRules = projectConfig.enableLlmRules ?? options.enableLlmRulesOverride ?? true;
110314
- let didStash = false;
110315
- let stashGit;
110316
- if (!isDiff && !skipGit && !options.skipStash) {
110317
- try {
110318
- stashGit = await getGit(project.path);
110319
- const status = await stashGit.status();
110320
- if (!status.isClean()) {
110321
- const gitRoot = (await stashGit.revparse(["--show-toplevel"])).trim();
110322
- const isSubdirectory = path15.resolve(project.path) !== path15.resolve(gitRoot);
110323
- if (!isSubdirectory) {
110324
- options.tracker?.detail("parse", "Stashing pending changes...");
110325
- options.onProgress?.({ detail: "Stashing pending changes to analyze committed state..." });
110326
- const stashResult = await stashGit.stash([
110327
- "push",
110328
- "--include-untracked",
110329
- "-m",
110330
- "truecourse-analysis-stash"
110331
- ]);
110332
- didStash = !stashResult.includes("No local changes");
110333
- }
110334
- }
110335
- } catch (error) {
110336
- log.warn(`[Analyzer] Failed to stash changes, analyzing current state: ${error instanceof Error ? error.message : String(error)}`);
110337
- }
110338
- }
110339
- try {
110461
+ return await runWithStash(project.path, {
110462
+ skipStash: isDiff || skipGit || (options.skipStash ?? false),
110463
+ message: "truecourse-analysis-stash",
110464
+ onStashStart: () => {
110465
+ options.tracker?.detail("parse", "Stashing pending changes...");
110466
+ options.onProgress?.({ detail: "Stashing pending changes to analyze committed state..." });
110467
+ },
110468
+ onRestoreStart: () => {
110469
+ options.tracker?.detail("parse", "Restoring pending changes...");
110470
+ options.onProgress?.({ detail: "Restoring pending changes..." });
110471
+ },
110472
+ onStashError: (error) => log.warn(`[Analyzer] Failed to stash changes, analyzing current state: ${error.message}`),
110473
+ onRestoreError: (error) => log.error(`[Analyzer] Failed to restore stashed changes. Run "git stash pop" manually. ${error.message}`)
110474
+ }, async () => {
110340
110475
  options.tracker?.start("parse", isDiff ? "Analyzing working tree..." : "Starting analysis...");
110341
110476
  const result = await runAnalysis(project.path, branch ?? void 0, (progress) => {
110342
110477
  options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
@@ -110448,17 +110583,7 @@ async function analyzeCore(project, options) {
110448
110583
  previousAnalysisId,
110449
110584
  analysisResult: result
110450
110585
  };
110451
- } finally {
110452
- if (didStash && stashGit) {
110453
- options.tracker?.detail("parse", "Restoring pending changes...");
110454
- options.onProgress?.({ detail: "Restoring pending changes..." });
110455
- try {
110456
- await stashGit.stash(["pop"]);
110457
- } catch (error) {
110458
- log.error(`[Analyzer] Failed to restore stashed changes. Run "git stash pop" manually. ${error instanceof Error ? error.message : String(error)}`);
110459
- }
110460
- }
110461
- }
110586
+ });
110462
110587
  } finally {
110463
110588
  releaseAnalyzeLock(project.path);
110464
110589
  }
@@ -110494,7 +110619,7 @@ var init_analyze_core = __esm({
110494
110619
  });
110495
110620
 
110496
110621
  // packages/core/dist/commands/analyze-persist.js
110497
- import path16 from "node:path";
110622
+ import path15 from "node:path";
110498
110623
  function persistFullAnalysis(project, core2, startedAt) {
110499
110624
  const filename = buildAnalysisFilename(core2.analysisId, core2.now);
110500
110625
  const snapshot = {
@@ -110622,8 +110747,8 @@ function buildDiffSnapshot(repoPath, core2, baseline) {
110622
110747
  const newViolations = pipelineResult.added.map(denormalize);
110623
110748
  const latestById = new Map(baseline.violations.map((v) => [v.id, v]));
110624
110749
  const resolvedViolations = pipelineResult.resolvedRefs.map((r) => latestById.get(r.id)).filter((v) => !!v);
110625
- const changedAbs = new Set(changedFiles.map((c2) => path16.resolve(repoPath, c2.path)));
110626
- const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path16.resolve(repoPath, p2)));
110750
+ const changedAbs = new Set(changedFiles.map((c2) => path15.resolve(repoPath, c2.path)));
110751
+ const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path15.resolve(repoPath, p2)));
110627
110752
  const affectedModules = graph.modules.filter((m) => matchesChanged(m.filePath));
110628
110753
  const affectedModuleIdSet = new Set(affectedModules.map((m) => m.id));
110629
110754
  const serviceNameById = new Map(graph.services.map((s) => [s.id, s.name]));
@@ -119990,7 +120115,7 @@ var require_shams = __commonJS({
119990
120115
  }
119991
120116
  var symVal = 42;
119992
120117
  obj[sym] = symVal;
119993
- for (var _ in obj) {
120118
+ for (var _2 in obj) {
119994
120119
  return false;
119995
120120
  }
119996
120121
  if (typeof Object.keys === "function" && Object.keys(obj).length !== 0) {
@@ -120658,7 +120783,7 @@ var require_form_data = __commonJS({
120658
120783
  var http = __require("http");
120659
120784
  var https = __require("https");
120660
120785
  var parseUrl = __require("url").parse;
120661
- var fs50 = __require("fs");
120786
+ var fs51 = __require("fs");
120662
120787
  var Stream = __require("stream").Stream;
120663
120788
  var crypto4 = __require("crypto");
120664
120789
  var mime = require_mime_types();
@@ -120725,7 +120850,7 @@ var require_form_data = __commonJS({
120725
120850
  if (value.end != void 0 && value.end != Infinity && value.start != void 0) {
120726
120851
  callback(null, value.end + 1 - (value.start ? value.start : 0));
120727
120852
  } else {
120728
- fs50.stat(value.path, function(err, stat) {
120853
+ fs51.stat(value.path, function(err, stat) {
120729
120854
  if (err) {
120730
120855
  callback(err);
120731
120856
  return;
@@ -121276,7 +121401,7 @@ var require_node2 = __commonJS({
121276
121401
  exports.inspectOpts = Object.keys(process.env).filter(function(key2) {
121277
121402
  return /^debug_/i.test(key2);
121278
121403
  }).reduce(function(obj, key2) {
121279
- var prop = key2.substring(6).toLowerCase().replace(/_([a-z])/g, function(_, k) {
121404
+ var prop = key2.substring(6).toLowerCase().replace(/_([a-z])/g, function(_2, k) {
121280
121405
  return k.toUpperCase();
121281
121406
  });
121282
121407
  var val = process.env[key2];
@@ -121343,8 +121468,8 @@ var require_node2 = __commonJS({
121343
121468
  }
121344
121469
  break;
121345
121470
  case "FILE":
121346
- var fs50 = __require("fs");
121347
- stream2 = new fs50.SyncWriteStream(fd2, { autoClose: false });
121471
+ var fs51 = __require("fs");
121472
+ stream2 = new fs51.SyncWriteStream(fd2, { autoClose: false });
121348
121473
  stream2._type = "fs";
121349
121474
  break;
121350
121475
  case "PIPE":
@@ -122915,11 +123040,11 @@ var require_axios = __commonJS({
122915
123040
  function normalizeHeader(header) {
122916
123041
  return header && String(header).trim().toLowerCase();
122917
123042
  }
122918
- function normalizeValue(value) {
123043
+ function normalizeValue2(value) {
122919
123044
  if (value === false || value == null) {
122920
123045
  return value;
122921
123046
  }
122922
- return utils$1.isArray(value) ? value.map(normalizeValue) : String(value).replace(/[\r\n]+$/, "");
123047
+ return utils$1.isArray(value) ? value.map(normalizeValue2) : String(value).replace(/[\r\n]+$/, "");
122923
123048
  }
122924
123049
  function parseTokens(str2) {
122925
123050
  const tokens = /* @__PURE__ */ Object.create(null);
@@ -122975,7 +123100,7 @@ var require_axios = __commonJS({
122975
123100
  }
122976
123101
  const key2 = utils$1.findKey(self2, lHeader);
122977
123102
  if (!key2 || self2[key2] === void 0 || _rewrite === true || _rewrite === void 0 && self2[key2] !== false) {
122978
- self2[key2 || _header] = normalizeValue(_value);
123103
+ self2[key2 || _header] = normalizeValue2(_value);
122979
123104
  }
122980
123105
  }
122981
123106
  const setHeaders = (headers, _rewrite) => utils$1.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite));
@@ -123066,7 +123191,7 @@ var require_axios = __commonJS({
123066
123191
  utils$1.forEach(this, (value, header) => {
123067
123192
  const key2 = utils$1.findKey(headers, header);
123068
123193
  if (key2) {
123069
- self2[key2] = normalizeValue(value);
123194
+ self2[key2] = normalizeValue2(value);
123070
123195
  delete self2[header];
123071
123196
  return;
123072
123197
  }
@@ -123074,7 +123199,7 @@ var require_axios = __commonJS({
123074
123199
  if (normalized !== header) {
123075
123200
  delete self2[header];
123076
123201
  }
123077
- self2[normalized] = normalizeValue(value);
123202
+ self2[normalized] = normalizeValue2(value);
123078
123203
  headers[normalized] = true;
123079
123204
  });
123080
123205
  return this;
@@ -125460,7 +125585,7 @@ var require_axios = __commonJS({
125460
125585
  // node_modules/.pnpm/posthog-node@4.18.0/node_modules/posthog-node/lib/node/index.mjs
125461
125586
  import { posix, dirname as dirname8, sep as sep2 } from "path";
125462
125587
  import { createReadStream } from "node:fs";
125463
- import { createInterface } from "node:readline";
125588
+ import { createInterface as createInterface2 } from "node:readline";
125464
125589
  function createEventProcessor(_posthog, {
125465
125590
  organization,
125466
125591
  projectId,
@@ -125880,7 +126005,7 @@ async function addSourceContext(frames) {
125880
126005
  function getContextLinesFromFile(path60, ranges, output) {
125881
126006
  return new Promise((resolve9) => {
125882
126007
  const stream = createReadStream(path60);
125883
- const lineReaded = createInterface({
126008
+ const lineReaded = createInterface2({
125884
126009
  input: stream
125885
126010
  });
125886
126011
  function destroyStreamAndResolve() {
@@ -128247,7 +128372,7 @@ var init_node = __esm({
128247
128372
  }
128248
128373
  };
128249
128374
  return Promise.race([
128250
- new Promise((_, reject) => {
128375
+ new Promise((_2, reject) => {
128251
128376
  safeSetTimeout(() => {
128252
128377
  this.logMsgIfDebug(() => console.error("Timed out while shutting down PostHog"));
128253
128378
  hasTimedOut = true;
@@ -129225,18 +129350,18 @@ var init_node = __esm({
129225
129350
  });
129226
129351
 
129227
129352
  // packages/core/dist/services/telemetry.service.js
129228
- import fs13 from "node:fs";
129229
- import path17 from "node:path";
129353
+ import fs14 from "node:fs";
129354
+ import path16 from "node:path";
129230
129355
  import os4 from "node:os";
129231
129356
  import crypto from "node:crypto";
129232
129357
  import { fileURLToPath as fileURLToPath5 } from "node:url";
129233
129358
  function getTelemetryConfigPath() {
129234
- return path17.join(os4.homedir(), ".truecourse", "telemetry.json");
129359
+ return path16.join(os4.homedir(), ".truecourse", "telemetry.json");
129235
129360
  }
129236
129361
  function readTelemetryConfig() {
129237
129362
  const configPath = getTelemetryConfigPath();
129238
129363
  try {
129239
- const raw = fs13.readFileSync(configPath, "utf-8");
129364
+ const raw = fs14.readFileSync(configPath, "utf-8");
129240
129365
  const parsed = JSON.parse(raw);
129241
129366
  const config2 = { ...DEFAULT_CONFIG2, ...parsed };
129242
129367
  if (!config2.anonymousId) {
@@ -129256,17 +129381,17 @@ function readTelemetryConfig() {
129256
129381
  }
129257
129382
  function writeTelemetryConfig(partial) {
129258
129383
  const configPath = getTelemetryConfigPath();
129259
- const dir = path17.dirname(configPath);
129260
- fs13.mkdirSync(dir, { recursive: true });
129384
+ const dir = path16.dirname(configPath);
129385
+ fs14.mkdirSync(dir, { recursive: true });
129261
129386
  let current;
129262
129387
  try {
129263
- const raw = fs13.readFileSync(configPath, "utf-8");
129388
+ const raw = fs14.readFileSync(configPath, "utf-8");
129264
129389
  current = { ...DEFAULT_CONFIG2, ...JSON.parse(raw) };
129265
129390
  } catch {
129266
129391
  current = { ...DEFAULT_CONFIG2 };
129267
129392
  }
129268
129393
  const merged = { ...current, ...partial };
129269
- fs13.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
129394
+ fs14.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
129270
129395
  }
129271
129396
  function isTelemetryEnabled() {
129272
129397
  if (!POSTHOG_API_KEY)
@@ -129312,7 +129437,7 @@ function detectLanguages(result) {
129312
129437
  const languages = /* @__PURE__ */ new Set();
129313
129438
  for (const service of result.services) {
129314
129439
  for (const filePath of service.files) {
129315
- const ext2 = path17.extname(filePath).toLowerCase();
129440
+ const ext2 = path16.extname(filePath).toLowerCase();
129316
129441
  const lang = EXTENSION_TO_LANGUAGE[ext2];
129317
129442
  if (lang)
129318
129443
  languages.add(lang);
@@ -129324,13 +129449,13 @@ function readToolVersion() {
129324
129449
  if (cachedVersion)
129325
129450
  return cachedVersion;
129326
129451
  if (true) {
129327
- cachedVersion = "0.6.1";
129452
+ cachedVersion = "0.6.6-next.0";
129328
129453
  return cachedVersion;
129329
129454
  }
129330
129455
  try {
129331
129456
  const here = fileURLToPath5(import.meta.url);
129332
- const pkgPath = path17.resolve(path17.dirname(here), "..", "..", "package.json");
129333
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
129457
+ const pkgPath = path16.resolve(path16.dirname(here), "..", "..", "package.json");
129458
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
129334
129459
  cachedVersion = String(pkg.version ?? "0.0.0");
129335
129460
  } catch {
129336
129461
  cachedVersion = "0.0.0";
@@ -131224,7 +131349,7 @@ var init_forbidden_artifact = __esm({
131224
131349
  // packages/contract-verifier/dist/resolver/lifters/named-constant.js
131225
131350
  function liftNamedConstant(body) {
131226
131351
  let type2 = "string";
131227
- let expectedValue = "";
131352
+ let expectedValue = void 0;
131228
131353
  for (const stmt of body) {
131229
131354
  const h = stmt.head;
131230
131355
  if (h.length === 0 || h[0].kind !== "ident")
@@ -131705,6 +131830,351 @@ var init_resolver = __esm({
131705
131830
  }
131706
131831
  });
131707
131832
 
131833
+ // packages/contract-verifier/dist/extractor/handler-facts.js
131834
+ function extractHandlerFacts(body, source) {
131835
+ return {
131836
+ emission: extractEmission(body, source),
131837
+ ownershipCheckCandidates: extractOwnershipCandidates(body, source)
131838
+ };
131839
+ }
131840
+ function emptyHandlerFacts() {
131841
+ return {
131842
+ emission: {
131843
+ staticEvents: /* @__PURE__ */ new Set(),
131844
+ hasDynamicEmit: false,
131845
+ failureEmitSites: [],
131846
+ branchEmits: /* @__PURE__ */ new Map()
131847
+ },
131848
+ ownershipCheckCandidates: []
131849
+ };
131850
+ }
131851
+ function extractEmission(body, source) {
131852
+ const callMap = collectEmitCalls(body, source);
131853
+ const failureEmitSites = [];
131854
+ for (const [event, calls] of callMap) {
131855
+ for (const call of calls) {
131856
+ if (emitIsInFailureBlock(call, source)) {
131857
+ failureEmitSites.push({
131858
+ event,
131859
+ lineStart: call.startPosition.row + 1,
131860
+ lineEnd: call.endPosition.row + 1
131861
+ });
131862
+ }
131863
+ }
131864
+ }
131865
+ return {
131866
+ staticEvents: new Set(callMap.keys()),
131867
+ hasDynamicEmit: handlerHasDynamicEmit(body, source),
131868
+ failureEmitSites,
131869
+ branchEmits: collectBranchEmits(body, source)
131870
+ };
131871
+ }
131872
+ function isCall(n) {
131873
+ return n.type === "call_expression" || n.type === "call";
131874
+ }
131875
+ function isMember(n) {
131876
+ return n?.type === "member_expression" || n?.type === "attribute";
131877
+ }
131878
+ function memberProp(n, source) {
131879
+ const p2 = n.childForFieldName("property") ?? n.childForFieldName("attribute");
131880
+ return p2 ? source.slice(p2.startIndex, p2.endIndex) : "";
131881
+ }
131882
+ function isBlock(n) {
131883
+ return n.type === "statement_block" || n.type === "block";
131884
+ }
131885
+ function isFnBoundary(n) {
131886
+ return n.type === "function_declaration" || n.type === "arrow_function" || n.type === "function_expression" || n.type === "function_definition";
131887
+ }
131888
+ function isControlFlow(n) {
131889
+ return /^(if_statement|switch_statement|try_statement|for_statement|for_in_statement|while_statement|do_statement)$/.test(n.type);
131890
+ }
131891
+ function strVal(n, source) {
131892
+ if (n.type !== "string")
131893
+ return null;
131894
+ const frag = n.namedChildren.find((c2) => c2.type === "string_fragment" || c2.type === "string_content");
131895
+ return frag ? source.slice(frag.startIndex, frag.endIndex) : null;
131896
+ }
131897
+ function isEmitFn(fn, source) {
131898
+ if (isMember(fn))
131899
+ return memberProp(fn, source) === "emit";
131900
+ if (fn.type === "identifier")
131901
+ return /^emit/i.test(source.slice(fn.startIndex, fn.endIndex));
131902
+ return false;
131903
+ }
131904
+ function collectEmitCalls(body, source) {
131905
+ const out = /* @__PURE__ */ new Map();
131906
+ const visit = (node2) => {
131907
+ if (isCall(node2)) {
131908
+ const fn = node2.childForFieldName("function");
131909
+ const args = node2.childForFieldName("arguments");
131910
+ if (fn && args && isEmitFn(fn, source)) {
131911
+ const first2 = args.namedChild(0);
131912
+ const eventName = first2 ? strVal(first2, source) : null;
131913
+ if (eventName !== null) {
131914
+ const arr = out.get(eventName) ?? [];
131915
+ arr.push(node2);
131916
+ out.set(eventName, arr);
131917
+ }
131918
+ }
131919
+ }
131920
+ for (const child of node2.namedChildren)
131921
+ visit(child);
131922
+ };
131923
+ visit(body);
131924
+ return out;
131925
+ }
131926
+ function handlerHasDynamicEmit(body, source) {
131927
+ let dynamic = false;
131928
+ const visit = (node2) => {
131929
+ if (dynamic)
131930
+ return;
131931
+ if (isCall(node2)) {
131932
+ const fn = node2.childForFieldName("function");
131933
+ const args = node2.childForFieldName("arguments");
131934
+ if (fn && args && isEmitFn(fn, source)) {
131935
+ const first2 = args.namedChild(0);
131936
+ if (first2 && first2.type !== "string") {
131937
+ dynamic = true;
131938
+ return;
131939
+ }
131940
+ }
131941
+ }
131942
+ for (const child of node2.namedChildren) {
131943
+ visit(child);
131944
+ if (dynamic)
131945
+ return;
131946
+ }
131947
+ };
131948
+ visit(body);
131949
+ return dynamic;
131950
+ }
131951
+ function emitIsInFailureBlock(emitCall, source) {
131952
+ let cur = emitCall.parent;
131953
+ while (cur) {
131954
+ if (isBlock(cur))
131955
+ return blockContainsFailureStatusShallow(cur, source);
131956
+ if (isFnBoundary(cur))
131957
+ break;
131958
+ cur = cur.parent;
131959
+ }
131960
+ return false;
131961
+ }
131962
+ function blockContainsFailureStatusShallow(block, source) {
131963
+ for (const stmt of block.namedChildren) {
131964
+ if (containsTopLevelFailureStatus(stmt, source))
131965
+ return true;
131966
+ }
131967
+ return false;
131968
+ }
131969
+ function containsTopLevelFailureStatus(stmt, source) {
131970
+ if (isControlFlow(stmt))
131971
+ return false;
131972
+ let found = false;
131973
+ const visit = (node2) => {
131974
+ if (found)
131975
+ return;
131976
+ if (isControlFlow(node2))
131977
+ return;
131978
+ if (isCall(node2) && callEmitsFailureStatus(node2, source)) {
131979
+ found = true;
131980
+ return;
131981
+ }
131982
+ for (const child of node2.namedChildren) {
131983
+ visit(child);
131984
+ if (found)
131985
+ return;
131986
+ }
131987
+ };
131988
+ visit(stmt);
131989
+ return found;
131990
+ }
131991
+ function callEmitsFailureStatus(call, source) {
131992
+ const fn = call.childForFieldName("function");
131993
+ if (!fn)
131994
+ return false;
131995
+ const args = call.childForFieldName("arguments");
131996
+ if (!args)
131997
+ return false;
131998
+ if (isMember(fn) && memberProp(fn, source) === "status") {
131999
+ const arg = args.namedChild(0);
132000
+ return !!arg && arg.type === "number" && /^[45]\d{2}$/.test(source.slice(arg.startIndex, arg.endIndex));
132001
+ }
132002
+ const fnName = fn.type === "identifier" ? source.slice(fn.startIndex, fn.endIndex) : isMember(fn) ? memberProp(fn, source) : "";
132003
+ if (fnName === "JSONResponse" || fnName === "HTTPException" || fnName === "Response") {
132004
+ for (let i = 0; i < args.namedChildCount; i++) {
132005
+ const a = args.namedChild(i);
132006
+ if (a?.type === "keyword_argument") {
132007
+ const name = a.childForFieldName("name");
132008
+ const value = a.childForFieldName("value");
132009
+ if (name && value && source.slice(name.startIndex, name.endIndex) === "status_code" && value.type === "integer" && /^[45]\d{2}$/.test(source.slice(value.startIndex, value.endIndex))) {
132010
+ return true;
132011
+ }
132012
+ }
132013
+ }
132014
+ const first2 = args.namedChild(0);
132015
+ if (first2?.type === "integer" && /^[45]\d{2}$/.test(source.slice(first2.startIndex, first2.endIndex)))
132016
+ return true;
132017
+ }
132018
+ return false;
132019
+ }
132020
+ function collectBranchEmits(body, source) {
132021
+ const out = /* @__PURE__ */ new Map();
132022
+ const visit = (node2) => {
132023
+ if (node2.type === "if_statement") {
132024
+ const cond = node2.childForFieldName("condition");
132025
+ const consequent = node2.childForFieldName("consequence");
132026
+ const alternative = node2.childForFieldName("alternative");
132027
+ const literal = cond ? conditionLiteral(cond, source) : null;
132028
+ if (literal !== null && consequent && !out.has(literal)) {
132029
+ out.set(literal, {
132030
+ consequentEmits: blockHasAnyEmit(consequent, source),
132031
+ alternativeEmits: alternative ? blockHasAnyEmit(alternative, source) : false
132032
+ });
132033
+ }
132034
+ }
132035
+ for (const child of node2.namedChildren)
132036
+ visit(child);
132037
+ };
132038
+ visit(body);
132039
+ return out;
132040
+ }
132041
+ function conditionLiteral(node2, source) {
132042
+ const u2 = unwrapParens(node2);
132043
+ if (u2.type === "binary_expression") {
132044
+ const opNode = u2.childForFieldName("operator");
132045
+ const op = opNode ? source.slice(opNode.startIndex, opNode.endIndex) : "";
132046
+ if (op !== "===" && op !== "==")
132047
+ return null;
132048
+ return litText(u2.childForFieldName("left"), source) ?? litText(u2.childForFieldName("right"), source);
132049
+ }
132050
+ if (u2.type === "comparison_operator") {
132051
+ const a = u2.namedChild(0);
132052
+ const b = u2.namedChild(1);
132053
+ if (!a || !b || source.slice(a.endIndex, b.startIndex).trim() !== "==")
132054
+ return null;
132055
+ return litText(a, source) ?? litText(b, source);
132056
+ }
132057
+ return null;
132058
+ }
132059
+ function litText(node2, source) {
132060
+ return node2 && node2.type === "string" ? strVal(node2, source) : null;
132061
+ }
132062
+ function unwrapParens(node2) {
132063
+ let cur = node2;
132064
+ while (cur.type === "parenthesized_expression") {
132065
+ const child = cur.namedChildren[0];
132066
+ if (!child)
132067
+ break;
132068
+ cur = child;
132069
+ }
132070
+ return cur;
132071
+ }
132072
+ function blockHasAnyEmit(block, source) {
132073
+ let found = false;
132074
+ const visit = (node2) => {
132075
+ if (found)
132076
+ return;
132077
+ if (isCall(node2)) {
132078
+ const fn = node2.childForFieldName("function");
132079
+ if (fn && isEmitFn(fn, source)) {
132080
+ found = true;
132081
+ return;
132082
+ }
132083
+ }
132084
+ for (const child of node2.namedChildren) {
132085
+ visit(child);
132086
+ if (found)
132087
+ return;
132088
+ }
132089
+ };
132090
+ visit(block);
132091
+ return found;
132092
+ }
132093
+ function extractOwnershipCandidates(body, source) {
132094
+ const out = [];
132095
+ const tryEq = (left, right, line) => {
132096
+ const leftAuth = matchesAuthSide(left, source);
132097
+ const rightAuth = matchesAuthSide(right, source);
132098
+ if (!leftAuth && !rightAuth)
132099
+ return;
132100
+ if (rightAuth) {
132101
+ const field = terminalProperty(left, source);
132102
+ if (field)
132103
+ out.push({ resourceField: field, line });
132104
+ }
132105
+ if (leftAuth) {
132106
+ const field = terminalProperty(right, source);
132107
+ if (field)
132108
+ out.push({ resourceField: field, line });
132109
+ }
132110
+ };
132111
+ const visit = (node2) => {
132112
+ if (node2.type === "binary_expression") {
132113
+ const opNode = node2.childForFieldName("operator");
132114
+ const op = opNode ? source.slice(opNode.startIndex, opNode.endIndex) : "";
132115
+ if (op === "===" || op === "==" || op === "!==" || op === "!=") {
132116
+ const left = node2.childForFieldName("left");
132117
+ const right = node2.childForFieldName("right");
132118
+ if (left && right)
132119
+ tryEq(left, right, node2.startPosition.row + 1);
132120
+ }
132121
+ }
132122
+ if (node2.type === "comparison_operator") {
132123
+ const a = node2.namedChild(0);
132124
+ const b = node2.namedChild(1);
132125
+ if (a && b) {
132126
+ const op = source.slice(a.endIndex, b.startIndex).trim();
132127
+ if (op === "==" || op === "!=")
132128
+ tryEq(a, b, node2.startPosition.row + 1);
132129
+ }
132130
+ }
132131
+ for (const child of node2.namedChildren)
132132
+ visit(child);
132133
+ };
132134
+ visit(body);
132135
+ return out;
132136
+ }
132137
+ function terminalProperty(node2, source) {
132138
+ if (node2.type !== "member_expression" && node2.type !== "attribute")
132139
+ return null;
132140
+ const prop = node2.childForFieldName("property") ?? node2.childForFieldName("attribute");
132141
+ if (!prop)
132142
+ return null;
132143
+ return source.slice(prop.startIndex, prop.endIndex);
132144
+ }
132145
+ function matchesAuthSide(node2, source) {
132146
+ if (node2.type === "attribute") {
132147
+ return /\b(req|request)\.(auth|user)\b|\bcurrent_user\b/.test(source.slice(node2.startIndex, node2.endIndex));
132148
+ }
132149
+ let cur = node2;
132150
+ while (cur) {
132151
+ if (cur.type === "identifier") {
132152
+ const text = source.slice(cur.startIndex, cur.endIndex);
132153
+ if (text === "req" || text === "request")
132154
+ return false;
132155
+ return false;
132156
+ }
132157
+ if (cur.type === "member_expression") {
132158
+ const text = source.slice(cur.startIndex, cur.endIndex);
132159
+ if (/\b(req|request)\.auth\b|\b(req|request)\?\.auth\b/.test(text))
132160
+ return true;
132161
+ cur = cur.childForFieldName("object");
132162
+ continue;
132163
+ }
132164
+ if (cur.type === "optional_chain_expression" || cur.type === "subscript_expression") {
132165
+ cur = cur.childForFieldName("object");
132166
+ continue;
132167
+ }
132168
+ return false;
132169
+ }
132170
+ return false;
132171
+ }
132172
+ var init_handler_facts = __esm({
132173
+ "packages/contract-verifier/dist/extractor/handler-facts.js"() {
132174
+ "use strict";
132175
+ }
132176
+ });
132177
+
131708
132178
  // packages/contract-verifier/dist/extractor/operation.js
131709
132179
  function extractOperationsFromFile(filePath, source, tree) {
131710
132180
  const fnIndex = buildFunctionIndex(tree.rootNode, source);
@@ -131788,6 +132258,7 @@ function tryExtractRouteCall(call, source, filePath, fnIndex) {
131788
132258
  const bodyToWalk = resolveDelegationTarget(handlerBody, source, fnIndex) ?? handlerBody;
131789
132259
  const responses = extractResponses(bodyToWalk, source);
131790
132260
  const observed = collectHandlerObservations(bodyToWalk, source);
132261
+ const facts = extractHandlerFacts(bodyToWalk, source);
131791
132262
  return {
131792
132263
  identity: `${method.toUpperCase()} ${pathLit}`,
131793
132264
  contract: {
@@ -131801,8 +132272,8 @@ function tryExtractRouteCall(call, source, filePath, fnIndex) {
131801
132272
  declarationLine: call.startPosition.row + 1,
131802
132273
  routerName,
131803
132274
  observed,
131804
- handlerBody: bodyToWalk,
131805
- handlerSource: source
132275
+ emission: facts.emission,
132276
+ ownershipCheckCandidates: facts.ownershipCheckCandidates
131806
132277
  };
131807
132278
  }
131808
132279
  function collectHandlerObservationsFromBody(body, source) {
@@ -131980,7 +132451,7 @@ function describeResCall(call, source) {
131980
132451
  }
131981
132452
  function describeWebResponseNew(node2, source) {
131982
132453
  const ctor = node2.childForFieldName("constructor");
131983
- if (!ctor || ctor.type !== "identifier" || sliceText(ctor, source) !== "Response")
132454
+ if (!ctor || ctor.type !== "identifier" || !WEB_RESPONSE_CTORS.has(sliceText(ctor, source)))
131984
132455
  return null;
131985
132456
  const args = node2.childForFieldName("arguments");
131986
132457
  const status = readStatusFromInit(args?.namedChild(1) ?? null, source) ?? "200";
@@ -132006,7 +132477,7 @@ function describeWebResponseCall(call, source) {
132006
132477
  if (obj.type !== "identifier")
132007
132478
  return null;
132008
132479
  const objName = sliceText(obj, source);
132009
- if (objName !== "Response" && objName !== "NextResponse")
132480
+ if (objName !== "Response" && objName !== "NextResponse" && objName !== "HttpResponse")
132010
132481
  return null;
132011
132482
  const args = call.childForFieldName("arguments");
132012
132483
  const status = readStatusFromInit(args?.namedChild(1) ?? null, source) ?? "200";
@@ -132163,10 +132634,135 @@ function readStringLiteral(node2, source) {
132163
132634
  }
132164
132635
  return null;
132165
132636
  }
132166
- var HTTP_METHODS3, RES_SEND_METHODS;
132637
+ function inferPluginMountPrefix(filePath) {
132638
+ const normalized = filePath.replace(/\\/g, "/");
132639
+ const m = normalized.match(/\/([^/]+)\/server(?:\/src)?\/routes\//);
132640
+ if (!m)
132641
+ return null;
132642
+ return "/" + m[1];
132643
+ }
132644
+ function joinPluginPath(prefix, routePath) {
132645
+ if (!routePath)
132646
+ return prefix || "/";
132647
+ const a = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
132648
+ const b = routePath.startsWith("/") ? routePath : "/" + routePath;
132649
+ return a + b;
132650
+ }
132651
+ function tryReadPluginRouteDescriptor(obj, source) {
132652
+ let method = null;
132653
+ let routePath = null;
132654
+ let authExempt = false;
132655
+ for (const child of obj.namedChildren) {
132656
+ if (child.type !== "pair")
132657
+ continue;
132658
+ const key2 = child.childForFieldName("key");
132659
+ const val = child.childForFieldName("value");
132660
+ if (!key2 || !val)
132661
+ continue;
132662
+ const keyName = key2.text.replace(/^['"]|['"]$/g, "");
132663
+ if (keyName === "method") {
132664
+ const v = readStringLiteral(val, source);
132665
+ if (v && PLUGIN_ROUTE_METHODS.has(v.toUpperCase()))
132666
+ method = v.toUpperCase();
132667
+ } else if (keyName === "path") {
132668
+ const v = readStringLiteral(val, source);
132669
+ if (v !== null && v.startsWith("/"))
132670
+ routePath = v;
132671
+ } else if (keyName === "config" && val.type === "object") {
132672
+ for (const cfgChild of val.namedChildren) {
132673
+ if (cfgChild.type !== "pair")
132674
+ continue;
132675
+ const cfgKey = cfgChild.childForFieldName("key");
132676
+ const cfgVal = cfgChild.childForFieldName("value");
132677
+ if (!cfgKey || !cfgVal)
132678
+ continue;
132679
+ const cfgKeyName = cfgKey.text.replace(/^['"]|['"]$/g, "");
132680
+ if (cfgKeyName === "auth" && cfgVal.type === "false") {
132681
+ authExempt = true;
132682
+ } else if (cfgKeyName === "policies" && cfgVal.type === "array" && cfgVal.namedChildCount > 0) {
132683
+ authExempt = true;
132684
+ }
132685
+ }
132686
+ }
132687
+ }
132688
+ if (!method || routePath === null)
132689
+ return null;
132690
+ return { method, path: routePath, line: obj.startPosition.row + 1, authExempt };
132691
+ }
132692
+ function collectPluginRouteDescriptors(node2, source) {
132693
+ const out = [];
132694
+ if (node2.type === "array") {
132695
+ for (const el of node2.namedChildren) {
132696
+ if (el.type === "object") {
132697
+ const desc = tryReadPluginRouteDescriptor(el, source);
132698
+ if (desc)
132699
+ out.push(desc);
132700
+ }
132701
+ }
132702
+ return out;
132703
+ }
132704
+ if (node2.type === "object") {
132705
+ for (const child of node2.namedChildren) {
132706
+ if (child.type !== "pair")
132707
+ continue;
132708
+ const key2 = child.childForFieldName("key");
132709
+ const val = child.childForFieldName("value");
132710
+ if (!key2 || !val)
132711
+ continue;
132712
+ const keyName = key2.text.replace(/^['"]|['"]$/g, "");
132713
+ if (keyName === "routes" && val.type === "array") {
132714
+ for (const el of val.namedChildren) {
132715
+ if (el.type === "object") {
132716
+ const desc = tryReadPluginRouteDescriptor(el, source);
132717
+ if (desc)
132718
+ out.push(desc);
132719
+ }
132720
+ }
132721
+ }
132722
+ }
132723
+ }
132724
+ return out;
132725
+ }
132726
+ function extractPluginStyleRoutesFromFile(filePath, source, tree) {
132727
+ const prefix = inferPluginMountPrefix(filePath);
132728
+ if (!prefix)
132729
+ return [];
132730
+ const out = [];
132731
+ const visit = (node2) => {
132732
+ if (node2.type === "export_statement") {
132733
+ const value = node2.childForFieldName("value");
132734
+ if (value) {
132735
+ const descriptors = collectPluginRouteDescriptors(value, source);
132736
+ for (const { method, path: routePath, line, authExempt } of descriptors) {
132737
+ const fullPath = joinPluginPath(prefix, routePath);
132738
+ out.push({
132739
+ identity: `${method} ${fullPath}`,
132740
+ contract: {
132741
+ protocol: "http",
132742
+ method,
132743
+ path: fullPath,
132744
+ tags: [],
132745
+ responses: []
132746
+ },
132747
+ filePath,
132748
+ declarationLine: line,
132749
+ observed: { queryParams: [], numericClamps: [], hasClampCall: false },
132750
+ ...authExempt ? { authExempt: true } : {}
132751
+ });
132752
+ }
132753
+ }
132754
+ }
132755
+ for (const child of node2.namedChildren)
132756
+ visit(child);
132757
+ };
132758
+ visit(tree.rootNode);
132759
+ return out;
132760
+ }
132761
+ var HTTP_METHODS3, RES_SEND_METHODS, WEB_RESPONSE_CTORS, PLUGIN_ROUTE_METHODS;
132167
132762
  var init_operation2 = __esm({
132168
132763
  "packages/contract-verifier/dist/extractor/operation.js"() {
132169
132764
  "use strict";
132765
+ init_handler_facts();
132170
132766
  HTTP_METHODS3 = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch"]);
132171
132767
  RES_SEND_METHODS = /* @__PURE__ */ new Set([
132172
132768
  "json",
@@ -132176,12 +132772,15 @@ var init_operation2 = __esm({
132176
132772
  "sendFile",
132177
132773
  "render"
132178
132774
  ]);
132775
+ WEB_RESPONSE_CTORS = /* @__PURE__ */ new Set(["Response", "HttpResponse"]);
132776
+ PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]);
132179
132777
  }
132180
132778
  });
132181
132779
 
132182
132780
  // packages/contract-verifier/dist/extractor/operation-fastapi.js
132183
132781
  function extractFastApiOperationsFromFile(filePath, source, tree) {
132184
132782
  const routers = collectRouters(tree.rootNode, source);
132783
+ const stringVars = collectStringVars(tree.rootNode, source);
132185
132784
  const out = [];
132186
132785
  walk(tree.rootNode, (node2) => {
132187
132786
  if (node2.type !== "decorated_definition")
@@ -132193,7 +132792,7 @@ function extractFastApiOperationsFromFile(filePath, source, tree) {
132193
132792
  const dec = node2.namedChild(i);
132194
132793
  if (dec?.type !== "decorator")
132195
132794
  continue;
132196
- const route = parseRouteDecorator(dec, source);
132795
+ const route = parseRouteDecorator(dec, source, stringVars);
132197
132796
  if (!route)
132198
132797
  continue;
132199
132798
  const router = routers.get(route.routerVar) ?? { prefix: "", hasAuthDep: false };
@@ -132202,6 +132801,7 @@ function extractFastApiOperationsFromFile(filePath, source, tree) {
132202
132801
  const paramsNode = def.childForFieldName("parameters");
132203
132802
  const responses = extractResponses2(body, source, route.successStatus);
132204
132803
  const observed = collectObservations(paramsNode, body, source, fullPath);
132804
+ const facts = body ? extractHandlerFacts(body, source) : emptyHandlerFacts();
132205
132805
  out.push({
132206
132806
  identity: `${route.method.toUpperCase()} ${fullPath}`,
132207
132807
  contract: {
@@ -132215,8 +132815,8 @@ function extractFastApiOperationsFromFile(filePath, source, tree) {
132215
132815
  declarationLine: node2.startPosition.row + 1,
132216
132816
  routerName: route.routerVar,
132217
132817
  observed,
132218
- handlerBody: body ?? void 0,
132219
- handlerSource: source
132818
+ emission: facts.emission,
132819
+ ownershipCheckCandidates: facts.ownershipCheckCandidates
132220
132820
  });
132221
132821
  break;
132222
132822
  }
@@ -132234,7 +132834,7 @@ function collectRouters(root, source) {
132234
132834
  return;
132235
132835
  const fn = right.childForFieldName("function");
132236
132836
  const fnName = fn ? source.slice(fn.startIndex, fn.endIndex) : "";
132237
- if (fnName !== "APIRouter" && fnName !== "FastAPI")
132837
+ if (fnName !== "FastAPI" && !fnName.endsWith("Router"))
132238
132838
  return;
132239
132839
  const args = right.childForFieldName("arguments");
132240
132840
  let prefix = "";
@@ -132267,7 +132867,37 @@ function fastApiFileHasAuthRouter(source, tree) {
132267
132867
  }
132268
132868
  return false;
132269
132869
  }
132270
- function parseRouteDecorator(dec, source) {
132870
+ function collectStringVars(root, source) {
132871
+ const vars = /* @__PURE__ */ new Map();
132872
+ walk(root, (node2) => {
132873
+ if (node2.type === "assignment") {
132874
+ const left = node2.childForFieldName("left");
132875
+ const right = node2.childForFieldName("right");
132876
+ if (left?.type === "identifier" && right?.type === "string") {
132877
+ vars.set(source.slice(left.startIndex, left.endIndex), pyStr(right, source));
132878
+ }
132879
+ }
132880
+ if (node2.type === "function_definition") {
132881
+ const params = node2.childForFieldName("parameters");
132882
+ if (params) {
132883
+ for (let i = 0; i < params.namedChildCount; i++) {
132884
+ const p2 = params.namedChild(i);
132885
+ if (!p2)
132886
+ continue;
132887
+ if (p2.type === "typed_default_parameter" || p2.type === "default_parameter") {
132888
+ const name = p2.childForFieldName("name");
132889
+ const value = p2.childForFieldName("value");
132890
+ if (name?.type === "identifier" && value?.type === "string") {
132891
+ vars.set(source.slice(name.startIndex, name.endIndex), pyStr(value, source));
132892
+ }
132893
+ }
132894
+ }
132895
+ }
132896
+ }
132897
+ });
132898
+ return vars;
132899
+ }
132900
+ function parseRouteDecorator(dec, source, stringVars) {
132271
132901
  const call = dec.namedChild(0);
132272
132902
  if (call?.type !== "call")
132273
132903
  return null;
@@ -132300,6 +132930,14 @@ function parseRouteDecorator(dec, source) {
132300
132930
  }
132301
132931
  }
132302
132932
  }
132933
+ if (path60 === null) {
132934
+ const firstArg = args.namedChild(0);
132935
+ if (firstArg?.type === "identifier") {
132936
+ const resolved = stringVars.get(source.slice(firstArg.startIndex, firstArg.endIndex));
132937
+ if (resolved !== void 0)
132938
+ path60 = resolved;
132939
+ }
132940
+ }
132303
132941
  if (path60 === null)
132304
132942
  return null;
132305
132943
  return { method, path: path60, routerVar, successStatus };
@@ -132510,28 +133148,29 @@ var HTTP_METHODS4, AUTH_DEP_NAMES;
132510
133148
  var init_operation_fastapi = __esm({
132511
133149
  "packages/contract-verifier/dist/extractor/operation-fastapi.js"() {
132512
133150
  "use strict";
133151
+ init_handler_facts();
132513
133152
  HTTP_METHODS4 = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch"]);
132514
133153
  AUTH_DEP_NAMES = /require_bearer|require_auth|authenticate|get_current_user|bearer|require_role|require_admin/i;
132515
133154
  }
132516
133155
  });
132517
133156
 
132518
133157
  // packages/contract-verifier/dist/extractor/source-walker.js
132519
- import fs32 from "node:fs";
132520
- import path37 from "node:path";
133158
+ import fs33 from "node:fs";
133159
+ import path36 from "node:path";
132521
133160
  async function eachParsedSource(rootDir, visit) {
132522
133161
  await initParsers();
132523
133162
  const tcIgnore = loadTcIgnore(rootDir);
132524
133163
  const walk11 = (dir) => {
132525
133164
  let entries;
132526
133165
  try {
132527
- entries = fs32.readdirSync(dir, { withFileTypes: true });
133166
+ entries = fs33.readdirSync(dir, { withFileTypes: true });
132528
133167
  } catch {
132529
133168
  return;
132530
133169
  }
132531
133170
  for (const entry of entries) {
132532
133171
  if (SKIP_DIRS2.has(entry.name))
132533
133172
  continue;
132534
- const full = path37.join(dir, entry.name);
133173
+ const full = path36.join(dir, entry.name);
132535
133174
  if (tcIgnore.ignores(full))
132536
133175
  continue;
132537
133176
  if (entry.isDirectory()) {
@@ -132540,19 +133179,22 @@ async function eachParsedSource(rootDir, visit) {
132540
133179
  }
132541
133180
  if (!entry.isFile())
132542
133181
  continue;
132543
- const lang = EXT_TO_LANG[path37.extname(entry.name)];
133182
+ const lang = EXT_TO_LANG[path36.extname(entry.name)];
132544
133183
  if (!lang)
132545
133184
  continue;
132546
133185
  let source;
132547
133186
  try {
132548
- source = fs32.readFileSync(full, "utf-8");
133187
+ source = fs33.readFileSync(full, "utf-8");
132549
133188
  } catch {
132550
133189
  continue;
132551
133190
  }
133191
+ let tree;
132552
133192
  try {
132553
- const tree = parseFile(full, source, lang);
133193
+ tree = parseFile(full, source, lang);
132554
133194
  visit({ filePath: full, source, tree, lang });
132555
133195
  } catch {
133196
+ } finally {
133197
+ tree?.delete();
132556
133198
  }
132557
133199
  }
132558
133200
  };
@@ -132608,8 +133250,8 @@ var init_source_walker = __esm({
132608
133250
  });
132609
133251
 
132610
133252
  // packages/contract-verifier/dist/extractor/file-based-routes.js
132611
- import fs33 from "node:fs";
132612
- import path38 from "node:path";
133253
+ import fs34 from "node:fs";
133254
+ import path37 from "node:path";
132613
133255
  async function extractFileBasedRoutesFromDir(rootDir) {
132614
133256
  await initParsers();
132615
133257
  const out = [];
@@ -132627,7 +133269,7 @@ function findDirs(rootDir, wantedRel) {
132627
133269
  const visit = (dir) => {
132628
133270
  let entries;
132629
133271
  try {
132630
- entries = fs33.readdirSync(dir, { withFileTypes: true });
133272
+ entries = fs34.readdirSync(dir, { withFileTypes: true });
132631
133273
  } catch {
132632
133274
  return;
132633
133275
  }
@@ -132636,11 +133278,11 @@ function findDirs(rootDir, wantedRel) {
132636
133278
  continue;
132637
133279
  if (!entry.isDirectory())
132638
133280
  continue;
132639
- const full = path38.join(dir, entry.name);
133281
+ const full = path37.join(dir, entry.name);
132640
133282
  if (tcIgnore.ignores(full))
132641
133283
  continue;
132642
- const match4 = path38.join(...wantedSegments);
132643
- if (full.endsWith(path38.sep + match4) || full === match4) {
133284
+ const match4 = path37.join(...wantedSegments);
133285
+ if (full.endsWith(path37.sep + match4) || full === match4) {
132644
133286
  results.push(full);
132645
133287
  continue;
132646
133288
  }
@@ -132653,10 +133295,10 @@ function findDirs(rootDir, wantedRel) {
132653
133295
  function walkRoot(rootAbs, root, out) {
132654
133296
  const tcIgnore = loadTcIgnore(rootAbs);
132655
133297
  const visit = (dir) => {
132656
- for (const entry of fs33.readdirSync(dir, { withFileTypes: true })) {
133298
+ for (const entry of fs34.readdirSync(dir, { withFileTypes: true })) {
132657
133299
  if (entry.name === "node_modules" || entry.name === ".git")
132658
133300
  continue;
132659
- const full = path38.join(dir, entry.name);
133301
+ const full = path37.join(dir, entry.name);
132660
133302
  if (tcIgnore.ignores(full))
132661
133303
  continue;
132662
133304
  if (entry.isDirectory()) {
@@ -132665,7 +133307,7 @@ function walkRoot(rootAbs, root, out) {
132665
133307
  }
132666
133308
  if (!entry.isFile())
132667
133309
  continue;
132668
- const ext2 = path38.extname(entry.name);
133310
+ const ext2 = path37.extname(entry.name);
132669
133311
  if (!TS_EXT.has(ext2))
132670
133312
  continue;
132671
133313
  if (root.shape === "next-app" && entry.name !== "route.ts" && entry.name !== "route.js" && entry.name !== "route.tsx" && entry.name !== "route.jsx") {
@@ -132674,11 +133316,11 @@ function walkRoot(rootAbs, root, out) {
132674
133316
  if (root.shape === "sveltekit" && !/^\+server\.(t|j)sx?$/.test(entry.name)) {
132675
133317
  continue;
132676
133318
  }
132677
- const relUnderRoot = path38.relative(rootAbs, full);
133319
+ const relUnderRoot = path37.relative(rootAbs, full);
132678
133320
  const url = deriveUrl(relUnderRoot, root);
132679
133321
  if (url === null)
132680
133322
  continue;
132681
- const source = fs33.readFileSync(full, "utf-8");
133323
+ const source = fs34.readFileSync(full, "utf-8");
132682
133324
  const lang = ext2 === ".tsx" ? "tsx" : ext2 === ".ts" ? "typescript" : "javascript";
132683
133325
  let tree;
132684
133326
  try {
@@ -132686,24 +133328,29 @@ function walkRoot(rootAbs, root, out) {
132686
133328
  } catch {
132687
133329
  continue;
132688
133330
  }
132689
- const exports = collectHttpMethodExports(tree.rootNode, source);
132690
- for (const exp of exports) {
132691
- const contract = {
132692
- protocol: "http",
132693
- method: exp.method.toUpperCase(),
132694
- path: url,
132695
- tags: [],
132696
- responses: extractResponsesFromBody(exp.body, source)
132697
- };
132698
- out.push({
132699
- identity: `${exp.method.toUpperCase()} ${url}`,
132700
- contract,
132701
- filePath: full,
132702
- declarationLine: exp.line,
132703
- observed: collectHandlerObservationsFromBody(exp.body, source),
132704
- handlerBody: exp.body,
132705
- handlerSource: source
132706
- });
133331
+ try {
133332
+ const exports = collectHttpMethodExports(tree.rootNode, source);
133333
+ for (const exp of exports) {
133334
+ const contract = {
133335
+ protocol: "http",
133336
+ method: exp.method.toUpperCase(),
133337
+ path: url,
133338
+ tags: [],
133339
+ responses: extractResponsesFromBody(exp.body, source)
133340
+ };
133341
+ const facts = extractHandlerFacts(exp.body, source);
133342
+ out.push({
133343
+ identity: `${exp.method.toUpperCase()} ${url}`,
133344
+ contract,
133345
+ filePath: full,
133346
+ declarationLine: exp.line,
133347
+ observed: collectHandlerObservationsFromBody(exp.body, source),
133348
+ emission: facts.emission,
133349
+ ownershipCheckCandidates: facts.ownershipCheckCandidates
133350
+ });
133351
+ }
133352
+ } finally {
133353
+ tree.delete();
132707
133354
  }
132708
133355
  }
132709
133356
  };
@@ -132711,7 +133358,7 @@ function walkRoot(rootAbs, root, out) {
132711
133358
  }
132712
133359
  function deriveUrl(relPath, root) {
132713
133360
  const noExt = relPath.replace(/\.(t|j)sx?$/, "");
132714
- const segments = noExt.split(path38.sep);
133361
+ const segments = noExt.split(path37.sep);
132715
133362
  switch (root.shape) {
132716
133363
  case "next-pages":
132717
133364
  return "/api/" + segmentsToUrl(segments, { stripIndex: true });
@@ -132795,6 +133442,7 @@ var init_file_based_routes = __esm({
132795
133442
  init_dist6();
132796
133443
  init_dist7();
132797
133444
  init_operation2();
133445
+ init_handler_facts();
132798
133446
  TS_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
132799
133447
  HTTP_METHODS5 = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch", "options", "head"]);
132800
133448
  ROUTE_ROOTS = [
@@ -132812,8 +133460,8 @@ var init_file_based_routes = __esm({
132812
133460
  });
132813
133461
 
132814
133462
  // packages/contract-verifier/dist/extractor/mount-graph.js
132815
- import fs34 from "node:fs";
132816
- import path39 from "node:path";
133463
+ import fs35 from "node:fs";
133464
+ import path38 from "node:path";
132817
133465
  function analyzeRouterFile(filePath, source, tree) {
132818
133466
  const declarations = [];
132819
133467
  const mounts = [];
@@ -132949,21 +133597,21 @@ function collectImports(node2, source, filePath, out) {
132949
133597
  function resolveImportPath(fromFile, spec) {
132950
133598
  if (!spec.startsWith("."))
132951
133599
  return null;
132952
- const baseDir = path39.dirname(fromFile);
133600
+ const baseDir = path38.dirname(fromFile);
132953
133601
  const noExt = spec.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
132954
133602
  const candidates = [
132955
133603
  noExt + ".ts",
132956
133604
  noExt + ".tsx",
132957
133605
  noExt + ".js",
132958
133606
  noExt + ".jsx",
132959
- path39.join(noExt, "index.ts"),
132960
- path39.join(noExt, "index.tsx"),
132961
- path39.join(noExt, "index.js"),
132962
- path39.join(noExt, "index.jsx")
133607
+ path38.join(noExt, "index.ts"),
133608
+ path38.join(noExt, "index.tsx"),
133609
+ path38.join(noExt, "index.js"),
133610
+ path38.join(noExt, "index.jsx")
132963
133611
  ];
132964
133612
  for (const c2 of candidates) {
132965
- const abs = path39.resolve(baseDir, c2);
132966
- if (fs34.existsSync(abs) && fs34.statSync(abs).isFile())
133613
+ const abs = path38.resolve(baseDir, c2);
133614
+ if (fs35.existsSync(abs) && fs35.statSync(abs).isFile())
132967
133615
  return abs;
132968
133616
  }
132969
133617
  return null;
@@ -133148,8 +133796,8 @@ var init_mount_graph = __esm({
133148
133796
  });
133149
133797
 
133150
133798
  // packages/contract-verifier/dist/extractor/auth-presence.js
133151
- import fs35 from "node:fs";
133152
- import path40 from "node:path";
133799
+ import fs36 from "node:fs";
133800
+ import path39 from "node:path";
133153
133801
  async function detectAuthPresence(rootDir) {
133154
133802
  await initParsers();
133155
133803
  const edges = [];
@@ -133159,10 +133807,10 @@ async function detectAuthPresence(rootDir) {
133159
133807
  const scanned = [];
133160
133808
  const tcIgnore = loadTcIgnore(rootDir);
133161
133809
  const visit = (dir) => {
133162
- for (const entry of fs35.readdirSync(dir, { withFileTypes: true })) {
133810
+ for (const entry of fs36.readdirSync(dir, { withFileTypes: true })) {
133163
133811
  if (entry.name === "node_modules" || entry.name === ".git")
133164
133812
  continue;
133165
- const full = path40.join(dir, entry.name);
133813
+ const full = path39.join(dir, entry.name);
133166
133814
  if (tcIgnore.ignores(full))
133167
133815
  continue;
133168
133816
  if (entry.isDirectory()) {
@@ -133171,10 +133819,10 @@ async function detectAuthPresence(rootDir) {
133171
133819
  }
133172
133820
  if (!entry.isFile())
133173
133821
  continue;
133174
- const ext2 = path40.extname(entry.name);
133822
+ const ext2 = path39.extname(entry.name);
133175
133823
  if (!TS_EXT2.has(ext2))
133176
133824
  continue;
133177
- const source = fs35.readFileSync(full, "utf-8");
133825
+ const source = fs36.readFileSync(full, "utf-8");
133178
133826
  const lang = ext2 === ".tsx" ? "tsx" : ext2 === ".ts" ? "typescript" : "javascript";
133179
133827
  let tree;
133180
133828
  try {
@@ -133182,8 +133830,12 @@ async function detectAuthPresence(rootDir) {
133182
133830
  } catch {
133183
133831
  continue;
133184
133832
  }
133185
- scanned.push(full);
133186
- collectFromTree(tree, source, full, edges, fileDefaultExports, fileImports, routerVarsByFile);
133833
+ try {
133834
+ scanned.push(full);
133835
+ collectFromTree(tree, source, full, edges, fileDefaultExports, fileImports, routerVarsByFile);
133836
+ } finally {
133837
+ tree.delete();
133838
+ }
133187
133839
  }
133188
133840
  };
133189
133841
  visit(rootDir);
@@ -133238,7 +133890,7 @@ async function detectAuthPresence(rootDir) {
133238
133890
  function resolveImportPath2(importingFile, sourceStr) {
133239
133891
  if (!sourceStr.startsWith("."))
133240
133892
  return null;
133241
- const baseRaw = path40.resolve(path40.dirname(importingFile), sourceStr);
133893
+ const baseRaw = path39.resolve(path39.dirname(importingFile), sourceStr);
133242
133894
  const candidates = [];
133243
133895
  candidates.push(baseRaw);
133244
133896
  if (baseRaw.endsWith(".js")) {
@@ -133249,14 +133901,14 @@ function resolveImportPath2(importingFile, sourceStr) {
133249
133901
  const stem = baseRaw.slice(0, -4);
133250
133902
  candidates.push(stem + ".tsx", stem + ".ts");
133251
133903
  }
133252
- if (!path40.extname(baseRaw)) {
133904
+ if (!path39.extname(baseRaw)) {
133253
133905
  for (const ext2 of [".ts", ".tsx", ".js", ".jsx"])
133254
133906
  candidates.push(baseRaw + ext2);
133255
133907
  for (const ext2 of [".ts", ".tsx", ".js", ".jsx"])
133256
- candidates.push(path40.join(baseRaw, `index${ext2}`));
133908
+ candidates.push(path39.join(baseRaw, `index${ext2}`));
133257
133909
  }
133258
133910
  for (const c2 of candidates) {
133259
- if (fs35.existsSync(c2) && fs35.statSync(c2).isFile())
133911
+ if (fs36.existsSync(c2) && fs36.statSync(c2).isFile())
133260
133912
  return c2;
133261
133913
  }
133262
133914
  return null;
@@ -133347,8 +133999,8 @@ var init_auth_presence = __esm({
133347
133999
  });
133348
134000
 
133349
134001
  // packages/contract-verifier/dist/extractor/idempotency-presence.js
133350
- import fs36 from "node:fs";
133351
- import path41 from "node:path";
134002
+ import fs37 from "node:fs";
134003
+ import path40 from "node:path";
133352
134004
  function routeKey(filePath, declarationLine) {
133353
134005
  return `${filePath}::${declarationLine}`;
133354
134006
  }
@@ -133356,14 +134008,14 @@ async function detectIdempotencyPresence(rootDir, requestHeader) {
133356
134008
  await initParsers();
133357
134009
  const headerLower = requestHeader.toLowerCase();
133358
134010
  const fileBindings = /* @__PURE__ */ new Map();
133359
- const fileTrees = /* @__PURE__ */ new Map();
134011
+ const pendingByFile = /* @__PURE__ */ new Map();
133360
134012
  const scanned = [];
133361
134013
  const tcIgnore = loadTcIgnore(rootDir);
133362
134014
  const visit = (dir) => {
133363
- for (const entry of fs36.readdirSync(dir, { withFileTypes: true })) {
134015
+ for (const entry of fs37.readdirSync(dir, { withFileTypes: true })) {
133364
134016
  if (entry.name === "node_modules" || entry.name === ".git")
133365
134017
  continue;
133366
- const full = path41.join(dir, entry.name);
134018
+ const full = path40.join(dir, entry.name);
133367
134019
  if (tcIgnore.ignores(full))
133368
134020
  continue;
133369
134021
  if (entry.isDirectory()) {
@@ -133372,10 +134024,10 @@ async function detectIdempotencyPresence(rootDir, requestHeader) {
133372
134024
  }
133373
134025
  if (!entry.isFile())
133374
134026
  continue;
133375
- const ext2 = path41.extname(entry.name);
134027
+ const ext2 = path40.extname(entry.name);
133376
134028
  if (!TS_EXT3.has(ext2))
133377
134029
  continue;
133378
- const source = fs36.readFileSync(full, "utf-8");
134030
+ const source = fs37.readFileSync(full, "utf-8");
133379
134031
  const lang = ext2 === ".tsx" ? "tsx" : ext2 === ".ts" ? "typescript" : "javascript";
133380
134032
  let tree;
133381
134033
  try {
@@ -133383,19 +134035,23 @@ async function detectIdempotencyPresence(rootDir, requestHeader) {
133383
134035
  } catch {
133384
134036
  continue;
133385
134037
  }
133386
- scanned.push(full);
133387
- fileTrees.set(full, { tree, source });
133388
- fileBindings.set(full, collectBindings(tree, source, full, headerLower));
134038
+ try {
134039
+ scanned.push(full);
134040
+ fileBindings.set(full, collectBindings(tree, source, full, headerLower));
134041
+ pendingByFile.set(full, collectPendingRoutes(tree, source, headerLower));
134042
+ } finally {
134043
+ tree.delete();
134044
+ }
133389
134045
  }
133390
134046
  };
133391
134047
  visit(rootDir);
133392
134048
  const protectedRoutes = /* @__PURE__ */ new Set();
133393
- for (const [filePath, { tree, source }] of fileTrees) {
133394
- walkRoutes(tree.rootNode, source, (declarationLine, middlewareIdents, handlerBody) => {
133395
- if (anyIdentIsAware(middlewareIdents, filePath, fileBindings) || handlerBody && nodeReadsHeader(handlerBody, source, headerLower)) {
133396
- protectedRoutes.add(routeKey(filePath, declarationLine));
134049
+ for (const [filePath, routes] of pendingByFile) {
134050
+ for (const r of routes) {
134051
+ if (r.handlerReadsHeader || anyIdentIsAware(r.middlewareIdents, filePath, fileBindings)) {
134052
+ protectedRoutes.add(routeKey(filePath, r.declarationLine));
133397
134053
  }
133398
- });
134054
+ }
133399
134055
  }
133400
134056
  return { protectedRoutes, scannedFiles: scanned };
133401
134057
  }
@@ -133465,6 +134121,17 @@ function collectBindings(tree, source, filePath, headerLower) {
133465
134121
  visit(tree.rootNode);
133466
134122
  return { imports, awareLocally, declaredFunctions };
133467
134123
  }
134124
+ function collectPendingRoutes(tree, source, headerLower) {
134125
+ const out = [];
134126
+ walkRoutes(tree.rootNode, source, (declarationLine, middlewareIdents, handlerBody) => {
134127
+ out.push({
134128
+ declarationLine,
134129
+ middlewareIdents,
134130
+ handlerReadsHeader: !!handlerBody && nodeReadsHeader(handlerBody, source, headerLower)
134131
+ });
134132
+ });
134133
+ return out;
134134
+ }
133468
134135
  function nodeReadsHeader(node2, source, headerLower) {
133469
134136
  const target = headerLower;
133470
134137
  let found = false;
@@ -133573,7 +134240,7 @@ function anyIdentIsAware(idents, filePath, fileBindings) {
133573
134240
  function resolveImportPath3(importingFile, sourceStr) {
133574
134241
  if (!sourceStr.startsWith("."))
133575
134242
  return null;
133576
- const baseRaw = path41.resolve(path41.dirname(importingFile), sourceStr);
134243
+ const baseRaw = path40.resolve(path40.dirname(importingFile), sourceStr);
133577
134244
  const candidates = [];
133578
134245
  candidates.push(baseRaw);
133579
134246
  if (baseRaw.endsWith(".js")) {
@@ -133584,14 +134251,14 @@ function resolveImportPath3(importingFile, sourceStr) {
133584
134251
  const stem = baseRaw.slice(0, -4);
133585
134252
  candidates.push(stem + ".tsx", stem + ".ts");
133586
134253
  }
133587
- if (!path41.extname(baseRaw)) {
134254
+ if (!path40.extname(baseRaw)) {
133588
134255
  for (const ext2 of [".ts", ".tsx", ".js", ".jsx"])
133589
134256
  candidates.push(baseRaw + ext2);
133590
134257
  for (const ext2 of [".ts", ".tsx", ".js", ".jsx"])
133591
- candidates.push(path41.join(baseRaw, `index${ext2}`));
134258
+ candidates.push(path40.join(baseRaw, `index${ext2}`));
133592
134259
  }
133593
134260
  for (const c2 of candidates) {
133594
- if (fs36.existsSync(c2) && fs36.statSync(c2).isFile())
134261
+ if (fs37.existsSync(c2) && fs37.statSync(c2).isFile())
133595
134262
  return c2;
133596
134263
  }
133597
134264
  return null;
@@ -135661,6 +136328,51 @@ function walk6(node2, visit) {
135661
136328
  walk6(c2, visit);
135662
136329
  }
135663
136330
  }
136331
+ function extractIdLiteralFromExport(_filePath, _source, tree) {
136332
+ for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
136333
+ const node2 = tree.rootNode.namedChild(i);
136334
+ if (!node2 || node2.type !== "export_statement")
136335
+ continue;
136336
+ for (let j2 = 0; j2 < node2.namedChildCount; j2++) {
136337
+ const child = node2.namedChild(j2);
136338
+ if (!child)
136339
+ continue;
136340
+ const id = extractIdFromObjectLike(child);
136341
+ if (id !== null)
136342
+ return id;
136343
+ }
136344
+ }
136345
+ return null;
136346
+ }
136347
+ function extractIdFromObjectLike(node2) {
136348
+ if (node2.type === "object") {
136349
+ return extractIdProperty(node2);
136350
+ }
136351
+ if (node2.type === "call_expression") {
136352
+ for (const arg of collectArgs4(node2)) {
136353
+ if (arg.type === "object") {
136354
+ const id = extractIdProperty(arg);
136355
+ if (id !== null)
136356
+ return id;
136357
+ }
136358
+ }
136359
+ }
136360
+ return null;
136361
+ }
136362
+ function extractIdProperty(obj) {
136363
+ for (let i = 0; i < obj.namedChildCount; i++) {
136364
+ const pair = obj.namedChild(i);
136365
+ if (pair?.type !== "pair")
136366
+ continue;
136367
+ const key2 = pair.childForFieldName("key");
136368
+ if (key2?.text !== "id")
136369
+ continue;
136370
+ const value = pair.childForFieldName("value");
136371
+ if (value?.type === "string")
136372
+ return value.text.slice(1, -1);
136373
+ }
136374
+ return null;
136375
+ }
135664
136376
  var ENUM_CONVENTION_NAME, ENUM_CONVENTION_SUFFIX;
135665
136377
  var init_ts_enums = __esm({
135666
136378
  "packages/contract-verifier/dist/extractor/enum/ts-enums.js"() {
@@ -135695,7 +136407,10 @@ function extractEnumClass(node2, filePath, source) {
135695
136407
  if (!name)
135696
136408
  return null;
135697
136409
  const supers = node2.childForFieldName("superclasses");
135698
- if (!supers || !superclassesLookEnum(supers, source))
136410
+ if (!supers)
136411
+ return null;
136412
+ const isAutoEnum = superclassesHaveAutoEnum(supers, source);
136413
+ if (!superclassesLookEnum(supers, source) && !isAutoEnum)
135699
136414
  return null;
135700
136415
  const body = node2.childForFieldName("body");
135701
136416
  if (!body)
@@ -135708,17 +136423,31 @@ function extractEnumClass(node2, filePath, source) {
135708
136423
  const assign = stmt.namedChild(0);
135709
136424
  if (assign?.type !== "assignment")
135710
136425
  continue;
136426
+ const left = assign.childForFieldName("left");
135711
136427
  const right = assign.childForFieldName("right");
135712
136428
  if (right?.type === "string") {
135713
136429
  const v = stringValue(right, source);
135714
136430
  if (v !== null)
135715
136431
  values.push(v);
136432
+ } else if (isAutoEnum && right?.type === "call" && left?.type === "identifier") {
136433
+ values.push(source.slice(left.startIndex, left.endIndex));
135716
136434
  }
135717
136435
  }
135718
136436
  if (values.length === 0)
135719
136437
  return null;
135720
136438
  return mkEnum2(name, values, "py-enum", node2, filePath);
135721
136439
  }
136440
+ function superclassesHaveAutoEnum(supers, source) {
136441
+ for (let i = 0; i < supers.namedChildCount; i++) {
136442
+ const c2 = supers.namedChild(i);
136443
+ if (!c2)
136444
+ continue;
136445
+ const text = source.slice(c2.startIndex, c2.endIndex);
136446
+ if (/(^|\.)AutoEnum$/.test(text))
136447
+ return true;
136448
+ }
136449
+ return false;
136450
+ }
135722
136451
  function superclassesLookEnum(supers, source) {
135723
136452
  for (let i = 0; i < supers.namedChildCount; i++) {
135724
136453
  const c2 = supers.namedChild(i);
@@ -135835,13 +136564,35 @@ var init_py_enums = __esm({
135835
136564
  });
135836
136565
 
135837
136566
  // packages/contract-verifier/dist/extractor/enum/index.js
136567
+ import path41 from "node:path";
135838
136568
  async function extractEnumsFromDir(rootDir) {
135839
136569
  const raw = [];
136570
+ const siblingIds = /* @__PURE__ */ new Map();
135840
136571
  await eachParsedSource(rootDir, (s) => {
135841
136572
  const matcher = MATCHERS2[s.lang];
135842
136573
  if (matcher)
135843
136574
  raw.push(...matcher(s));
136575
+ const base = path41.basename(s.filePath);
136576
+ if ((base === "index.ts" || base === "index.js") && s.lang === "typescript") {
136577
+ const idVal = extractIdLiteralFromExport(s.filePath, s.source, s.tree);
136578
+ if (idVal !== null) {
136579
+ const groupDir = path41.dirname(path41.dirname(s.filePath));
136580
+ const list = siblingIds.get(groupDir) ?? [];
136581
+ list.push({ value: idVal, filePath: s.filePath });
136582
+ siblingIds.set(groupDir, list);
136583
+ }
136584
+ }
135844
136585
  });
136586
+ for (const [groupDir, entries] of siblingIds) {
136587
+ if (entries.length < 3)
136588
+ continue;
136589
+ raw.push({
136590
+ name: path41.basename(groupDir),
136591
+ values: [...new Set(entries.map((e) => e.value))].sort(),
136592
+ shape: "sibling-id-literal",
136593
+ source: { filePath: entries[0].filePath, lineStart: 1, lineEnd: 1 }
136594
+ });
136595
+ }
135845
136596
  const seen = /* @__PURE__ */ new Map();
135846
136597
  for (const e of raw) {
135847
136598
  const key2 = `${e.name}|${e.values.join(",")}`;
@@ -135890,7 +136641,7 @@ var init_minimatch = __esm({
135890
136641
  });
135891
136642
 
135892
136643
  // packages/contract-verifier/dist/extractor/manifests.js
135893
- import fs37 from "node:fs";
136644
+ import fs38 from "node:fs";
135894
136645
  import path42 from "node:path";
135895
136646
  function collectDependencies(rootDir) {
135896
136647
  const out = [];
@@ -135898,7 +136649,7 @@ function collectDependencies(rootDir) {
135898
136649
  const visit = (dir) => {
135899
136650
  let entries;
135900
136651
  try {
135901
- entries = fs37.readdirSync(dir, { withFileTypes: true });
136652
+ entries = fs38.readdirSync(dir, { withFileTypes: true });
135902
136653
  } catch {
135903
136654
  return;
135904
136655
  }
@@ -135930,7 +136681,7 @@ function collectDependencies(rootDir) {
135930
136681
  function readPackageJson(filePath) {
135931
136682
  let pkg;
135932
136683
  try {
135933
- pkg = JSON.parse(fs37.readFileSync(filePath, "utf-8"));
136684
+ pkg = JSON.parse(fs38.readFileSync(filePath, "utf-8"));
135934
136685
  } catch {
135935
136686
  return [];
135936
136687
  }
@@ -135955,7 +136706,7 @@ function pyDistName(spec) {
135955
136706
  function readRequirements(filePath) {
135956
136707
  let text;
135957
136708
  try {
135958
- text = fs37.readFileSync(filePath, "utf-8");
136709
+ text = fs38.readFileSync(filePath, "utf-8");
135959
136710
  } catch {
135960
136711
  return [];
135961
136712
  }
@@ -135970,7 +136721,7 @@ function readRequirements(filePath) {
135970
136721
  function readPyproject(filePath) {
135971
136722
  let text;
135972
136723
  try {
135973
- text = fs37.readFileSync(filePath, "utf-8");
136724
+ text = fs38.readFileSync(filePath, "utf-8");
135974
136725
  } catch {
135975
136726
  return [];
135976
136727
  }
@@ -135995,7 +136746,7 @@ function readPyproject(filePath) {
135995
136746
  function readSetupPy(filePath) {
135996
136747
  let text;
135997
136748
  try {
135998
- text = fs37.readFileSync(filePath, "utf-8");
136749
+ text = fs38.readFileSync(filePath, "utf-8");
135999
136750
  } catch {
136000
136751
  return [];
136001
136752
  }
@@ -136034,7 +136785,7 @@ var init_manifests = __esm({
136034
136785
  });
136035
136786
 
136036
136787
  // packages/contract-verifier/dist/extractor/forbidden/index.js
136037
- import fs38 from "node:fs";
136788
+ import fs39 from "node:fs";
136038
136789
  import path43 from "node:path";
136039
136790
  function detectForbiddenFiles(rootDir, pattern) {
136040
136791
  const out = [];
@@ -136042,7 +136793,7 @@ function detectForbiddenFiles(rootDir, pattern) {
136042
136793
  const visit = (dir) => {
136043
136794
  let entries;
136044
136795
  try {
136045
- entries = fs38.readdirSync(dir, { withFileTypes: true });
136796
+ entries = fs39.readdirSync(dir, { withFileTypes: true });
136046
136797
  } catch {
136047
136798
  return;
136048
136799
  }
@@ -136220,7 +136971,7 @@ function detectFlagInConfigFiles(rootDir, name) {
136220
136971
  const visit = (dir) => {
136221
136972
  let entries;
136222
136973
  try {
136223
- entries = fs38.readdirSync(dir, { withFileTypes: true });
136974
+ entries = fs39.readdirSync(dir, { withFileTypes: true });
136224
136975
  } catch {
136225
136976
  return;
136226
136977
  }
@@ -136239,7 +136990,7 @@ function detectFlagInConfigFiles(rootDir, name) {
136239
136990
  continue;
136240
136991
  let source;
136241
136992
  try {
136242
- source = fs38.readFileSync(full, "utf-8");
136993
+ source = fs39.readFileSync(full, "utf-8");
136243
136994
  } catch {
136244
136995
  continue;
136245
136996
  }
@@ -136279,6 +137030,7 @@ var init_forbidden = __esm({
136279
137030
  // packages/contract-verifier/dist/extractor/constant/ts-constants.js
136280
137031
  function extractConstantsFromFile(filePath, source, tree) {
136281
137032
  const out = [];
137033
+ const seenWindowGlobals = /* @__PURE__ */ new Set();
136282
137034
  walk9(tree.rootNode, (node2) => {
136283
137035
  if (node2.type === "variable_declarator") {
136284
137036
  const result = extractDeclarator(node2, filePath, source);
@@ -136291,6 +137043,22 @@ function extractConstantsFromFile(filePath, source, tree) {
136291
137043
  out.push(result);
136292
137044
  return true;
136293
137045
  }
137046
+ if (node2.type === "member_expression") {
137047
+ const objNode = node2.childForFieldName("object");
137048
+ const propNode = node2.childForFieldName("property");
137049
+ if (objNode?.text === "window" && propNode?.type === "property_identifier") {
137050
+ const propName = propNode.text;
137051
+ if (!seenWindowGlobals.has(propName)) {
137052
+ seenWindowGlobals.add(propName);
137053
+ out.push({
137054
+ name: propName,
137055
+ value: void 0,
137056
+ shape: "window-global",
137057
+ source: mkLoc(node2, filePath)
137058
+ });
137059
+ }
137060
+ }
137061
+ }
136294
137062
  return true;
136295
137063
  });
136296
137064
  return out;
@@ -136330,6 +137098,12 @@ function extractDeclarator(node2, filePath, source) {
136330
137098
  shape: "object-property",
136331
137099
  source: mkLoc(kNode, filePath)
136332
137100
  });
137101
+ out.push({
137102
+ name: `${name}.${k}`,
137103
+ value: v,
137104
+ shape: "object-property",
137105
+ source: mkLoc(kNode, filePath)
137106
+ });
136333
137107
  }
136334
137108
  return out;
136335
137109
  }
@@ -136665,13 +137439,13 @@ var init_constant = __esm({
136665
137439
  });
136666
137440
 
136667
137441
  // packages/contract-verifier/dist/extractor/effect/index.js
136668
- function isCall(n) {
137442
+ function isCall2(n) {
136669
137443
  return n.type === "call_expression" || n.type === "call";
136670
137444
  }
136671
- function isMember(n) {
137445
+ function isMember2(n) {
136672
137446
  return n?.type === "member_expression" || n?.type === "attribute";
136673
137447
  }
136674
- function memberProp(n, source) {
137448
+ function memberProp2(n, source) {
136675
137449
  const p2 = n.childForFieldName("property") ?? n.childForFieldName("attribute");
136676
137450
  return p2 ? source.slice(p2.startIndex, p2.endIndex) : "";
136677
137451
  }
@@ -136679,7 +137453,7 @@ function memberObject(n, source) {
136679
137453
  const o = n.childForFieldName("object");
136680
137454
  return o ? source.slice(o.startIndex, o.endIndex) : "";
136681
137455
  }
136682
- function strVal(n, source) {
137456
+ function strVal2(n, source) {
136683
137457
  if (n.type !== "string")
136684
137458
  return null;
136685
137459
  const frag = n.namedChildren.find((c2) => c2.type === "string_fragment" || c2.type === "string_content");
@@ -136688,19 +137462,19 @@ function strVal(n, source) {
136688
137462
  function matchEffects(s) {
136689
137463
  const out = [];
136690
137464
  const visit = (node2) => {
136691
- if (isCall(node2)) {
137465
+ if (isCall2(node2)) {
136692
137466
  const fn = node2.childForFieldName("function");
136693
137467
  const args = node2.childForFieldName("arguments");
136694
137468
  if (fn && args) {
136695
137469
  let channel = null;
136696
- if (isMember(fn) && memberProp(fn, s.source) === "emit") {
137470
+ if (isMember2(fn) && memberProp2(fn, s.source) === "emit") {
136697
137471
  channel = memberObject(fn, s.source) || "event-bus";
136698
137472
  } else if (fn.type === "identifier" && /^emit/i.test(s.source.slice(fn.startIndex, fn.endIndex))) {
136699
137473
  channel = "event-bus";
136700
137474
  }
136701
137475
  if (channel !== null) {
136702
137476
  const first2 = args.namedChild(0);
136703
- const event = first2 ? strVal(first2, s.source) : null;
137477
+ const event = first2 ? strVal2(first2, s.source) : null;
136704
137478
  if (event) {
136705
137479
  out.push({
136706
137480
  event,
@@ -136747,255 +137521,16 @@ var init_effect = __esm({
136747
137521
  function extractEmissionFacts(ops) {
136748
137522
  const facts = /* @__PURE__ */ new Map();
136749
137523
  for (const op of ops) {
136750
- if (!op.handlerBody || !op.handlerSource)
137524
+ if (!op.emission)
136751
137525
  continue;
136752
- const body = op.handlerBody;
136753
- const source = op.handlerSource;
136754
- const callMap = collectEmitCalls(body, source);
136755
- const failureEmitSites = [];
136756
- for (const [event, calls] of callMap) {
136757
- for (const call of calls) {
136758
- if (emitIsInFailureBlock(call, source)) {
136759
- failureEmitSites.push({
136760
- event,
136761
- lineStart: call.startPosition.row + 1,
136762
- lineEnd: call.endPosition.row + 1
136763
- });
136764
- }
136765
- }
136766
- }
136767
137526
  facts.set(op.identity, {
136768
137527
  filePath: op.filePath,
136769
137528
  declarationLine: op.declarationLine,
136770
- staticEvents: new Set(callMap.keys()),
136771
- hasDynamicEmit: handlerHasDynamicEmit(body, source),
136772
- failureEmitSites,
136773
- branchEmits: collectBranchEmits(body, source)
137529
+ ...op.emission
136774
137530
  });
136775
137531
  }
136776
137532
  return facts;
136777
137533
  }
136778
- function isCall2(n) {
136779
- return n.type === "call_expression" || n.type === "call";
136780
- }
136781
- function isMember2(n) {
136782
- return n?.type === "member_expression" || n?.type === "attribute";
136783
- }
136784
- function memberProp2(n, source) {
136785
- const p2 = n.childForFieldName("property") ?? n.childForFieldName("attribute");
136786
- return p2 ? source.slice(p2.startIndex, p2.endIndex) : "";
136787
- }
136788
- function isBlock(n) {
136789
- return n.type === "statement_block" || n.type === "block";
136790
- }
136791
- function isFnBoundary(n) {
136792
- return n.type === "function_declaration" || n.type === "arrow_function" || n.type === "function_expression" || n.type === "function_definition";
136793
- }
136794
- function isControlFlow(n) {
136795
- return /^(if_statement|switch_statement|try_statement|for_statement|for_in_statement|while_statement|do_statement)$/.test(n.type);
136796
- }
136797
- function strVal2(n, source) {
136798
- if (n.type !== "string")
136799
- return null;
136800
- const frag = n.namedChildren.find((c2) => c2.type === "string_fragment" || c2.type === "string_content");
136801
- return frag ? source.slice(frag.startIndex, frag.endIndex) : null;
136802
- }
136803
- function isEmitFn(fn, source) {
136804
- if (isMember2(fn))
136805
- return memberProp2(fn, source) === "emit";
136806
- if (fn.type === "identifier")
136807
- return /^emit/i.test(source.slice(fn.startIndex, fn.endIndex));
136808
- return false;
136809
- }
136810
- function collectEmitCalls(body, source) {
136811
- const out = /* @__PURE__ */ new Map();
136812
- const visit = (node2) => {
136813
- if (isCall2(node2)) {
136814
- const fn = node2.childForFieldName("function");
136815
- const args = node2.childForFieldName("arguments");
136816
- if (fn && args && isEmitFn(fn, source)) {
136817
- const first2 = args.namedChild(0);
136818
- const eventName = first2 ? strVal2(first2, source) : null;
136819
- if (eventName !== null) {
136820
- const arr = out.get(eventName) ?? [];
136821
- arr.push(node2);
136822
- out.set(eventName, arr);
136823
- }
136824
- }
136825
- }
136826
- for (const child of node2.namedChildren)
136827
- visit(child);
136828
- };
136829
- visit(body);
136830
- return out;
136831
- }
136832
- function handlerHasDynamicEmit(body, source) {
136833
- let dynamic = false;
136834
- const visit = (node2) => {
136835
- if (dynamic)
136836
- return;
136837
- if (isCall2(node2)) {
136838
- const fn = node2.childForFieldName("function");
136839
- const args = node2.childForFieldName("arguments");
136840
- if (fn && args && isEmitFn(fn, source)) {
136841
- const first2 = args.namedChild(0);
136842
- if (first2 && first2.type !== "string") {
136843
- dynamic = true;
136844
- return;
136845
- }
136846
- }
136847
- }
136848
- for (const child of node2.namedChildren) {
136849
- visit(child);
136850
- if (dynamic)
136851
- return;
136852
- }
136853
- };
136854
- visit(body);
136855
- return dynamic;
136856
- }
136857
- function emitIsInFailureBlock(emitCall, source) {
136858
- let cur = emitCall.parent;
136859
- while (cur) {
136860
- if (isBlock(cur))
136861
- return blockContainsFailureStatusShallow(cur, source);
136862
- if (isFnBoundary(cur))
136863
- break;
136864
- cur = cur.parent;
136865
- }
136866
- return false;
136867
- }
136868
- function blockContainsFailureStatusShallow(block, source) {
136869
- for (const stmt of block.namedChildren) {
136870
- if (containsTopLevelFailureStatus(stmt, source))
136871
- return true;
136872
- }
136873
- return false;
136874
- }
136875
- function containsTopLevelFailureStatus(stmt, source) {
136876
- if (isControlFlow(stmt))
136877
- return false;
136878
- let found = false;
136879
- const visit = (node2) => {
136880
- if (found)
136881
- return;
136882
- if (isControlFlow(node2))
136883
- return;
136884
- if (isCall2(node2) && callEmitsFailureStatus(node2, source)) {
136885
- found = true;
136886
- return;
136887
- }
136888
- for (const child of node2.namedChildren) {
136889
- visit(child);
136890
- if (found)
136891
- return;
136892
- }
136893
- };
136894
- visit(stmt);
136895
- return found;
136896
- }
136897
- function callEmitsFailureStatus(call, source) {
136898
- const fn = call.childForFieldName("function");
136899
- if (!fn)
136900
- return false;
136901
- const args = call.childForFieldName("arguments");
136902
- if (!args)
136903
- return false;
136904
- if (isMember2(fn) && memberProp2(fn, source) === "status") {
136905
- const arg = args.namedChild(0);
136906
- return !!arg && arg.type === "number" && /^[45]\d{2}$/.test(source.slice(arg.startIndex, arg.endIndex));
136907
- }
136908
- const fnName = fn.type === "identifier" ? source.slice(fn.startIndex, fn.endIndex) : isMember2(fn) ? memberProp2(fn, source) : "";
136909
- if (fnName === "JSONResponse" || fnName === "HTTPException" || fnName === "Response") {
136910
- for (let i = 0; i < args.namedChildCount; i++) {
136911
- const a = args.namedChild(i);
136912
- if (a?.type === "keyword_argument") {
136913
- const name = a.childForFieldName("name");
136914
- const value = a.childForFieldName("value");
136915
- if (name && value && source.slice(name.startIndex, name.endIndex) === "status_code" && value.type === "integer" && /^[45]\d{2}$/.test(source.slice(value.startIndex, value.endIndex))) {
136916
- return true;
136917
- }
136918
- }
136919
- }
136920
- const first2 = args.namedChild(0);
136921
- if (first2?.type === "integer" && /^[45]\d{2}$/.test(source.slice(first2.startIndex, first2.endIndex)))
136922
- return true;
136923
- }
136924
- return false;
136925
- }
136926
- function collectBranchEmits(body, source) {
136927
- const out = /* @__PURE__ */ new Map();
136928
- const visit = (node2) => {
136929
- if (node2.type === "if_statement") {
136930
- const cond = node2.childForFieldName("condition");
136931
- const consequent = node2.childForFieldName("consequence");
136932
- const alternative = node2.childForFieldName("alternative");
136933
- const literal = cond ? conditionLiteral(cond, source) : null;
136934
- if (literal !== null && consequent && !out.has(literal)) {
136935
- out.set(literal, {
136936
- consequentEmits: blockHasAnyEmit(consequent, source),
136937
- alternativeEmits: alternative ? blockHasAnyEmit(alternative, source) : false
136938
- });
136939
- }
136940
- }
136941
- for (const child of node2.namedChildren)
136942
- visit(child);
136943
- };
136944
- visit(body);
136945
- return out;
136946
- }
136947
- function conditionLiteral(node2, source) {
136948
- const u2 = unwrapParens(node2);
136949
- if (u2.type === "binary_expression") {
136950
- const opNode = u2.childForFieldName("operator");
136951
- const op = opNode ? source.slice(opNode.startIndex, opNode.endIndex) : "";
136952
- if (op !== "===" && op !== "==")
136953
- return null;
136954
- return litText(u2.childForFieldName("left"), source) ?? litText(u2.childForFieldName("right"), source);
136955
- }
136956
- if (u2.type === "comparison_operator") {
136957
- const a = u2.namedChild(0);
136958
- const b = u2.namedChild(1);
136959
- if (!a || !b || source.slice(a.endIndex, b.startIndex).trim() !== "==")
136960
- return null;
136961
- return litText(a, source) ?? litText(b, source);
136962
- }
136963
- return null;
136964
- }
136965
- function litText(node2, source) {
136966
- return node2 && node2.type === "string" ? strVal2(node2, source) : null;
136967
- }
136968
- function unwrapParens(node2) {
136969
- let cur = node2;
136970
- while (cur.type === "parenthesized_expression") {
136971
- const child = cur.namedChildren[0];
136972
- if (!child)
136973
- break;
136974
- cur = child;
136975
- }
136976
- return cur;
136977
- }
136978
- function blockHasAnyEmit(block, source) {
136979
- let found = false;
136980
- const visit = (node2) => {
136981
- if (found)
136982
- return;
136983
- if (isCall2(node2)) {
136984
- const fn = node2.childForFieldName("function");
136985
- if (fn && isEmitFn(fn, source)) {
136986
- found = true;
136987
- return;
136988
- }
136989
- }
136990
- for (const child of node2.namedChildren) {
136991
- visit(child);
136992
- if (found)
136993
- return;
136994
- }
136995
- };
136996
- visit(block);
136997
- return found;
136998
- }
136999
137534
  var init_emission_facts = __esm({
137000
137535
  "packages/contract-verifier/dist/extractor/effect/emission-facts.js"() {
137001
137536
  "use strict";
@@ -137003,7 +137538,7 @@ var init_emission_facts = __esm({
137003
137538
  });
137004
137539
 
137005
137540
  // packages/contract-verifier/dist/extractor/entity-schema/index.js
137006
- import fs39 from "node:fs";
137541
+ import fs40 from "node:fs";
137007
137542
  import path44 from "node:path";
137008
137543
  async function extractEntitiesFromDir(rootDir) {
137009
137544
  const tcIgnore = loadTcIgnore(rootDir);
@@ -137011,7 +137546,7 @@ async function extractEntitiesFromDir(rootDir) {
137011
137546
  const walk11 = (dir) => {
137012
137547
  let entries;
137013
137548
  try {
137014
- entries = fs39.readdirSync(dir, { withFileTypes: true });
137549
+ entries = fs40.readdirSync(dir, { withFileTypes: true });
137015
137550
  } catch {
137016
137551
  return;
137017
137552
  }
@@ -137024,7 +137559,7 @@ async function extractEntitiesFromDir(rootDir) {
137024
137559
  if (e.isDirectory())
137025
137560
  walk11(full);
137026
137561
  else if (e.isFile() && e.name.endsWith(".prisma")) {
137027
- out.push(...parsePrismaModels(full, fs39.readFileSync(full, "utf-8")));
137562
+ out.push(...parsePrismaModels(full, fs40.readFileSync(full, "utf-8")));
137028
137563
  }
137029
137564
  }
137030
137565
  };
@@ -137568,7 +138103,7 @@ async function extractFormulaFacts(rootDir, targetField) {
137568
138103
  return result;
137569
138104
  }
137570
138105
  function candidateNames(fieldName) {
137571
- const camel = fieldName.replace(/_([a-z])/g, (_, c2) => c2.toUpperCase());
138106
+ const camel = fieldName.replace(/_([a-z])/g, (_2, c2) => c2.toUpperCase());
137572
138107
  const snake = fieldName.replace(/[A-Z]/g, (c2) => `_${c2.toLowerCase()}`);
137573
138108
  const names = /* @__PURE__ */ new Set();
137574
138109
  for (const base of /* @__PURE__ */ new Set([fieldName, camel, snake])) {
@@ -138119,7 +138654,7 @@ var init_characteristic_imports = __esm({
138119
138654
  });
138120
138655
 
138121
138656
  // packages/contract-verifier/dist/extractor/architecture/shared/config-files.js
138122
- import fs40 from "node:fs";
138657
+ import fs41 from "node:fs";
138123
138658
  import path45 from "node:path";
138124
138659
  function collectFileIndex(rootDir) {
138125
138660
  const files = [];
@@ -138128,7 +138663,7 @@ function collectFileIndex(rootDir) {
138128
138663
  const visit = (dir) => {
138129
138664
  let entries;
138130
138665
  try {
138131
- entries = fs40.readdirSync(dir, { withFileTypes: true });
138666
+ entries = fs41.readdirSync(dir, { withFileTypes: true });
138132
138667
  } catch {
138133
138668
  return;
138134
138669
  }
@@ -138154,7 +138689,7 @@ function collectFileIndex(rootDir) {
138154
138689
  return cache2.get(relPath);
138155
138690
  let text;
138156
138691
  try {
138157
- text = fs40.readFileSync(path45.join(rootDir, relPath), "utf-8");
138692
+ text = fs41.readFileSync(path45.join(rootDir, relPath), "utf-8");
138158
138693
  } catch {
138159
138694
  text = null;
138160
138695
  }
@@ -138607,15 +139142,24 @@ __export(extractor_exports, {
138607
139142
  extractStateMachineFacts: () => extractStateMachineFacts,
138608
139143
  getArchitectureDetector: () => getArchitectureDetector
138609
139144
  });
138610
- import fs41 from "node:fs";
139145
+ import fs42 from "node:fs";
138611
139146
  import path47 from "node:path";
139147
+ function isTestFile2(filePath) {
139148
+ const parts = filePath.replace(/\\/g, "/").split("/");
139149
+ const fileName = parts[parts.length - 1];
139150
+ if (/\.(test|spec)\.(t|j)sx?$/.test(fileName))
139151
+ return true;
139152
+ if (parts.includes("__tests__"))
139153
+ return true;
139154
+ return false;
139155
+ }
138612
139156
  async function extractOperationsFromDir(rootDir) {
138613
139157
  await initParsers();
138614
139158
  const rawOps = [];
138615
139159
  const fileAnalyses = [];
138616
139160
  const tcIgnore = loadTcIgnore(rootDir);
138617
139161
  const visit = (dir) => {
138618
- for (const entry of fs41.readdirSync(dir, { withFileTypes: true })) {
139162
+ for (const entry of fs42.readdirSync(dir, { withFileTypes: true })) {
138619
139163
  if (entry.name === "node_modules" || entry.name === ".git")
138620
139164
  continue;
138621
139165
  const full = path47.join(dir, entry.name);
@@ -138630,13 +139174,19 @@ async function extractOperationsFromDir(rootDir) {
138630
139174
  const ext2 = path47.extname(entry.name);
138631
139175
  if (!TS_EXT4.has(ext2))
138632
139176
  continue;
138633
- const source = fs41.readFileSync(full, "utf-8");
139177
+ if (isTestFile2(full))
139178
+ continue;
139179
+ const source = fs42.readFileSync(full, "utf-8");
138634
139180
  const lang = ext2 === ".ts" || ext2 === ".tsx" ? ext2 === ".tsx" ? "tsx" : "typescript" : "javascript";
139181
+ let tree;
138635
139182
  try {
138636
- const tree = parseFile(full, source, lang);
139183
+ tree = parseFile(full, source, lang);
138637
139184
  rawOps.push(...extractOperationsFromFile(full, source, tree));
139185
+ rawOps.push(...extractPluginStyleRoutesFromFile(full, source, tree));
138638
139186
  fileAnalyses.push(analyzeRouterFile(full, source, tree));
138639
139187
  } catch {
139188
+ } finally {
139189
+ tree?.delete();
138640
139190
  }
138641
139191
  }
138642
139192
  };
@@ -138702,6 +139252,8 @@ function compareOperation(input) {
138702
139252
  const codeByStatus = /* @__PURE__ */ new Map();
138703
139253
  for (const r of input.code.responses)
138704
139254
  codeByStatus.set(r.status, r);
139255
+ if (input.code.responses.length === 0)
139256
+ return out;
138705
139257
  for (const specResp of input.spec.responses) {
138706
139258
  if (specResp.inheritedFrom)
138707
139259
  continue;
@@ -139090,6 +139642,8 @@ function compareAuthRequirement(input) {
139090
139642
  continue;
139091
139643
  if (!matchesOperation2(input.contract.selector, op, specOp))
139092
139644
  continue;
139645
+ if (op.authExempt)
139646
+ continue;
139093
139647
  if (input.protectedFiles.has(op.filePath))
139094
139648
  continue;
139095
139649
  if (input.contract.except?.some((ex) => matchesOperation2(ex, op, specOp)))
@@ -139186,9 +139740,9 @@ function compareAuthorizationRule(input) {
139186
139740
  for (const op of input.recognizedOps) {
139187
139741
  if (!targets.has(op.identity))
139188
139742
  continue;
139189
- if (!op.handlerBody || !op.handlerSource)
139743
+ if (!op.ownershipCheckCandidates)
139190
139744
  continue;
139191
- if (handlerHasOwnershipCheck(op.handlerBody, op.handlerSource, fieldName))
139745
+ if (op.ownershipCheckCandidates.some((c2) => c2.resourceField === fieldName))
139192
139746
  continue;
139193
139747
  out.push({
139194
139748
  id: randomUUID13(),
@@ -139212,79 +139766,6 @@ function parseResourceField(predicate) {
139212
139766
  return m[1];
139213
139767
  return null;
139214
139768
  }
139215
- function handlerHasOwnershipCheck(body, source, fieldName) {
139216
- let found = false;
139217
- const check = (left, right) => (matchesAuthSide(left, source) || matchesAuthSide(right, source)) && (matchesResourceField(left, source, fieldName) || matchesResourceField(right, source, fieldName));
139218
- const visit = (node2) => {
139219
- if (found)
139220
- return;
139221
- if (node2.type === "binary_expression") {
139222
- const opNode = node2.childForFieldName("operator");
139223
- const op = opNode ? source.slice(opNode.startIndex, opNode.endIndex) : "";
139224
- if (op === "===" || op === "==" || op === "!==" || op === "!=") {
139225
- const left = node2.childForFieldName("left");
139226
- const right = node2.childForFieldName("right");
139227
- if (left && right && check(left, right)) {
139228
- found = true;
139229
- return;
139230
- }
139231
- }
139232
- }
139233
- if (node2.type === "comparison_operator") {
139234
- const a = node2.namedChild(0);
139235
- const b = node2.namedChild(1);
139236
- if (a && b) {
139237
- const op = source.slice(a.endIndex, b.startIndex).trim();
139238
- if ((op === "==" || op === "!=") && check(a, b)) {
139239
- found = true;
139240
- return;
139241
- }
139242
- }
139243
- }
139244
- for (const child of node2.namedChildren) {
139245
- visit(child);
139246
- if (found)
139247
- return;
139248
- }
139249
- };
139250
- visit(body);
139251
- return found;
139252
- }
139253
- function matchesAuthSide(node2, source) {
139254
- if (node2.type === "attribute") {
139255
- return /\b(req|request)\.(auth|user)\b|\bcurrent_user\b/.test(source.slice(node2.startIndex, node2.endIndex));
139256
- }
139257
- let cur = node2;
139258
- while (cur) {
139259
- if (cur.type === "identifier") {
139260
- const text = source.slice(cur.startIndex, cur.endIndex);
139261
- if (text === "req" || text === "request")
139262
- return false;
139263
- return false;
139264
- }
139265
- if (cur.type === "member_expression") {
139266
- const text = source.slice(cur.startIndex, cur.endIndex);
139267
- if (/\b(req|request)\.auth\b|\b(req|request)\?\.auth\b/.test(text))
139268
- return true;
139269
- cur = cur.childForFieldName("object");
139270
- continue;
139271
- }
139272
- if (cur.type === "optional_chain_expression" || cur.type === "subscript_expression") {
139273
- cur = cur.childForFieldName("object");
139274
- continue;
139275
- }
139276
- return false;
139277
- }
139278
- return false;
139279
- }
139280
- function matchesResourceField(node2, source, fieldName) {
139281
- if (node2.type !== "member_expression" && node2.type !== "attribute")
139282
- return false;
139283
- const prop = node2.childForFieldName("property") ?? node2.childForFieldName("attribute");
139284
- if (!prop)
139285
- return false;
139286
- return source.slice(prop.startIndex, prop.endIndex) === fieldName;
139287
- }
139288
139769
  var init_authorization_rule2 = __esm({
139289
139770
  "packages/contract-verifier/dist/comparator/authorization-rule.js"() {
139290
139771
  "use strict";
@@ -139972,10 +140453,10 @@ function compareEnum(input) {
139972
140453
  });
139973
140454
  } else {
139974
140455
  for (const m of nameMatches) {
139975
- const specSet = new Set(contract.values);
139976
- const codeSet = new Set(m.values);
139977
- const missing = contract.values.filter((v) => !codeSet.has(v));
139978
- const extra = m.values.filter((v) => !specSet.has(v));
140456
+ const specNorm = new Map(contract.values.map((v) => [normalizeValue(v), v]));
140457
+ const codeNorm = new Map(m.values.map((v) => [normalizeValue(v), v]));
140458
+ const missing = contract.values.filter((v) => !codeNorm.has(normalizeValue(v)));
140459
+ const extra = m.values.filter((v) => !specNorm.has(normalizeValue(v)));
139979
140460
  for (const v of missing) {
139980
140461
  drifts.push(mkValueDrift(ref, "missing-value", v, m, contract.values));
139981
140462
  }
@@ -140003,10 +140484,10 @@ function compareEnum(input) {
140003
140484
  continue;
140004
140485
  }
140005
140486
  for (const m of subsetMatches) {
140006
- const specSet = new Set(subset.values);
140007
- const codeSet = new Set(m.values);
140008
- const missing = subset.values.filter((v) => !codeSet.has(v));
140009
- const extra = m.values.filter((v) => !specSet.has(v));
140487
+ const specNorm = new Map(subset.values.map((v) => [normalizeValue(v), v]));
140488
+ const codeNorm = new Map(m.values.map((v) => [normalizeValue(v), v]));
140489
+ const missing = subset.values.filter((v) => !codeNorm.has(normalizeValue(v)));
140490
+ const extra = m.values.filter((v) => !specNorm.has(normalizeValue(v)));
140010
140491
  for (const v of missing) {
140011
140492
  drifts.push(mkSubsetDrift(ref, subset.name, "missing-value", v, m, subset.values));
140012
140493
  }
@@ -140036,6 +140517,12 @@ function matchByName(contract, codeEnums, specName) {
140036
140517
  continue;
140037
140518
  }
140038
140519
  if (codeName.includes(target) || target.includes(codeName)) {
140520
+ if (!codeName.includes(target)) {
140521
+ const qualifyingPrefix = target.slice(0, target.indexOf(codeName));
140522
+ if (qualifyingPrefix.length > 5) {
140523
+ continue;
140524
+ }
140525
+ }
140039
140526
  if (valueSetOverlap(contract.values, e.values) >= 0.5) {
140040
140527
  out.push(e);
140041
140528
  continue;
@@ -140048,13 +140535,22 @@ function matchByName(contract, codeEnums, specName) {
140048
140535
  if (overlap >= 0.6 && sizeDiff <= 2) {
140049
140536
  out.push(e);
140050
140537
  }
140538
+ } else if (minLen >= 2) {
140539
+ const overlap = valueSetOverlap(contract.values, e.values);
140540
+ const sizeDiff = Math.abs(contract.values.length - e.values.length);
140541
+ if (overlap === 1 && sizeDiff === 0) {
140542
+ out.push(e);
140543
+ }
140051
140544
  }
140052
140545
  }
140053
140546
  return out;
140054
140547
  }
140548
+ function normalizeValue(v) {
140549
+ return v.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
140550
+ }
140055
140551
  function valueSetOverlap(a, b) {
140056
- const aSet = new Set(a);
140057
- const bSet = new Set(b);
140552
+ const aSet = new Set(a.map(normalizeValue));
140553
+ const bSet = new Set(b.map(normalizeValue));
140058
140554
  const intersection = [...aSet].filter((v) => bSet.has(v)).length;
140059
140555
  const union = (/* @__PURE__ */ new Set([...aSet, ...bSet])).size;
140060
140556
  return union === 0 ? 0 : intersection / union;
@@ -140166,7 +140662,26 @@ import { randomUUID as randomUUID21 } from "node:crypto";
140166
140662
  function compareNamedConstant(input) {
140167
140663
  const { ref, contract, codeConstants } = input;
140168
140664
  const target = normalizeName2(ref.identity);
140169
- const matches = codeConstants.filter((c2) => normalizeName2(c2.name) === target);
140665
+ let matches = codeConstants.filter((c2) => normalizeName2(c2.name) === target);
140666
+ if (matches.length === 0 && ref.identity.includes(".")) {
140667
+ const lastPart = ref.identity.split(".").pop();
140668
+ const lastTarget = normalizeName2(lastPart);
140669
+ matches = codeConstants.filter((c2) => {
140670
+ if (normalizeName2(c2.name) !== lastTarget)
140671
+ return false;
140672
+ if (c2.shape === "window-global")
140673
+ return true;
140674
+ if (c2.shape === "const-literal") {
140675
+ return contract.expectedValue === void 0 || deepEqual(
140676
+ contract.expectedValue,
140677
+ c2.value,
140678
+ /*allowExtraCodeKeys*/
140679
+ true
140680
+ );
140681
+ }
140682
+ return false;
140683
+ });
140684
+ }
140170
140685
  if (matches.length === 0) {
140171
140686
  return [{
140172
140687
  id: randomUUID21(),
@@ -140177,11 +140692,14 @@ function compareNamedConstant(input) {
140177
140692
  filePath: ref.identity,
140178
140693
  lineStart: 0,
140179
140694
  lineEnd: 0,
140180
- message: `Spec declares constant \`${ref.identity}\` (expected ${formatValue2(contract.expectedValue)}), but no code-side constant matches by name.`,
140695
+ message: `Spec declares constant \`${ref.identity}\` but no code-side constant matches by name.`,
140181
140696
  specSide: `expected: ${formatValue2(contract.expectedValue)}`,
140182
140697
  codeSide: "<no match>"
140183
140698
  }];
140184
140699
  }
140700
+ if (contract.expectedValue === void 0) {
140701
+ return [];
140702
+ }
140185
140703
  const drifts = [];
140186
140704
  for (const m of matches) {
140187
140705
  if (deepEqual(
@@ -140272,6 +140790,8 @@ function compareArchitectureDecision(input) {
140272
140790
  const { ref, contract, detected, codeDir } = input;
140273
140791
  const { category, chosen, reason } = contract;
140274
140792
  const why = reason ? ` Rationale: ${reason}` : "";
140793
+ if (!chosen)
140794
+ return [];
140275
140795
  if (detected.confidence === "inconclusive") {
140276
140796
  return [
140277
140797
  mk(ref, `architecture.${category}.inconclusive`, "info", codeDir, 0, `Spec asserts ${category} = \`${chosen}\`, but no ${category} signals were found in the code \u2014 the claim is not testable from current signals.${why}`, `${category}: ${chosen}`, "<no signals>")
@@ -140375,12 +140895,12 @@ var init_types4 = __esm({
140375
140895
  });
140376
140896
 
140377
140897
  // packages/contract-verifier/dist/verify.js
140378
- import fs42 from "node:fs";
140898
+ import fs43 from "node:fs";
140379
140899
  import path49 from "node:path";
140380
140900
  async function verify(opts) {
140381
140901
  const specFiles = [];
140382
140902
  walkTcFiles(opts.contractsDir, (filePath) => {
140383
- const source = fs42.readFileSync(filePath, "utf-8");
140903
+ const source = fs43.readFileSync(filePath, "utf-8");
140384
140904
  specFiles.push(parseFile2(filePath, source));
140385
140905
  }, opts.includeInferred ?? false);
140386
140906
  const resolution = resolve8(specFiles);
@@ -140684,7 +141204,7 @@ async function verify(opts) {
140684
141204
  };
140685
141205
  }
140686
141206
  function walkTcFiles(rootDir, visit, includeInferred) {
140687
- for (const entry of fs42.readdirSync(rootDir, { withFileTypes: true })) {
141207
+ for (const entry of fs43.readdirSync(rootDir, { withFileTypes: true })) {
140688
141208
  const full = path49.join(rootDir, entry.name);
140689
141209
  if (entry.isDirectory()) {
140690
141210
  if (!includeInferred && entry.name === "_inferred")
@@ -140930,7 +141450,7 @@ var init_serialize = __esm({
140930
141450
  });
140931
141451
 
140932
141452
  // packages/contract-verifier/dist/infer/index.js
140933
- import fs43 from "node:fs";
141453
+ import fs44 from "node:fs";
140934
141454
  import path51 from "node:path";
140935
141455
  async function infer(opts) {
140936
141456
  const authored = loadAuthored(opts.contractsDir);
@@ -140954,7 +141474,7 @@ async function infer(opts) {
140954
141474
  function loadAuthored(contractsDir) {
140955
141475
  const files = [];
140956
141476
  walkAuthoredTcFiles(contractsDir, (filePath) => {
140957
- files.push(parseFile2(filePath, fs43.readFileSync(filePath, "utf-8")));
141477
+ files.push(parseFile2(filePath, fs44.readFileSync(filePath, "utf-8")));
140958
141478
  });
140959
141479
  return resolve8(files).index;
140960
141480
  }
@@ -141494,21 +142014,21 @@ function writeInferred(contractsDir, decisions, options = {}) {
141494
142014
  proposed.push(filePath);
141495
142015
  continue;
141496
142016
  }
141497
- fs43.mkdirSync(path51.dirname(filePath), { recursive: true });
141498
- const existing = fs43.existsSync(filePath) ? fs43.readFileSync(filePath, "utf-8") : null;
142017
+ fs44.mkdirSync(path51.dirname(filePath), { recursive: true });
142018
+ const existing = fs44.existsSync(filePath) ? fs44.readFileSync(filePath, "utf-8") : null;
141499
142019
  if (existing === r.tcSource)
141500
142020
  continue;
141501
- fs43.writeFileSync(filePath, r.tcSource);
142021
+ fs44.writeFileSync(filePath, r.tcSource);
141502
142022
  written.push(filePath);
141503
142023
  }
141504
- if (!options.dryRun && fs43.existsSync(root))
142024
+ if (!options.dryRun && fs44.existsSync(root))
141505
142025
  pruneStale(root, live);
141506
142026
  return { written, proposed };
141507
142027
  }
141508
142028
  function walkAuthoredTcFiles(rootDir, visit) {
141509
- if (!fs43.existsSync(rootDir))
142029
+ if (!fs44.existsSync(rootDir))
141510
142030
  return;
141511
- for (const entry of fs43.readdirSync(rootDir, { withFileTypes: true })) {
142031
+ for (const entry of fs44.readdirSync(rootDir, { withFileTypes: true })) {
141512
142032
  if (entry.isDirectory()) {
141513
142033
  if (entry.name === INFERRED_DIR)
141514
142034
  continue;
@@ -141524,22 +142044,22 @@ function toRelCode(codeDir, fp) {
141524
142044
  }
141525
142045
  function pruneStale(root, live) {
141526
142046
  const visit = (dir) => {
141527
- if (!fs43.existsSync(dir))
142047
+ if (!fs44.existsSync(dir))
141528
142048
  return false;
141529
142049
  let dirEmpty = true;
141530
- for (const entry of fs43.readdirSync(dir, { withFileTypes: true })) {
142050
+ for (const entry of fs44.readdirSync(dir, { withFileTypes: true })) {
141531
142051
  const full = path51.join(dir, entry.name);
141532
142052
  if (entry.isDirectory()) {
141533
142053
  if (visit(full)) {
141534
142054
  try {
141535
- fs43.rmdirSync(full);
142055
+ fs44.rmdirSync(full);
141536
142056
  } catch {
141537
142057
  }
141538
142058
  } else {
141539
142059
  dirEmpty = false;
141540
142060
  }
141541
142061
  } else if (entry.isFile() && entry.name.endsWith(".tc") && !live.has(full)) {
141542
- fs43.unlinkSync(full);
142062
+ fs44.unlinkSync(full);
141543
142063
  } else {
141544
142064
  dirEmpty = false;
141545
142065
  }
@@ -141698,9 +142218,10 @@ async function runAdd(options = {}) {
141698
142218
 
141699
142219
  // tools/cli/src/commands/analyze.ts
141700
142220
  init_dist4();
141701
- import path19 from "node:path";
142221
+ import path18 from "node:path";
141702
142222
 
141703
142223
  // packages/shared/dist/llm/transport.js
142224
+ init_claude_binary();
141704
142225
  import { spawn } from "node:child_process";
141705
142226
  import { createHash as createHash2, randomUUID } from "node:crypto";
141706
142227
  import fs4 from "node:fs";
@@ -141711,7 +142232,7 @@ function stripCodeFences(text) {
141711
142232
  return fence ? fence[1] : trimmed2;
141712
142233
  }
141713
142234
  function cliTransport(opts = {}) {
141714
- const bin = opts.bin ?? process.env.CLAUDE_CODE_BIN ?? "claude";
142235
+ const bin = opts.bin ?? resolveClaudeBinary();
141715
142236
  return (req) => new Promise((resolve9, reject) => {
141716
142237
  const modelArgs = [];
141717
142238
  if (req.model)
@@ -141859,8 +142380,12 @@ init_project_config();
141859
142380
  init_git();
141860
142381
  init_logger();
141861
142382
 
142383
+ // tools/cli/src/lib/claude-preflight.ts
142384
+ init_dist4();
142385
+
141862
142386
  // packages/core/dist/lib/cli-binary.js
141863
142387
  var import_cross_spawn2 = __toESM(require_cross_spawn(), 1);
142388
+ init_dist7();
141864
142389
  function isCliBinaryAvailable(binary2) {
141865
142390
  const result = (0, import_cross_spawn2.sync)(binary2, ["--version"], {
141866
142391
  stdio: "ignore",
@@ -141868,9 +142393,80 @@ function isCliBinaryAvailable(binary2) {
141868
142393
  });
141869
142394
  return result.status === 0;
141870
142395
  }
142396
+ function cleanClaudeEnv() {
142397
+ const env = { ...process.env };
142398
+ for (const key2 of Object.keys(env)) {
142399
+ if (key2.startsWith("CLAUDE_CODE") || key2.startsWith("CLAUDE_INTERNAL")) {
142400
+ delete env[key2];
142401
+ }
142402
+ }
142403
+ return env;
142404
+ }
142405
+ function classifyClaudeProbe(code, output) {
142406
+ if (code === 0)
142407
+ return { ok: true };
142408
+ return { ok: false, reason: "failed", code, output: output.trim() };
142409
+ }
142410
+ function checkClaudeAuth(binary2 = resolveClaudeBinary(), options = {}) {
142411
+ if (!isCliBinaryAvailable(binary2)) {
142412
+ return Promise.resolve({ ok: false, reason: "not-found" });
142413
+ }
142414
+ const timeoutMs = options.timeoutMs ?? 6e4;
142415
+ return new Promise((resolve9) => {
142416
+ const proc = (0, import_cross_spawn2.spawn)(binary2, ["-p", "Reply with the single word: ok"], {
142417
+ stdio: ["ignore", "pipe", "pipe"],
142418
+ env: cleanClaudeEnv()
142419
+ });
142420
+ const output = [];
142421
+ let settled = false;
142422
+ const finish = (r) => {
142423
+ if (settled)
142424
+ return;
142425
+ settled = true;
142426
+ clearTimeout(timer);
142427
+ resolve9(r);
142428
+ };
142429
+ const timer = setTimeout(() => {
142430
+ proc.kill("SIGKILL");
142431
+ finish({ ok: true });
142432
+ }, timeoutMs);
142433
+ proc.stdout?.on("data", (b) => output.push(b));
142434
+ proc.stderr?.on("data", (b) => output.push(b));
142435
+ proc.on("error", () => finish({ ok: true }));
142436
+ proc.on("close", (code) => finish(classifyClaudeProbe(code, Buffer.concat(output).toString("utf-8"))));
142437
+ });
142438
+ }
142439
+
142440
+ // tools/cli/src/lib/claude-preflight.ts
142441
+ async function preflightClaudeOrExit() {
142442
+ const s = fe();
142443
+ s.start("Checking the `claude` CLI is logged in");
142444
+ const result = await checkClaudeAuth();
142445
+ if (result.ok) {
142446
+ s.stop("`claude` CLI ready");
142447
+ return;
142448
+ }
142449
+ s.stop("`claude` CLI not ready");
142450
+ const { title, hint } = describeClaudePreflightFailure(result);
142451
+ O2.error(title);
142452
+ O2.message(hint);
142453
+ pt("Aborted \u2014 fix the `claude` CLI and retry.");
142454
+ process.exit(1);
142455
+ }
142456
+ function describeClaudePreflightFailure(result) {
142457
+ if (result.reason === "not-found") {
142458
+ return {
142459
+ title: "The `claude` CLI isn't installed or isn't on your PATH.",
142460
+ hint: "Install Claude Code (https://docs.anthropic.com/en/docs/claude-code), or set CLAUDE_CODE_BINARY to its name/path, then retry."
142461
+ };
142462
+ }
142463
+ return {
142464
+ title: `The \`claude\` CLI failed (exit ${result.code ?? "null"}). Its output:`,
142465
+ hint: result.output || "(no output)"
142466
+ };
142467
+ }
141871
142468
 
141872
142469
  // tools/cli/src/commands/analyze.ts
141873
- init_config2();
141874
142470
  init_helpers();
141875
142471
 
141876
142472
  // tools/cli/src/commands/llm-prompt.ts
@@ -141913,8 +142509,8 @@ function showFirstRunNotice() {
141913
142509
  // tools/cli/src/community-prompts.ts
141914
142510
  init_paths();
141915
142511
  init_helpers();
141916
- import fs14 from "node:fs";
141917
- import path18 from "node:path";
142512
+ import fs15 from "node:fs";
142513
+ import path17 from "node:path";
141918
142514
  var DISCORD_URL = "https://discord.gg/TanxB63arz";
141919
142515
  var GITHUB_URL = "https://github.com/truecourse-ai/truecourse";
141920
142516
  function link(url) {
@@ -141962,11 +142558,11 @@ var DEFAULT_CONFIG3 = {
141962
142558
  starShown: false
141963
142559
  };
141964
142560
  function getConfigPath2() {
141965
- return path18.join(getGlobalDir(), "community-prompts.json");
142561
+ return path17.join(getGlobalDir(), "community-prompts.json");
141966
142562
  }
141967
142563
  function readConfig2() {
141968
142564
  try {
141969
- const raw = fs14.readFileSync(getConfigPath2(), "utf-8");
142565
+ const raw = fs15.readFileSync(getConfigPath2(), "utf-8");
141970
142566
  return { ...DEFAULT_CONFIG3, ...JSON.parse(raw) };
141971
142567
  } catch {
141972
142568
  return { ...DEFAULT_CONFIG3 };
@@ -141974,8 +142570,8 @@ function readConfig2() {
141974
142570
  }
141975
142571
  function writeConfig2(config2) {
141976
142572
  const configPath = getConfigPath2();
141977
- fs14.mkdirSync(path18.dirname(configPath), { recursive: true });
141978
- fs14.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
142573
+ fs15.mkdirSync(path17.dirname(configPath), { recursive: true });
142574
+ fs15.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
141979
142575
  }
141980
142576
  function recordAnalyzeAndMaybePrompt() {
141981
142577
  if (!isInteractive()) return;
@@ -142004,15 +142600,6 @@ ${link(GITHUB_URL)}`
142004
142600
  }
142005
142601
 
142006
142602
  // tools/cli/src/commands/analyze.ts
142007
- function ensureClaudeCli() {
142008
- const binary2 = config.claudeCodeBinary;
142009
- if (isCliBinaryAvailable(binary2)) return;
142010
- O2.error(
142011
- `Claude Code CLI not found (tried \`${binary2}\`). TrueCourse requires the Claude Code binary to run analysis.
142012
- Install it from https://docs.anthropic.com/en/docs/claude-code, or set CLAUDE_CODE_BINARY to its name or absolute path if it's installed elsewhere.`
142013
- );
142014
- process.exit(1);
142015
- }
142016
142603
  function resolveOrInitProject() {
142017
142604
  const repoDir = resolveRepoDir(process.cwd()) ?? process.cwd();
142018
142605
  ensureRepoTruecourseDir(repoDir);
@@ -142139,18 +142726,18 @@ async function resolveStashDecision(options, repoPath) {
142139
142726
  }
142140
142727
  async function runAnalyze(options = {}) {
142141
142728
  mt("Analyzing repository");
142142
- ensureClaudeCli();
142143
142729
  showFirstRunNotice();
142144
142730
  const project = resolveOrInitProject();
142145
142731
  O2.step(`Repository: ${project.name}`);
142146
142732
  await promptInstallSkills(project.path, { install: options.installSkills });
142147
142733
  configureLogger({
142148
- filePath: path19.join(project.path, ".truecourse/logs/analyze.log")
142734
+ filePath: path18.join(project.path, ".truecourse/logs/analyze.log")
142149
142735
  });
142150
142736
  const config2 = readProjectConfig(project.path);
142151
142737
  const enabledCategories = config2.enabledCategories ?? void 0;
142152
142738
  const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
142153
142739
  const enableLlmRules = llmDecision.enabled;
142740
+ if (enableLlmRules) await preflightClaudeOrExit();
142154
142741
  const stashDecision = await resolveStashDecision(options, project.path);
142155
142742
  renderPhase = enableLlmRules ? "pre-llm" : "all";
142156
142743
  if (wipeLegacyPostgresData()) {
@@ -142217,18 +142804,18 @@ async function runAnalyzeDiff(options = {}) {
142217
142804
  const { diffInProcess: diffInProcess2 } = await Promise.resolve().then(() => (init_diff_in_process(), diff_in_process_exports));
142218
142805
  const { renderDiffResultsSummary: renderDiffResultsSummary2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
142219
142806
  mt("Running diff check");
142220
- ensureClaudeCli();
142221
142807
  showFirstRunNotice();
142222
142808
  const project = resolveOrInitProject();
142223
142809
  O2.step(`Repository: ${project.name}`);
142224
142810
  await promptInstallSkills(project.path, { install: options.installSkills });
142225
142811
  configureLogger({
142226
- filePath: path19.join(project.path, ".truecourse/logs/analyze.log")
142812
+ filePath: path18.join(project.path, ".truecourse/logs/analyze.log")
142227
142813
  });
142228
142814
  const config2 = readProjectConfig(project.path);
142229
142815
  const enabledCategories = config2.enabledCategories ?? void 0;
142230
142816
  const llmDecision = resolveLlmDecision(options, config2.enableLlmRules ?? true);
142231
142817
  const enableLlmRules = llmDecision.enabled;
142818
+ if (enableLlmRules) await preflightClaudeOrExit();
142232
142819
  renderPhase = enableLlmRules ? "pre-llm" : "all";
142233
142820
  const stepDefs = buildAnalysisSteps(enabledCategories, enableLlmRules);
142234
142821
  const tracker = new StepTracker((payload) => {
@@ -142292,25 +142879,25 @@ init_dist4();
142292
142879
  init_paths();
142293
142880
  init_registry();
142294
142881
  init_helpers();
142295
- import { spawn as spawn5 } from "node:child_process";
142296
- import fs19 from "node:fs";
142297
- import path24 from "node:path";
142882
+ import { spawn as spawn6 } from "node:child_process";
142883
+ import fs20 from "node:fs";
142884
+ import path23 from "node:path";
142298
142885
  import { fileURLToPath as fileURLToPath6 } from "node:url";
142299
142886
 
142300
142887
  // tools/cli/src/commands/service/macos.ts
142301
- import fs16 from "node:fs";
142302
- import path20 from "node:path";
142888
+ import fs17 from "node:fs";
142889
+ import path19 from "node:path";
142303
142890
  import os5 from "node:os";
142304
142891
  import { execSync } from "node:child_process";
142305
142892
 
142306
142893
  // tools/cli/src/commands/service/env.ts
142307
- import fs15 from "node:fs";
142894
+ import fs16 from "node:fs";
142308
142895
  function parseEnvFile(filePath) {
142309
142896
  const vars = {};
142310
- if (!fs15.existsSync(filePath)) {
142897
+ if (!fs16.existsSync(filePath)) {
142311
142898
  return vars;
142312
142899
  }
142313
- const content = fs15.readFileSync(filePath, "utf-8");
142900
+ const content = fs16.readFileSync(filePath, "utf-8");
142314
142901
  for (const line of content.split("\n")) {
142315
142902
  const trimmed2 = line.trim();
142316
142903
  if (!trimmed2 || trimmed2.startsWith("#")) continue;
@@ -142330,15 +142917,15 @@ function parseEnvFile(filePath) {
142330
142917
 
142331
142918
  // tools/cli/src/commands/service/macos.ts
142332
142919
  var SERVICE_LABEL = "com.truecourse.server";
142333
- var PLIST_DIR = path20.join(os5.homedir(), "Library", "LaunchAgents");
142334
- var PLIST_PATH = path20.join(PLIST_DIR, `${SERVICE_LABEL}.plist`);
142920
+ var PLIST_DIR = path19.join(os5.homedir(), "Library", "LaunchAgents");
142921
+ var PLIST_PATH = path19.join(PLIST_DIR, `${SERVICE_LABEL}.plist`);
142335
142922
  function escapeXml(s) {
142336
142923
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
142337
142924
  }
142338
142925
  function buildPlist(serverPath, logPath, envVars) {
142339
- const logDir = path20.dirname(logPath);
142340
- const stdoutPath = path20.join(logDir, "dashboard.out.log");
142341
- const stderrPath = path20.join(logDir, "dashboard.err.log");
142926
+ const logDir = path19.dirname(logPath);
142927
+ const stdoutPath = path19.join(logDir, "dashboard.out.log");
142928
+ const stderrPath = path19.join(logDir, "dashboard.err.log");
142342
142929
  let envSection = "";
142343
142930
  if (Object.keys(envVars).length > 0) {
142344
142931
  const entries = Object.entries(envVars).map(([k, v]) => ` <key>${escapeXml(k)}</key>
@@ -142374,17 +142961,17 @@ ${entries}
142374
142961
  }
142375
142962
  var MacOSService = class {
142376
142963
  async install(serverPath, logPath) {
142377
- const envFile = path20.join(os5.homedir(), ".truecourse", ".env");
142964
+ const envFile = path19.join(os5.homedir(), ".truecourse", ".env");
142378
142965
  const envVars = parseEnvFile(envFile);
142379
142966
  if (process.env.PATH && !envVars.PATH) {
142380
142967
  envVars.PATH = process.env.PATH;
142381
142968
  }
142382
- envVars.TRUECOURSE_LOG_DIR = path20.dirname(logPath);
142383
- envVars.TRUECOURSE_HOME = path20.join(os5.homedir(), ".truecourse");
142384
- fs16.mkdirSync(PLIST_DIR, { recursive: true });
142385
- fs16.mkdirSync(path20.dirname(logPath), { recursive: true });
142969
+ envVars.TRUECOURSE_LOG_DIR = path19.dirname(logPath);
142970
+ envVars.TRUECOURSE_HOME = path19.join(os5.homedir(), ".truecourse");
142971
+ fs17.mkdirSync(PLIST_DIR, { recursive: true });
142972
+ fs17.mkdirSync(path19.dirname(logPath), { recursive: true });
142386
142973
  const plist = buildPlist(serverPath, logPath, envVars);
142387
- fs16.writeFileSync(PLIST_PATH, plist, "utf-8");
142974
+ fs17.writeFileSync(PLIST_PATH, plist, "utf-8");
142388
142975
  execSync(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
142389
142976
  }
142390
142977
  async uninstall() {
@@ -142392,12 +142979,12 @@ var MacOSService = class {
142392
142979
  execSync(`launchctl unload "${PLIST_PATH}"`, { stdio: "pipe" });
142393
142980
  } catch {
142394
142981
  }
142395
- if (fs16.existsSync(PLIST_PATH)) {
142396
- fs16.unlinkSync(PLIST_PATH);
142982
+ if (fs17.existsSync(PLIST_PATH)) {
142983
+ fs17.unlinkSync(PLIST_PATH);
142397
142984
  }
142398
142985
  }
142399
142986
  async start() {
142400
- if (!fs16.existsSync(PLIST_PATH)) {
142987
+ if (!fs17.existsSync(PLIST_PATH)) {
142401
142988
  throw new Error("Service is not installed. Run 'truecourse service install' first.");
142402
142989
  }
142403
142990
  execSync(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
@@ -142427,22 +143014,22 @@ var MacOSService = class {
142427
143014
  }
142428
143015
  }
142429
143016
  async isInstalled() {
142430
- return fs16.existsSync(PLIST_PATH);
143017
+ return fs17.existsSync(PLIST_PATH);
142431
143018
  }
142432
143019
  };
142433
143020
 
142434
143021
  // tools/cli/src/commands/service/linux.ts
142435
- import fs17 from "node:fs";
142436
- import path21 from "node:path";
143022
+ import fs18 from "node:fs";
143023
+ import path20 from "node:path";
142437
143024
  import os6 from "node:os";
142438
143025
  import { execSync as execSync2 } from "node:child_process";
142439
143026
  var SERVICE_NAME = "truecourse";
142440
- var UNIT_DIR = path21.join(os6.homedir(), ".config", "systemd", "user");
142441
- var UNIT_PATH = path21.join(UNIT_DIR, `${SERVICE_NAME}.service`);
143027
+ var UNIT_DIR = path20.join(os6.homedir(), ".config", "systemd", "user");
143028
+ var UNIT_PATH = path20.join(UNIT_DIR, `${SERVICE_NAME}.service`);
142442
143029
  function buildUnitFile(serverPath, logPath) {
142443
- const truecourseHome = path21.join(os6.homedir(), ".truecourse");
142444
- const envFile = path21.join(truecourseHome, ".env");
142445
- const logDir = path21.dirname(logPath);
143030
+ const truecourseHome = path20.join(os6.homedir(), ".truecourse");
143031
+ const envFile = path20.join(truecourseHome, ".env");
143032
+ const logDir = path20.dirname(logPath);
142446
143033
  return `[Unit]
142447
143034
  Description=TrueCourse Server
142448
143035
  After=network.target
@@ -142455,8 +143042,8 @@ RestartSec=5
142455
143042
  EnvironmentFile=${envFile}
142456
143043
  Environment=TRUECOURSE_HOME=${truecourseHome}
142457
143044
  Environment=TRUECOURSE_LOG_DIR=${logDir}
142458
- StandardOutput=append:${path21.join(logDir, "dashboard.out.log")}
142459
- StandardError=append:${path21.join(logDir, "dashboard.err.log")}
143045
+ StandardOutput=append:${path20.join(logDir, "dashboard.out.log")}
143046
+ StandardError=append:${path20.join(logDir, "dashboard.err.log")}
142460
143047
 
142461
143048
  [Install]
142462
143049
  WantedBy=default.target
@@ -142464,10 +143051,10 @@ WantedBy=default.target
142464
143051
  }
142465
143052
  var LinuxService = class {
142466
143053
  async install(serverPath, logPath) {
142467
- fs17.mkdirSync(UNIT_DIR, { recursive: true });
142468
- fs17.mkdirSync(path21.dirname(logPath), { recursive: true });
143054
+ fs18.mkdirSync(UNIT_DIR, { recursive: true });
143055
+ fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
142469
143056
  const unit = buildUnitFile(serverPath, logPath);
142470
- fs17.writeFileSync(UNIT_PATH, unit, "utf-8");
143057
+ fs18.writeFileSync(UNIT_PATH, unit, "utf-8");
142471
143058
  execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
142472
143059
  execSync2(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "pipe" });
142473
143060
  }
@@ -142480,8 +143067,8 @@ var LinuxService = class {
142480
143067
  execSync2(`systemctl --user disable ${SERVICE_NAME}`, { stdio: "pipe" });
142481
143068
  } catch {
142482
143069
  }
142483
- if (fs17.existsSync(UNIT_PATH)) {
142484
- fs17.unlinkSync(UNIT_PATH);
143070
+ if (fs18.existsSync(UNIT_PATH)) {
143071
+ fs18.unlinkSync(UNIT_PATH);
142485
143072
  }
142486
143073
  try {
142487
143074
  execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
@@ -142510,14 +143097,14 @@ var LinuxService = class {
142510
143097
  }
142511
143098
  }
142512
143099
  async isInstalled() {
142513
- return fs17.existsSync(UNIT_PATH);
143100
+ return fs18.existsSync(UNIT_PATH);
142514
143101
  }
142515
143102
  };
142516
143103
 
142517
143104
  // tools/cli/src/commands/service/windows.ts
142518
143105
  import { execSync as execSync3 } from "node:child_process";
142519
143106
  import os7 from "node:os";
142520
- import path22 from "node:path";
143107
+ import path21 from "node:path";
142521
143108
  var SERVICE_DISPLAY_NAME = "TrueCourse";
142522
143109
  var SERVICE_SCM_NAME = "truecourse.exe";
142523
143110
  var WindowsService = class {
@@ -142533,8 +143120,8 @@ var WindowsService = class {
142533
143120
  async install(serverPath, logPath) {
142534
143121
  const nw = await this.getNodeWindows();
142535
143122
  const { Service } = nw;
142536
- const logDir = path22.dirname(logPath);
142537
- const truecourseHome = path22.join(os7.homedir(), ".truecourse");
143123
+ const logDir = path21.dirname(logPath);
143124
+ const truecourseHome = path21.join(os7.homedir(), ".truecourse");
142538
143125
  return new Promise((resolve9, reject) => {
142539
143126
  const svc = new Service({
142540
143127
  name: SERVICE_DISPLAY_NAME,
@@ -142620,8 +143207,8 @@ function getPlatform() {
142620
143207
  }
142621
143208
 
142622
143209
  // tools/cli/src/commands/service/logs.ts
142623
- import fs18 from "node:fs";
142624
- import path23 from "node:path";
143210
+ import fs19 from "node:fs";
143211
+ import path22 from "node:path";
142625
143212
  import os8 from "node:os";
142626
143213
  var MAX_LOG_SIZE2 = 10 * 1024 * 1024;
142627
143214
  var MAX_LOG_FILES2 = 5;
@@ -142632,38 +143219,38 @@ var ROTATABLE_LOG_NAMES = [
142632
143219
  "dashboard.log"
142633
143220
  ];
142634
143221
  function getLogDir() {
142635
- return path23.join(os8.homedir(), ".truecourse", "logs");
143222
+ return path22.join(os8.homedir(), ".truecourse", "logs");
142636
143223
  }
142637
143224
  function getLogPath() {
142638
- return path23.join(getLogDir(), "dashboard.log");
143225
+ return path22.join(getLogDir(), "dashboard.log");
142639
143226
  }
142640
143227
  function existingLogFiles(logDir) {
142641
- if (!fs18.existsSync(logDir)) return [];
142642
- return fs18.readdirSync(logDir).filter((name) => /\.log$/.test(name)).sort().map((name) => path23.join(logDir, name));
143228
+ if (!fs19.existsSync(logDir)) return [];
143229
+ return fs19.readdirSync(logDir).filter((name) => /\.log$/.test(name)).sort().map((name) => path22.join(logDir, name));
142643
143230
  }
142644
143231
  function rotateOne(logFile) {
142645
- if (!fs18.existsSync(logFile)) return;
142646
- if (fs18.statSync(logFile).size < MAX_LOG_SIZE2) return;
143232
+ if (!fs19.existsSync(logFile)) return;
143233
+ if (fs19.statSync(logFile).size < MAX_LOG_SIZE2) return;
142647
143234
  for (let i = MAX_LOG_FILES2; i >= 1; i--) {
142648
143235
  const older = `${logFile}.${i}`;
142649
143236
  if (i === MAX_LOG_FILES2) {
142650
- if (fs18.existsSync(older)) fs18.unlinkSync(older);
143237
+ if (fs19.existsSync(older)) fs19.unlinkSync(older);
142651
143238
  } else {
142652
143239
  const newer = `${logFile}.${i + 1}`;
142653
- if (fs18.existsSync(older)) fs18.renameSync(older, newer);
143240
+ if (fs19.existsSync(older)) fs19.renameSync(older, newer);
142654
143241
  }
142655
143242
  }
142656
- fs18.renameSync(logFile, `${logFile}.1`);
143243
+ fs19.renameSync(logFile, `${logFile}.1`);
142657
143244
  }
142658
143245
  function rotateLogs(logDir) {
142659
143246
  for (const name of ROTATABLE_LOG_NAMES) {
142660
- rotateOne(path23.join(logDir, name));
143247
+ rotateOne(path22.join(logDir, name));
142661
143248
  }
142662
- if (!fs18.existsSync(logDir)) return;
143249
+ if (!fs19.existsSync(logDir)) return;
142663
143250
  for (const file of existingLogFiles(logDir)) rotateOne(file);
142664
143251
  }
142665
143252
  function readLastLines(filePath, maxLines) {
142666
- const content = fs18.readFileSync(filePath, "utf-8");
143253
+ const content = fs19.readFileSync(filePath, "utf-8");
142667
143254
  const lines = content.split("\n");
142668
143255
  if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
142669
143256
  return lines.slice(-maxLines).join("\n");
@@ -142674,7 +143261,7 @@ function dumpLogTails(logDir, linesPerFile = 50) {
142674
143261
  for (const file of files) {
142675
143262
  const tail = readLastLines(file, linesPerFile);
142676
143263
  process.stdout.write(`
142677
- ----- ${path23.basename(file)} (last ${linesPerFile} lines) -----
143264
+ ----- ${path22.basename(file)} (last ${linesPerFile} lines) -----
142678
143265
  `);
142679
143266
  process.stdout.write(tail);
142680
143267
  if (!tail.endsWith("\n")) process.stdout.write("\n");
@@ -142691,18 +143278,18 @@ function tailLogs() {
142691
143278
  }
142692
143279
  dumpLogTails(logDir);
142693
143280
  const sizes = /* @__PURE__ */ new Map();
142694
- for (const file of files) sizes.set(file, fs18.statSync(file).size);
143281
+ for (const file of files) sizes.set(file, fs19.statSync(file).size);
142695
143282
  const watch = (file) => {
142696
- fs18.watchFile(file, { interval: POLL_INTERVAL_MS }, () => {
143283
+ fs19.watchFile(file, { interval: POLL_INTERVAL_MS }, () => {
142697
143284
  try {
142698
- const newSize = fs18.statSync(file).size;
143285
+ const newSize = fs19.statSync(file).size;
142699
143286
  const lastSize = sizes.get(file) ?? 0;
142700
143287
  if (newSize > lastSize) {
142701
- const fd = fs18.openSync(file, "r");
143288
+ const fd = fs19.openSync(file, "r");
142702
143289
  const buf = Buffer.alloc(newSize - lastSize);
142703
- fs18.readSync(fd, buf, 0, buf.length, lastSize);
142704
- fs18.closeSync(fd);
142705
- const tag = `[${path23.basename(file)}] `;
143290
+ fs19.readSync(fd, buf, 0, buf.length, lastSize);
143291
+ fs19.closeSync(fd);
143292
+ const tag = `[${path22.basename(file)}] `;
142706
143293
  const text = buf.toString("utf-8");
142707
143294
  const tagged = text.split("\n").map((line, i, arr) => i === arr.length - 1 && line === "" ? "" : tag + line).join("\n");
142708
143295
  process.stdout.write(tagged);
@@ -142716,7 +143303,7 @@ function tailLogs() {
142716
143303
  };
142717
143304
  for (const file of files) watch(file);
142718
143305
  const cleanup = () => {
142719
- for (const file of files) fs18.unwatchFile(file);
143306
+ for (const file of files) fs19.unwatchFile(file);
142720
143307
  process.exit(0);
142721
143308
  };
142722
143309
  process.on("SIGINT", cleanup);
@@ -142724,15 +143311,15 @@ function tailLogs() {
142724
143311
  }
142725
143312
 
142726
143313
  // tools/cli/src/commands/dashboard.ts
142727
- var __dirname2 = path24.dirname(fileURLToPath6(import.meta.url));
143314
+ var __dirname2 = path23.dirname(fileURLToPath6(import.meta.url));
142728
143315
  function resolveServerEntry() {
142729
143316
  const candidates = [
142730
143317
  // Packaged CLI: dist/cli.mjs next to dist/server.mjs
142731
- path24.join(__dirname2, "server.mjs"),
143318
+ path23.join(__dirname2, "server.mjs"),
142732
143319
  // Source build output: tools/cli/dist → ../../../../dist/server.mjs
142733
- path24.resolve(__dirname2, "..", "..", "..", "..", "dist", "server.mjs")
143320
+ path23.resolve(__dirname2, "..", "..", "..", "..", "dist", "server.mjs")
142734
143321
  ];
142735
- return candidates.find((p2) => fs19.existsSync(p2)) ?? null;
143322
+ return candidates.find((p2) => fs20.existsSync(p2)) ?? null;
142736
143323
  }
142737
143324
  async function waitForHealth(url, timeoutMs = 3e4) {
142738
143325
  const start = Date.now();
@@ -142769,7 +143356,7 @@ async function promptRunMode() {
142769
143356
  async function runConsoleMode(serverEntry) {
142770
143357
  const url = getServerUrl();
142771
143358
  O2.step("Starting dashboard server...");
142772
- const serverProcess = spawn5(process.execPath, [serverEntry], {
143359
+ const serverProcess = spawn6(process.execPath, [serverEntry], {
142773
143360
  stdio: "inherit",
142774
143361
  env: { ...process.env }
142775
143362
  });
@@ -142860,7 +143447,7 @@ async function runDashboard(options = {}) {
142860
143447
  );
142861
143448
  process.exit(1);
142862
143449
  }
142863
- const configured = fs19.existsSync(getConfigPath());
143450
+ const configured = fs20.existsSync(getConfigPath());
142864
143451
  const needsDecision = !configured || options.reconfigure;
142865
143452
  let runMode;
142866
143453
  if (options.mode) {
@@ -143312,13 +143899,13 @@ async function runRulesReset({ ruleKey }) {
143312
143899
 
143313
143900
  // tools/cli/src/commands/contracts.ts
143314
143901
  init_dist4();
143315
- import fs48 from "node:fs";
143902
+ import fs49 from "node:fs";
143316
143903
  import path55 from "node:path";
143317
143904
 
143318
143905
  // packages/contract-extractor/dist/cache.js
143319
143906
  import { createHash as createHash3 } from "node:crypto";
143320
- import fs20 from "node:fs";
143321
- import path25 from "node:path";
143907
+ import fs21 from "node:fs";
143908
+ import path24 from "node:path";
143322
143909
 
143323
143910
  // packages/contract-extractor/dist/types.js
143324
143911
  init_zod();
@@ -143896,14 +144483,20 @@ Use sensible defaults when the spec doesn't spell them out:
143896
144483
  on-violation {
143897
144484
  status 401
143898
144485
  error-code unauthenticated
143899
- body ErrorEnvelope:error.envelope.standard
143900
144486
  }
143901
144487
  }
143902
144488
 
143903
- Default \`on-violation\` is **always** status 401, error-code \`unauthenticated\`,
143904
- body \`ErrorEnvelope:error.envelope.standard\` when the spec doesn't specify
143905
- otherwise. Default selector is \`path-glob "/api/**"\` when the spec says "all
143906
- /api/* endpoints" without naming a more specific pattern.
144489
+ Default \`on-violation\` is status 401, error-code \`unauthenticated\`. Add a
144490
+ \`body ErrorEnvelope:error.envelope.standard\` line ONLY when the spec/corpus
144491
+ actually establishes a standard error envelope \u2014 a dedicated errors /
144492
+ error-response section, an error-code catalog, or another slice that describes
144493
+ the error body shape. When the spec is SILENT about the error response body
144494
+ (common for terse auth docs that only state the scheme), OMIT the \`body\` line:
144495
+ a \`body\` reference to an envelope nothing defines is a dangling cross-reference
144496
+ that fails the validation gate, while an omitted body is faithful. See
144497
+ "Error-envelope references must be grounded" below. Default selector is
144498
+ \`path-glob "/api/**"\` when the spec says "all /api/* endpoints" without naming a
144499
+ more specific pattern.
143907
144500
 
143908
144501
  When a spec describes role-based auth ("admin only", "moderators can \u2026"), produce
143909
144502
  a SEPARATE \`auth-requirement\` with \`required-role <role>\`, identity
@@ -143938,7 +144531,8 @@ is the correct fallback when the slice lacks operation context.
143938
144531
  on-violation {
143939
144532
  status 403
143940
144533
  error-code forbidden
143941
- body ErrorEnvelope:error.envelope.standard
144534
+ // body ErrorEnvelope:error.envelope.standard only when that envelope is
144535
+ // defined in the corpus \u2014 see "Error-envelope references must be grounded".
143942
144536
  }
143943
144537
  }
143944
144538
 
@@ -144485,6 +145079,37 @@ response as a literal:
144485
145079
  - Only declare a literal \`response 401/403 on \u2026\` when the spec describes the
144486
145080
  response as standalone, NOT delegated to a rule.
144487
145081
 
145082
+ # Error-envelope references must be grounded
145083
+
145084
+ \`ErrorEnvelope:error.envelope.standard\` is a CROSS-REFERENCE, not a literal \u2014 it
145085
+ only resolves if a matching \`error-envelope error.envelope.standard { \u2026 }\`
145086
+ artifact exists somewhere in the corpus. Emitting the reference with nothing
145087
+ defining it produces a dangling cross-reference that fails the validation gate,
145088
+ exactly like referencing an undefined \`Enum:\`. This is the same prime-directive
145089
+ rule as enums ("never reference an enum without defining it"), applied to error
145090
+ bodies.
145091
+
145092
+ The rule applies EVERYWHERE an envelope can appear:
145093
+ \`on-violation { \u2026 body ErrorEnvelope:\u2026 }\` on auth-requirements and
145094
+ authorization-rules, and \`response \u2026 { body envelope ErrorEnvelope:\u2026 { \u2026 } }\`
145095
+ on operations.
145096
+
145097
+ - Reference \`ErrorEnvelope:error.envelope.standard\` ONLY when the spec actually
145098
+ establishes a standard error envelope: a dedicated errors / error-response
145099
+ section, an error-code catalog, or prose that names "the standard error
145100
+ envelope". In a multi-doc corpus the defining section may live in ANOTHER
145101
+ slice \u2014 that's fine, just like an \`Enum:\` defined in a sibling slice.
145102
+ - When you reference it AND the slice in front of you is the one that describes
145103
+ the envelope, ALSO emit the \`error-envelope error.envelope.standard { \u2026 }\`
145104
+ artifact \u2014 mirroring the described shape; never invent fields the spec does
145105
+ not list.
145106
+ - When the spec is SILENT about the error response body \u2014 no error section, no
145107
+ envelope, no error codes \u2014 do NOT emit the reference at all. Omit the \`body\`
145108
+ line from \`on-violation\`; on an operation response use a plain \`body \u2026\` only
145109
+ if the spec gives a shape, otherwise omit it. Faithful under-specification
145110
+ beats a dangling reference: never conjure a standard envelope just to fill the
145111
+ slot.
145112
+
144488
145113
  # Naming conventions for artifact identities
144489
145114
 
144490
145115
  Use these canonical identity formats:
@@ -144529,29 +145154,29 @@ function buildUserPrompt(slice3) {
144529
145154
  }
144530
145155
 
144531
145156
  // packages/contract-extractor/dist/cache.js
144532
- var CACHE_DIR = path25.join(".truecourse", ".cache", "extractor");
145157
+ var CACHE_DIR = path24.join(".truecourse", ".cache", "extractor");
144533
145158
  var SLICES_SUBDIR = "slices";
144534
145159
  var MANIFEST_FILE = "manifest.json";
144535
145160
  var EXTRACTION_PROMPT_FINGERPRINT = createHash3("sha256").update(SYSTEM_PROMPT).digest("hex").slice(0, 16);
144536
145161
  function cachePaths(repoRoot6) {
144537
- const cacheDir = path25.join(repoRoot6, CACHE_DIR);
145162
+ const cacheDir = path24.join(repoRoot6, CACHE_DIR);
144538
145163
  return {
144539
145164
  cacheDir,
144540
- slicesDir: path25.join(cacheDir, SLICES_SUBDIR),
144541
- manifestPath: path25.join(cacheDir, MANIFEST_FILE)
145165
+ slicesDir: path24.join(cacheDir, SLICES_SUBDIR),
145166
+ manifestPath: path24.join(cacheDir, MANIFEST_FILE)
144542
145167
  };
144543
145168
  }
144544
145169
  function ensureCacheDirs(repoRoot6) {
144545
145170
  const paths = cachePaths(repoRoot6);
144546
- fs20.mkdirSync(paths.slicesDir, { recursive: true });
145171
+ fs21.mkdirSync(paths.slicesDir, { recursive: true });
144547
145172
  return paths;
144548
145173
  }
144549
145174
  function readSliceEntry(repoRoot6, sliceId) {
144550
- const file = path25.join(cachePaths(repoRoot6).slicesDir, `${sliceId}.json`);
144551
- if (!fs20.existsSync(file))
145175
+ const file = path24.join(cachePaths(repoRoot6).slicesDir, `${sliceId}.json`);
145176
+ if (!fs21.existsSync(file))
144552
145177
  return null;
144553
145178
  try {
144554
- const raw = JSON.parse(fs20.readFileSync(file, "utf-8"));
145179
+ const raw = JSON.parse(fs21.readFileSync(file, "utf-8"));
144555
145180
  const entry = SliceCacheEntrySchema.parse(raw);
144556
145181
  if (entry.promptFingerprint !== EXTRACTION_PROMPT_FINGERPRINT)
144557
145182
  return null;
@@ -144571,16 +145196,16 @@ function writeSliceEntry(repoRoot6, slice3, result) {
144571
145196
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
144572
145197
  promptFingerprint: EXTRACTION_PROMPT_FINGERPRINT
144573
145198
  };
144574
- const file = path25.join(cachePaths(repoRoot6).slicesDir, `${slice3.id}.json`);
144575
- fs20.writeFileSync(file, JSON.stringify(entry, null, 2));
145199
+ const file = path24.join(cachePaths(repoRoot6).slicesDir, `${slice3.id}.json`);
145200
+ fs21.writeFileSync(file, JSON.stringify(entry, null, 2));
144576
145201
  return entry;
144577
145202
  }
144578
145203
  function readManifest(repoRoot6) {
144579
145204
  const file = cachePaths(repoRoot6).manifestPath;
144580
- if (!fs20.existsSync(file))
145205
+ if (!fs21.existsSync(file))
144581
145206
  return null;
144582
145207
  try {
144583
- const raw = JSON.parse(fs20.readFileSync(file, "utf-8"));
145208
+ const raw = JSON.parse(fs21.readFileSync(file, "utf-8"));
144584
145209
  return ManifestSchema.parse(raw);
144585
145210
  } catch {
144586
145211
  return null;
@@ -144589,11 +145214,11 @@ function readManifest(repoRoot6) {
144589
145214
  function writeManifest(repoRoot6, manifest) {
144590
145215
  ensureCacheDirs(repoRoot6);
144591
145216
  const file = cachePaths(repoRoot6).manifestPath;
144592
- fs20.writeFileSync(file, JSON.stringify(manifest, null, 2));
145217
+ fs21.writeFileSync(file, JSON.stringify(manifest, null, 2));
144593
145218
  }
144594
145219
  function gcOrphanedSlices(repoRoot6, manifest) {
144595
145220
  const paths = cachePaths(repoRoot6);
144596
- if (!fs20.existsSync(paths.slicesDir))
145221
+ if (!fs21.existsSync(paths.slicesDir))
144597
145222
  return 0;
144598
145223
  const live = /* @__PURE__ */ new Set();
144599
145224
  for (const spec of Object.values(manifest.specs)) {
@@ -144601,12 +145226,12 @@ function gcOrphanedSlices(repoRoot6, manifest) {
144601
145226
  live.add(s.sliceId);
144602
145227
  }
144603
145228
  let removed = 0;
144604
- for (const name of fs20.readdirSync(paths.slicesDir)) {
145229
+ for (const name of fs21.readdirSync(paths.slicesDir)) {
144605
145230
  if (!name.endsWith(".json"))
144606
145231
  continue;
144607
145232
  const id = name.slice(0, -".json".length);
144608
145233
  if (!live.has(id)) {
144609
- fs20.unlinkSync(path25.join(paths.slicesDir, name));
145234
+ fs21.unlinkSync(path24.join(paths.slicesDir, name));
144610
145235
  removed++;
144611
145236
  }
144612
145237
  }
@@ -144615,8 +145240,8 @@ function gcOrphanedSlices(repoRoot6, manifest) {
144615
145240
 
144616
145241
  // packages/contract-extractor/dist/claims-reader.js
144617
145242
  import crypto2 from "node:crypto";
144618
- import fs31 from "node:fs";
144619
- import path36 from "node:path";
145243
+ import fs32 from "node:fs";
145244
+ import path35 from "node:path";
144620
145245
 
144621
145246
  // packages/spec-consolidator/dist/types.js
144622
145247
  init_zod();
@@ -144826,8 +145451,8 @@ var ModuleManifestSchema = external_exports.object({
144826
145451
  init_dist7();
144827
145452
  import { execFileSync as execFileSync2 } from "node:child_process";
144828
145453
  import { createHash as createHash4 } from "node:crypto";
144829
- import fs21 from "node:fs";
144830
- import path26 from "node:path";
145454
+ import fs22 from "node:fs";
145455
+ import path25 from "node:path";
144831
145456
  var SKIP_DIRS = /* @__PURE__ */ new Set([
144832
145457
  "node_modules",
144833
145458
  ".git",
@@ -144848,7 +145473,7 @@ function discoverDocs(rootDir, opts = {}) {
144848
145473
  const visit = (dir) => {
144849
145474
  let entries;
144850
145475
  try {
144851
- entries = fs21.readdirSync(dir, { withFileTypes: true });
145476
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
144852
145477
  } catch {
144853
145478
  return;
144854
145479
  }
@@ -144858,7 +145483,7 @@ function discoverDocs(rootDir, opts = {}) {
144858
145483
  continue;
144859
145484
  if (entry.name.startsWith(".") && entry.name !== ".")
144860
145485
  continue;
144861
- const full = path26.join(dir, entry.name);
145486
+ const full = path25.join(dir, entry.name);
144862
145487
  if (tcIgnore.ignores(full))
144863
145488
  continue;
144864
145489
  if (entry.isDirectory()) {
@@ -144867,7 +145492,7 @@ function discoverDocs(rootDir, opts = {}) {
144867
145492
  }
144868
145493
  if (!entry.isFile())
144869
145494
  continue;
144870
- if (path26.extname(entry.name).toLowerCase() !== ".md")
145495
+ if (path25.extname(entry.name).toLowerCase() !== ".md")
144871
145496
  continue;
144872
145497
  const candidate = makeCandidate(full, rootDir, previewLines, opts);
144873
145498
  if (candidate)
@@ -144881,12 +145506,12 @@ function makeCandidate(absPath, rootDir, previewLines, opts) {
144881
145506
  let content;
144882
145507
  let stat;
144883
145508
  try {
144884
- content = fs21.readFileSync(absPath, "utf-8");
144885
- stat = fs21.statSync(absPath);
145509
+ content = fs22.readFileSync(absPath, "utf-8");
145510
+ stat = fs22.statSync(absPath);
144886
145511
  } catch {
144887
145512
  return null;
144888
145513
  }
144889
- const rel = path26.relative(rootDir, absPath).split(path26.sep).join("/");
145514
+ const rel = path25.relative(rootDir, absPath).split(path25.sep).join("/");
144890
145515
  const preview = content.split(/\r?\n/).slice(0, previewLines).join("\n");
144891
145516
  const contentHash = createHash4("sha256").update(content).digest("hex");
144892
145517
  const lastTouched = opts.skipGit ? stat.mtime.toISOString() : gitLastTouched(rootDir, rel) ?? stat.mtime.toISOString();
@@ -144914,8 +145539,8 @@ function gitLastTouched(rootDir, relPath) {
144914
145539
  }
144915
145540
  }
144916
145541
  function classifyDoc(relPath, content) {
144917
- const base = path26.basename(relPath).toLowerCase();
144918
- const dirParts = path26.dirname(relPath).split("/").map((p2) => p2.toLowerCase());
145542
+ const base = path25.basename(relPath).toLowerCase();
145543
+ const dirParts = path25.dirname(relPath).split("/").map((p2) => p2.toLowerCase());
144919
145544
  if (/^(specs?|specification|specs?-.*)\.md$/i.test(base))
144920
145545
  return "spec";
144921
145546
  if (/^adr[-_]?\d+/i.test(base) || dirParts.some((p2) => p2 === "adr" || p2 === "adrs")) {
@@ -145147,6 +145772,14 @@ You are given ONE block of markdown from a documentation file (a PRD, ADR, RFC,
145147
145772
 
145148
145773
  6. The faithfulness rule. Encode ONLY what the spec STATES. Never guess. Never default to common patterns. If the spec doesn't say what status code is returned, don't assume 200. If the spec doesn't say auth is required, don't add an auth claim.
145149
145774
 
145775
+ 7. The completeness rule (enumerations). When the spec presents an enumeration \u2014 a bulleted list, a numbered list, a markdown table column of values, or an inline comma-separated list \u2014 capture EVERY entry. Never summarize, never include a "representative subset", never drop entries because they look similar to ones you already captured. This applies regardless of how you structure the claim:
145776
+
145777
+ - If you emit ONE claim whose \`content.fields\` lists the entries (e.g. an entity's columns, an enum's value-to-description map), every enumerated value must be a key in \`fields\`. A 15-item bulleted list must produce 15 keys, not 8.
145778
+ - If you emit ONE claim per entry (the forbidden-artifact pattern), emit exactly N claims for N enumerated items.
145779
+ - If the spec lists values across multiple sections of the same doc, each section contributes its own claim; the merger handles the union downstream \u2014 don't pre-dedupe across sections.
145780
+
145781
+ Counting check before you return: if the source block contains a bulleted or numbered list under a heading like "Available <X>", "Supported <X> types", "Operation types", "Roles", "Statuses", or similar, count the bullets in that list, then count the entries you've emitted. The counts MUST match. If they don't, you've summarized \u2014 re-extract the missed entries before returning.
145782
+
145150
145783
  # Content shapes per topic
145151
145784
 
145152
145785
  ## endpoints
@@ -145347,7 +145980,7 @@ async function runOne(transport, block, timeoutMs, model, fallbackModel) {
145347
145980
 
145348
145981
  // packages/spec-consolidator/dist/extractor.js
145349
145982
  import { createHash as createHash6 } from "node:crypto";
145350
- import fs22 from "node:fs";
145983
+ import fs23 from "node:fs";
145351
145984
  async function extractClaims(rootDir, opts = {}) {
145352
145985
  const docs = opts.docs ?? discoverDocs(rootDir, opts);
145353
145986
  const allBlocks = [];
@@ -145424,7 +146057,7 @@ function shortQuote(text) {
145424
146057
  return [...lines.slice(0, 40), `\u2026 (+${lines.length - 40} more lines)`].join("\n");
145425
146058
  }
145426
146059
  function readFileSync10(absPath) {
145427
- return fs22.readFileSync(absPath, "utf-8");
146060
+ return fs23.readFileSync(absPath, "utf-8");
145428
146061
  }
145429
146062
 
145430
146063
  // packages/spec-consolidator/dist/merger.js
@@ -145614,7 +146247,7 @@ function pickRichestSameFile(list) {
145614
146247
  }
145615
146248
  }
145616
146249
  const winner = list[winnerIdx];
145617
- const others = list.filter((_, i) => i !== winnerIdx);
146250
+ const others = list.filter((_2, i) => i !== winnerIdx);
145618
146251
  return {
145619
146252
  ...winner,
145620
146253
  provenance: {
@@ -146271,21 +146904,21 @@ function uniqueSorted(arr) {
146271
146904
  // packages/spec-consolidator/dist/cache.js
146272
146905
  init_zod();
146273
146906
  import { createHash as createHash8 } from "node:crypto";
146274
- import fs23 from "node:fs";
146275
- import path27 from "node:path";
146907
+ import fs24 from "node:fs";
146908
+ import path26 from "node:path";
146276
146909
  var EXTRACTION_PROMPT_FINGERPRINT2 = createHash8("sha256").update(SYSTEM_PROMPT2).digest("hex").slice(0, 16);
146277
- var CACHE_RELATIVE = path27.join(".truecourse", ".cache", "consolidator");
146910
+ var CACHE_RELATIVE = path26.join(".truecourse", ".cache", "consolidator");
146278
146911
  var BLOCKS_SUBDIR = "blocks";
146279
146912
  function cachePaths2(repoRoot6) {
146280
- const cacheDir = path27.join(repoRoot6, CACHE_RELATIVE);
146913
+ const cacheDir = path26.join(repoRoot6, CACHE_RELATIVE);
146281
146914
  return {
146282
146915
  cacheDir,
146283
- blocksDir: path27.join(cacheDir, BLOCKS_SUBDIR)
146916
+ blocksDir: path26.join(cacheDir, BLOCKS_SUBDIR)
146284
146917
  };
146285
146918
  }
146286
146919
  function ensureCacheDirs2(repoRoot6) {
146287
146920
  const paths = cachePaths2(repoRoot6);
146288
- fs23.mkdirSync(paths.blocksDir, { recursive: true });
146921
+ fs24.mkdirSync(paths.blocksDir, { recursive: true });
146289
146922
  return paths;
146290
146923
  }
146291
146924
  var BlockCacheEntrySchema = external_exports.object({
@@ -146302,11 +146935,11 @@ var BlockCacheEntrySchema = external_exports.object({
146302
146935
  promptFingerprint: external_exports.string().optional()
146303
146936
  });
146304
146937
  function readBlockCache(repoRoot6, blockId) {
146305
- const file = path27.join(cachePaths2(repoRoot6).blocksDir, `${blockId}.json`);
146306
- if (!fs23.existsSync(file))
146938
+ const file = path26.join(cachePaths2(repoRoot6).blocksDir, `${blockId}.json`);
146939
+ if (!fs24.existsSync(file))
146307
146940
  return null;
146308
146941
  try {
146309
- const raw = JSON.parse(fs23.readFileSync(file, "utf-8"));
146942
+ const raw = JSON.parse(fs24.readFileSync(file, "utf-8"));
146310
146943
  const entry = BlockCacheEntrySchema.parse(raw);
146311
146944
  if (entry.promptFingerprint !== EXTRACTION_PROMPT_FINGERPRINT2)
146312
146945
  return null;
@@ -146323,19 +146956,19 @@ function writeBlockCache(repoRoot6, blockId, extraction) {
146323
146956
  cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
146324
146957
  promptFingerprint: EXTRACTION_PROMPT_FINGERPRINT2
146325
146958
  };
146326
- const file = path27.join(cachePaths2(repoRoot6).blocksDir, `${blockId}.json`);
146327
- fs23.writeFileSync(file, JSON.stringify(entry, null, 2));
146959
+ const file = path26.join(cachePaths2(repoRoot6).blocksDir, `${blockId}.json`);
146960
+ fs24.writeFileSync(file, JSON.stringify(entry, null, 2));
146328
146961
  }
146329
146962
  var SCAN_STATE_FILE = "scan-state.json";
146330
146963
  function scanStatePath(repoRoot6) {
146331
- return path27.join(cachePaths2(repoRoot6).cacheDir, SCAN_STATE_FILE);
146964
+ return path26.join(cachePaths2(repoRoot6).cacheDir, SCAN_STATE_FILE);
146332
146965
  }
146333
146966
  function readScanState(repoRoot6) {
146334
146967
  const file = scanStatePath(repoRoot6);
146335
- if (!fs23.existsSync(file))
146968
+ if (!fs24.existsSync(file))
146336
146969
  return null;
146337
146970
  try {
146338
- const raw = JSON.parse(fs23.readFileSync(file, "utf-8"));
146971
+ const raw = JSON.parse(fs24.readFileSync(file, "utf-8"));
146339
146972
  if (typeof raw.scannedAt !== "string")
146340
146973
  return null;
146341
146974
  if (!Array.isArray(raw.openConflicts) || !Array.isArray(raw.decidedConflicts))
@@ -146348,16 +146981,16 @@ function readScanState(repoRoot6) {
146348
146981
  function writeScanState(repoRoot6, state) {
146349
146982
  ensureCacheDirs2(repoRoot6);
146350
146983
  const file = scanStatePath(repoRoot6);
146351
- fs23.writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
146984
+ fs24.writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
146352
146985
  }
146353
146986
 
146354
146987
  // packages/spec-consolidator/dist/claims-store.js
146355
146988
  init_zod();
146356
- import fs24 from "node:fs";
146357
- import path28 from "node:path";
146989
+ import fs25 from "node:fs";
146990
+ import path27 from "node:path";
146358
146991
  var CLAIMS_FILE = "claims.json";
146359
146992
  function claimsFilePath(repoRoot6) {
146360
- return path28.join(repoRoot6, ".truecourse", "specs", CLAIMS_FILE);
146993
+ return path27.join(repoRoot6, ".truecourse", "specs", CLAIMS_FILE);
146361
146994
  }
146362
146995
  var ClaimsFileModuleSchema = ModuleManifestSchema;
146363
146996
  var ClaimsFileEntrySchema = ClaimSchema.extend({
@@ -146378,10 +147011,10 @@ var ClaimsFileSchema = external_exports.object({
146378
147011
  });
146379
147012
  function readClaims(repoRoot6) {
146380
147013
  const file = claimsFilePath(repoRoot6);
146381
- if (!fs24.existsSync(file))
147014
+ if (!fs25.existsSync(file))
146382
147015
  return null;
146383
147016
  try {
146384
- const raw = JSON.parse(fs24.readFileSync(file, "utf-8"));
147017
+ const raw = JSON.parse(fs25.readFileSync(file, "utf-8"));
146385
147018
  return ClaimsFileSchema.parse(raw);
146386
147019
  } catch {
146387
147020
  return null;
@@ -146389,26 +147022,26 @@ function readClaims(repoRoot6) {
146389
147022
  }
146390
147023
  function writeClaims(repoRoot6, input) {
146391
147024
  const file = claimsFilePath(repoRoot6);
146392
- fs24.mkdirSync(path28.dirname(file), { recursive: true });
147025
+ fs25.mkdirSync(path27.dirname(file), { recursive: true });
146393
147026
  const payload = {
146394
147027
  version: 1,
146395
147028
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
146396
147029
  modules: input.modules,
146397
147030
  claims: input.claims
146398
147031
  };
146399
- fs24.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n");
147032
+ fs25.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n");
146400
147033
  }
146401
147034
  function entryFromClaim(claim, module, source = "extracted") {
146402
147035
  return { ...claim, module, source };
146403
147036
  }
146404
147037
 
146405
147038
  // packages/spec-consolidator/dist/orchestrator.js
146406
- import fs30 from "node:fs";
146407
- import path35 from "node:path";
147039
+ import fs31 from "node:fs";
147040
+ import path34 from "node:path";
146408
147041
 
146409
147042
  // packages/spec-consolidator/dist/version-chain.js
146410
147043
  import { createHash as createHash9 } from "node:crypto";
146411
- import path29 from "node:path";
147044
+ import path28 from "node:path";
146412
147045
  function materializeManualChains(manualChains, docs) {
146413
147046
  const byPath = new Map(docs.map((d3) => [d3.path, d3]));
146414
147047
  const out = [];
@@ -146444,10 +147077,10 @@ function detectVersionChains(docs) {
146444
147077
  }
146445
147078
  var VERSION_SUFFIX_RE = /(.*?)(v\d+)(\.[^./]+)?$/i;
146446
147079
  function filenameVersionPair(a, b) {
146447
- if (path29.dirname(a.path) !== path29.dirname(b.path))
147080
+ if (path28.dirname(a.path) !== path28.dirname(b.path))
146448
147081
  return null;
146449
- const aMatch = VERSION_SUFFIX_RE.exec(stripExt(path29.basename(a.path)));
146450
- const bMatch = VERSION_SUFFIX_RE.exec(stripExt(path29.basename(b.path)));
147082
+ const aMatch = VERSION_SUFFIX_RE.exec(stripExt(path28.basename(a.path)));
147083
+ const bMatch = VERSION_SUFFIX_RE.exec(stripExt(path28.basename(b.path)));
146451
147084
  if (!aMatch || !bMatch)
146452
147085
  return null;
146453
147086
  if (aMatch[1].toLowerCase() !== bMatch[1].toLowerCase())
@@ -146470,8 +147103,8 @@ function makeChain(docs, detectedFrom) {
146470
147103
  // packages/spec-consolidator/dist/chain-recheck.js
146471
147104
  init_zod();
146472
147105
  import { createHash as createHash10 } from "node:crypto";
146473
- import fs25 from "node:fs";
146474
- import path30 from "node:path";
147106
+ import fs26 from "node:fs";
147107
+ import path29 from "node:path";
146475
147108
  var CACHE_FILE = "chain-recheck.json";
146476
147109
  var FUNDAMENTAL_SUBJECTS = [
146477
147110
  /\bauth\b/i,
@@ -146564,7 +147197,7 @@ async function runChainRecheck(repoRoot6, pairs2, opts = {}) {
146564
147197
  }
146565
147198
  function readSafe(absPath) {
146566
147199
  try {
146567
- return fs25.readFileSync(absPath, "utf-8");
147200
+ return fs26.readFileSync(absPath, "utf-8");
146568
147201
  } catch {
146569
147202
  return null;
146570
147203
  }
@@ -146658,14 +147291,14 @@ function computePairCacheKey(pair, olderContent, newerContent) {
146658
147291
  return createHash10("sha256").update(`${PROMPT_FINGERPRINT}::${pair.older.path}|${olderHash}::${pair.newer.path}|${newerHash}`).digest("hex");
146659
147292
  }
146660
147293
  function pairCacheFile(repoRoot6) {
146661
- return path30.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE);
147294
+ return path29.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE);
146662
147295
  }
146663
147296
  function readPairCache(repoRoot6, cacheKey) {
146664
147297
  const file = pairCacheFile(repoRoot6);
146665
- if (!fs25.existsSync(file))
147298
+ if (!fs26.existsSync(file))
146666
147299
  return null;
146667
147300
  try {
146668
- const raw = JSON.parse(fs25.readFileSync(file, "utf-8"));
147301
+ const raw = JSON.parse(fs26.readFileSync(file, "utf-8"));
146669
147302
  const entry = (raw.entries ?? []).find((e) => e.cacheKey === cacheKey);
146670
147303
  return entry ? ChainRecheckResultSchema.parse(entry.result) : null;
146671
147304
  } catch {
@@ -146676,9 +147309,9 @@ function writePairCache(repoRoot6, cacheKey, result) {
146676
147309
  ensureCacheDirs2(repoRoot6);
146677
147310
  const file = pairCacheFile(repoRoot6);
146678
147311
  let entries = [];
146679
- if (fs25.existsSync(file)) {
147312
+ if (fs26.existsSync(file)) {
146680
147313
  try {
146681
- const raw = JSON.parse(fs25.readFileSync(file, "utf-8"));
147314
+ const raw = JSON.parse(fs26.readFileSync(file, "utf-8"));
146682
147315
  entries = raw.entries ?? [];
146683
147316
  } catch {
146684
147317
  entries = [];
@@ -146686,14 +147319,14 @@ function writePairCache(repoRoot6, cacheKey, result) {
146686
147319
  }
146687
147320
  const filtered = entries.filter((e) => e.cacheKey !== cacheKey);
146688
147321
  filtered.push({ cacheKey, result, cachedAt: (/* @__PURE__ */ new Date()).toISOString() });
146689
- fs25.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
147322
+ fs26.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
146690
147323
  }
146691
147324
 
146692
147325
  // packages/spec-consolidator/dist/conflict-explainer.js
146693
147326
  init_zod();
146694
147327
  import { createHash as createHash11 } from "node:crypto";
146695
- import fs26 from "node:fs";
146696
- import path31 from "node:path";
147328
+ import fs27 from "node:fs";
147329
+ import path30 from "node:path";
146697
147330
  var CACHE_FILE2 = "conflict-explanations.json";
146698
147331
  async function explainConflicts(repoRoot6, conflicts2, opts = {}) {
146699
147332
  if (opts.enabled === false)
@@ -146912,14 +147545,14 @@ function computeCacheKey(conflict) {
146912
147545
  return createHash11("sha256").update(`${fp}::${conflict.id}::${ids}`).digest("hex");
146913
147546
  }
146914
147547
  function cacheFile(repoRoot6) {
146915
- return path31.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE2);
147548
+ return path30.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE2);
146916
147549
  }
146917
147550
  function readCache(repoRoot6, cacheKey) {
146918
147551
  const file = cacheFile(repoRoot6);
146919
- if (!fs26.existsSync(file))
147552
+ if (!fs27.existsSync(file))
146920
147553
  return null;
146921
147554
  try {
146922
- const raw = ExplanationCacheFileSchema.parse(JSON.parse(fs26.readFileSync(file, "utf-8")));
147555
+ const raw = ExplanationCacheFileSchema.parse(JSON.parse(fs27.readFileSync(file, "utf-8")));
146923
147556
  const entry = raw.entries.find((e) => e.cacheKey === cacheKey);
146924
147557
  return entry ? entry.explanation : null;
146925
147558
  } catch {
@@ -146930,23 +147563,23 @@ function writeCache(repoRoot6, cacheKey, explanation) {
146930
147563
  ensureCacheDirs2(repoRoot6);
146931
147564
  const file = cacheFile(repoRoot6);
146932
147565
  let entries = [];
146933
- if (fs26.existsSync(file)) {
147566
+ if (fs27.existsSync(file)) {
146934
147567
  try {
146935
- entries = ExplanationCacheFileSchema.parse(JSON.parse(fs26.readFileSync(file, "utf-8"))).entries;
147568
+ entries = ExplanationCacheFileSchema.parse(JSON.parse(fs27.readFileSync(file, "utf-8"))).entries;
146936
147569
  } catch {
146937
147570
  entries = [];
146938
147571
  }
146939
147572
  }
146940
147573
  const filtered = entries.filter((e) => e.cacheKey !== cacheKey);
146941
147574
  filtered.push({ cacheKey, explanation, cachedAt: (/* @__PURE__ */ new Date()).toISOString() });
146942
- fs26.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
147575
+ fs27.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
146943
147576
  }
146944
147577
 
146945
147578
  // packages/spec-consolidator/dist/conflict-resolver.js
146946
147579
  init_zod();
146947
147580
  import { createHash as createHash12 } from "node:crypto";
146948
- import fs27 from "node:fs";
146949
- import path32 from "node:path";
147581
+ import fs28 from "node:fs";
147582
+ import path31 from "node:path";
146950
147583
  var CACHE_FILE3 = "conflict-resolutions.json";
146951
147584
  async function resolveConflicts(repoRoot6, conflicts2, opts = {}) {
146952
147585
  if (opts.enabled === false || conflicts2.length === 0)
@@ -147158,14 +147791,14 @@ function computeCacheKey2(conflict) {
147158
147791
  return createHash12("sha256").update(`${PROMPT_FINGERPRINT2}::${conflict.id}::${ids}`).digest("hex");
147159
147792
  }
147160
147793
  function cacheFile2(repoRoot6) {
147161
- return path32.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE3);
147794
+ return path31.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE3);
147162
147795
  }
147163
147796
  function readCache2(repoRoot6, cacheKey) {
147164
147797
  const file = cacheFile2(repoRoot6);
147165
- if (!fs27.existsSync(file))
147798
+ if (!fs28.existsSync(file))
147166
147799
  return null;
147167
147800
  try {
147168
- const raw = ResolutionCacheFileSchema.parse(JSON.parse(fs27.readFileSync(file, "utf-8")));
147801
+ const raw = ResolutionCacheFileSchema.parse(JSON.parse(fs28.readFileSync(file, "utf-8")));
147169
147802
  const entry = raw.entries.find((e) => e.cacheKey === cacheKey);
147170
147803
  return entry ? entry.resolution : null;
147171
147804
  } catch {
@@ -147176,23 +147809,23 @@ function writeCache2(repoRoot6, cacheKey, resolution) {
147176
147809
  ensureCacheDirs2(repoRoot6);
147177
147810
  const file = cacheFile2(repoRoot6);
147178
147811
  let entries = [];
147179
- if (fs27.existsSync(file)) {
147812
+ if (fs28.existsSync(file)) {
147180
147813
  try {
147181
- entries = ResolutionCacheFileSchema.parse(JSON.parse(fs27.readFileSync(file, "utf-8"))).entries;
147814
+ entries = ResolutionCacheFileSchema.parse(JSON.parse(fs28.readFileSync(file, "utf-8"))).entries;
147182
147815
  } catch {
147183
147816
  entries = [];
147184
147817
  }
147185
147818
  }
147186
147819
  const filtered = entries.filter((e) => e.cacheKey !== cacheKey);
147187
147820
  filtered.push({ cacheKey, resolution, cachedAt: (/* @__PURE__ */ new Date()).toISOString() });
147188
- fs27.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
147821
+ fs28.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
147189
147822
  }
147190
147823
 
147191
147824
  // packages/spec-consolidator/dist/relevance-filter.js
147192
147825
  init_zod();
147193
147826
  import { createHash as createHash13 } from "node:crypto";
147194
- import fs28 from "node:fs";
147195
- import path33 from "node:path";
147827
+ import fs29 from "node:fs";
147828
+ import path32 from "node:path";
147196
147829
  var CACHE_FILE4 = "relevance.json";
147197
147830
  async function filterByRelevance(repoRoot6, docs, opts = {}) {
147198
147831
  if (opts.enabled === false || docs.length === 0) {
@@ -147344,14 +147977,14 @@ function computeCacheKey3(doc) {
147344
147977
  return createHash13("sha256").update(`${PROMPT_FINGERPRINT3}::${doc.path}::${doc.contentHash}`).digest("hex");
147345
147978
  }
147346
147979
  function cacheFile3(repoRoot6) {
147347
- return path33.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE4);
147980
+ return path32.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE4);
147348
147981
  }
147349
147982
  function readCache3(repoRoot6, cacheKey) {
147350
147983
  const file = cacheFile3(repoRoot6);
147351
- if (!fs28.existsSync(file))
147984
+ if (!fs29.existsSync(file))
147352
147985
  return null;
147353
147986
  try {
147354
- const raw = RelevanceCacheFileSchema.parse(JSON.parse(fs28.readFileSync(file, "utf-8")));
147987
+ const raw = RelevanceCacheFileSchema.parse(JSON.parse(fs29.readFileSync(file, "utf-8")));
147355
147988
  const entry = raw.entries.find((e) => e.cacheKey === cacheKey);
147356
147989
  return entry ? entry.verdict : null;
147357
147990
  } catch {
@@ -147362,23 +147995,23 @@ function writeCache3(repoRoot6, cacheKey, verdict) {
147362
147995
  ensureCacheDirs2(repoRoot6);
147363
147996
  const file = cacheFile3(repoRoot6);
147364
147997
  let entries = [];
147365
- if (fs28.existsSync(file)) {
147998
+ if (fs29.existsSync(file)) {
147366
147999
  try {
147367
- entries = RelevanceCacheFileSchema.parse(JSON.parse(fs28.readFileSync(file, "utf-8"))).entries;
148000
+ entries = RelevanceCacheFileSchema.parse(JSON.parse(fs29.readFileSync(file, "utf-8"))).entries;
147368
148001
  } catch {
147369
148002
  entries = [];
147370
148003
  }
147371
148004
  }
147372
148005
  const filtered = entries.filter((e) => e.cacheKey !== cacheKey);
147373
148006
  filtered.push({ cacheKey, verdict, cachedAt: (/* @__PURE__ */ new Date()).toISOString() });
147374
- fs28.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
148007
+ fs29.writeFileSync(file, JSON.stringify({ entries: filtered }, null, 2) + "\n");
147375
148008
  }
147376
148009
 
147377
148010
  // packages/spec-consolidator/dist/version-chain-llm.js
147378
148011
  init_zod();
147379
148012
  import { createHash as createHash14 } from "node:crypto";
147380
- import fs29 from "node:fs";
147381
- import path34 from "node:path";
148013
+ import fs30 from "node:fs";
148014
+ import path33 from "node:path";
147382
148015
  var CACHE_FILE5 = "chain-detection.json";
147383
148016
  var DetectedChainOutputSchema = external_exports.object({
147384
148017
  chains: external_exports.array(external_exports.object({
@@ -147481,11 +148114,11 @@ function computeCacheKey4(inputs) {
147481
148114
  return createHash14("sha256").update(`${PROMPT_FINGERPRINT4}::${material}`).digest("hex");
147482
148115
  }
147483
148116
  function readChainDetectionCache(repoRoot6, cacheKey) {
147484
- const file = path34.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE5);
147485
- if (!fs29.existsSync(file))
148117
+ const file = path33.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE5);
148118
+ if (!fs30.existsSync(file))
147486
148119
  return null;
147487
148120
  try {
147488
- const raw = JSON.parse(fs29.readFileSync(file, "utf-8"));
148121
+ const raw = JSON.parse(fs30.readFileSync(file, "utf-8"));
147489
148122
  if (raw.cacheKey !== cacheKey)
147490
148123
  return null;
147491
148124
  return DetectedChainOutputSchema.parse(raw.result);
@@ -147495,13 +148128,13 @@ function readChainDetectionCache(repoRoot6, cacheKey) {
147495
148128
  }
147496
148129
  function writeChainDetectionCache(repoRoot6, cacheKey, result) {
147497
148130
  ensureCacheDirs2(repoRoot6);
147498
- const file = path34.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE5);
148131
+ const file = path33.join(cachePaths2(repoRoot6).cacheDir, CACHE_FILE5);
147499
148132
  const entry = {
147500
148133
  cacheKey,
147501
148134
  result,
147502
148135
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
147503
148136
  };
147504
- fs29.writeFileSync(file, JSON.stringify(entry, null, 2) + "\n");
148137
+ fs30.writeFileSync(file, JSON.stringify(entry, null, 2) + "\n");
147505
148138
  }
147506
148139
  function materializeChains(result, docs) {
147507
148140
  const byPath = new Map(docs.map((d3) => [d3.path, d3]));
@@ -147658,7 +148291,7 @@ function synthesizeChainConflict(chain) {
147658
148291
  return {
147659
148292
  id: chain.id,
147660
148293
  topic: "overview",
147661
- subject: `version chain: ${chain.docs.map((d3) => path35.basename(d3.path)).join(" \u2192 ")}`,
148294
+ subject: `version chain: ${chain.docs.map((d3) => path34.basename(d3.path)).join(" \u2192 ")}`,
147662
148295
  candidates,
147663
148296
  defaultPick: candidates.length - 1
147664
148297
  };
@@ -147670,7 +148303,7 @@ function docToSyntheticClaim(doc, chain) {
147670
148303
  return {
147671
148304
  id: `version-chain:${chain.id}:${doc.path}`,
147672
148305
  topic: "overview",
147673
- subject: `version chain: ${chain.docs.map((d3) => path35.basename(d3.path)).join(" \u2192 ")}`,
148306
+ subject: `version chain: ${chain.docs.map((d3) => path34.basename(d3.path)).join(" \u2192 ")}`,
147674
148307
  content: {
147675
148308
  file: doc.path,
147676
148309
  detectedFrom: chain.detectedFrom,
@@ -147845,14 +148478,14 @@ function stitchChainConflicts(merge2, chainConflicts, chains, decisions) {
147845
148478
  }
147846
148479
  var EMPTY_DECISIONS = { version: 1, decisions: [], manualChains: [], manualIncludes: [] };
147847
148480
  function decisionsPath(repoRoot6) {
147848
- return path35.join(repoRoot6, ".truecourse", "specs", "decisions.json");
148481
+ return path34.join(repoRoot6, ".truecourse", "specs", "decisions.json");
147849
148482
  }
147850
148483
  function readDecisions(repoRoot6) {
147851
148484
  const file = decisionsPath(repoRoot6);
147852
- if (!fs30.existsSync(file))
148485
+ if (!fs31.existsSync(file))
147853
148486
  return EMPTY_DECISIONS;
147854
148487
  try {
147855
- const raw = JSON.parse(fs30.readFileSync(file, "utf-8"));
148488
+ const raw = JSON.parse(fs31.readFileSync(file, "utf-8"));
147856
148489
  return DecisionsFileSchema.parse(raw);
147857
148490
  } catch {
147858
148491
  return EMPTY_DECISIONS;
@@ -147860,8 +148493,8 @@ function readDecisions(repoRoot6) {
147860
148493
  }
147861
148494
  function writeDecisions(repoRoot6, decisions) {
147862
148495
  const file = decisionsPath(repoRoot6);
147863
- fs30.mkdirSync(path35.dirname(file), { recursive: true });
147864
- fs30.writeFileSync(file, JSON.stringify(decisions, null, 2) + "\n");
148496
+ fs31.mkdirSync(path34.dirname(file), { recursive: true });
148497
+ fs31.writeFileSync(file, JSON.stringify(decisions, null, 2) + "\n");
147865
148498
  }
147866
148499
  function wrapBlockRunner(repoRoot6, inner, onBlockDone) {
147867
148500
  return async (blocks) => {
@@ -147973,11 +148606,11 @@ function synthesizeCustomClaim(conflict, content, resolvedAt) {
147973
148606
 
147974
148607
  // packages/contract-extractor/dist/claims-reader.js
147975
148608
  function canonicalSpecPath(repoRoot6) {
147976
- return path36.join(repoRoot6, ".truecourse", "specs", "claims.json");
148609
+ return path35.join(repoRoot6, ".truecourse", "specs", "claims.json");
147977
148610
  }
147978
148611
  function hasCanonicalSpec(repoRoot6) {
147979
148612
  const file = canonicalSpecPath(repoRoot6);
147980
- if (!fs31.existsSync(file))
148613
+ if (!fs32.existsSync(file))
147981
148614
  return false;
147982
148615
  const parsed = readClaims(repoRoot6);
147983
148616
  return parsed !== null;
@@ -148578,7 +149211,7 @@ function rulesFor(a) {
148578
149211
  }
148579
149212
  if (a.kind === "auth-requirement") {
148580
149213
  if (!/\bon-violation\s*\{/.test(src)) {
148581
- out.push("missing `on-violation { status ... error-code ... body ErrorEnvelope:... }`.");
149214
+ out.push("missing `on-violation { status ... error-code ... }` (add `body ErrorEnvelope:error.envelope.standard` only when that envelope is defined in the corpus).");
148582
149215
  }
148583
149216
  if (/\brequired-role\b/.test(src)) {
148584
149217
  const hasBroadGlob = /selector\s+path-glob\s+"\/api\/(\*\*|\*)"/.test(src);
@@ -148851,7 +149484,7 @@ function validateMerged(artifacts) {
148851
149484
  }
148852
149485
 
148853
149486
  // packages/contract-extractor/dist/writer.js
148854
- import fs44 from "node:fs";
149487
+ import fs45 from "node:fs";
148855
149488
  import path52 from "node:path";
148856
149489
  var CONTRACTS_DIR = path52.join(".truecourse", "contracts");
148857
149490
  var SHARED_KINDS2 = /* @__PURE__ */ new Set([
@@ -148871,14 +149504,14 @@ function writeContracts(repoRoot6, artifacts, options = {}) {
148871
149504
  proposed.push(req.filePath);
148872
149505
  continue;
148873
149506
  }
148874
- fs44.mkdirSync(path52.dirname(req.filePath), { recursive: true });
148875
- const existing = fs44.existsSync(req.filePath) ? fs44.readFileSync(req.filePath, "utf-8") : null;
149507
+ fs45.mkdirSync(path52.dirname(req.filePath), { recursive: true });
149508
+ const existing = fs45.existsSync(req.filePath) ? fs45.readFileSync(req.filePath, "utf-8") : null;
148876
149509
  if (existing === req.tcSource)
148877
149510
  continue;
148878
- fs44.writeFileSync(req.filePath, req.tcSource);
149511
+ fs45.writeFileSync(req.filePath, req.tcSource);
148879
149512
  written.push(req.filePath);
148880
149513
  }
148881
- if (options.prune && !options.dryRun && fs44.existsSync(root)) {
149514
+ if (options.prune && !options.dryRun && fs45.existsSync(root)) {
148882
149515
  const live = new Set(requests.map((r) => r.filePath));
148883
149516
  pruneStale2(root, live);
148884
149517
  }
@@ -148947,16 +149580,16 @@ function slugifyIdentity2(identity) {
148947
149580
  }
148948
149581
  function pruneStale2(root, live) {
148949
149582
  const visit = (dir) => {
148950
- if (!fs44.existsSync(dir))
149583
+ if (!fs45.existsSync(dir))
148951
149584
  return false;
148952
149585
  let dirEmpty = true;
148953
- for (const entry of fs44.readdirSync(dir, { withFileTypes: true })) {
149586
+ for (const entry of fs45.readdirSync(dir, { withFileTypes: true })) {
148954
149587
  const full = path52.join(dir, entry.name);
148955
149588
  if (entry.isDirectory()) {
148956
149589
  const childEmpty = visit(full);
148957
149590
  if (childEmpty) {
148958
149591
  try {
148959
- fs44.rmdirSync(full);
149592
+ fs45.rmdirSync(full);
148960
149593
  } catch {
148961
149594
  }
148962
149595
  } else {
@@ -148964,7 +149597,7 @@ function pruneStale2(root, live) {
148964
149597
  }
148965
149598
  } else if (entry.isFile()) {
148966
149599
  if (entry.name.endsWith(".tc") && !live.has(full)) {
148967
- fs44.unlinkSync(full);
149600
+ fs45.unlinkSync(full);
148968
149601
  } else {
148969
149602
  dirEmpty = false;
148970
149603
  }
@@ -149178,7 +149811,7 @@ function hashString(s) {
149178
149811
 
149179
149812
  // packages/core/dist/config/llm-models.js
149180
149813
  init_paths();
149181
- import fs45 from "node:fs";
149814
+ import fs46 from "node:fs";
149182
149815
  var STAGE_DEFAULTS = {
149183
149816
  "spec.chainDetect": "haiku",
149184
149817
  "spec.claimExtract": "sonnet",
@@ -149207,10 +149840,10 @@ function stageEnvVar(stageId) {
149207
149840
  }
149208
149841
  function readConfigSync(repoDir) {
149209
149842
  const file = getRepoConfigPath(repoDir);
149210
- if (!fs45.existsSync(file))
149843
+ if (!fs46.existsSync(file))
149211
149844
  return {};
149212
149845
  try {
149213
- return JSON.parse(fs45.readFileSync(file, "utf-8"));
149846
+ return JSON.parse(fs46.readFileSync(file, "utf-8"));
149214
149847
  } catch {
149215
149848
  return {};
149216
149849
  }
@@ -149282,14 +149915,14 @@ function describeStageResolutions(repoDir = resolveRepoDir(process.cwd())) {
149282
149915
  // packages/core/dist/commands/spec-in-process.js
149283
149916
  init_dist8();
149284
149917
  init_git();
149285
- import fs47 from "node:fs";
149918
+ import fs48 from "node:fs";
149286
149919
  import path54 from "node:path";
149287
149920
  import { randomUUID as randomUUID23 } from "node:crypto";
149288
149921
 
149289
149922
  // packages/core/dist/lib/verify-store.js
149290
149923
  init_atomic_write();
149291
149924
  init_analysis_store();
149292
- import fs46 from "node:fs";
149925
+ import fs47 from "node:fs";
149293
149926
  import path53 from "node:path";
149294
149927
 
149295
149928
  // packages/core/dist/types/verify-snapshot.js
@@ -149363,7 +149996,7 @@ function readVerifyLatest(repoPath) {
149363
149996
  const file = verifyLatestPath(repoPath);
149364
149997
  let mtime;
149365
149998
  try {
149366
- mtime = fs46.statSync(file).mtimeMs;
149999
+ mtime = fs47.statSync(file).mtimeMs;
149367
150000
  } catch (err) {
149368
150001
  if (err.code === "ENOENT") {
149369
150002
  latestCache2.delete(repoPath);
@@ -149374,7 +150007,7 @@ function readVerifyLatest(repoPath) {
149374
150007
  const cached = latestCache2.get(repoPath);
149375
150008
  if (cached && cached.mtime === mtime)
149376
150009
  return cached.data;
149377
- const data = JSON.parse(fs46.readFileSync(file, "utf-8"));
150010
+ const data = JSON.parse(fs47.readFileSync(file, "utf-8"));
149378
150011
  latestCache2.set(repoPath, { mtime, data });
149379
150012
  return data;
149380
150013
  }
@@ -149389,9 +150022,9 @@ function writeVerifyRun(repoPath, snapshot) {
149389
150022
  }
149390
150023
  function readVerifyHistory(repoPath) {
149391
150024
  const file = verifyHistoryPath(repoPath);
149392
- if (!fs46.existsSync(file))
150025
+ if (!fs47.existsSync(file))
149393
150026
  return { runs: [] };
149394
- return JSON.parse(fs46.readFileSync(file, "utf-8"));
150027
+ return JSON.parse(fs47.readFileSync(file, "utf-8"));
149395
150028
  }
149396
150029
  function appendVerifyHistory(repoPath, entry) {
149397
150030
  const history = readVerifyHistory(repoPath);
@@ -149403,7 +150036,7 @@ function writeVerifyDiff(repoPath, diff) {
149403
150036
  }
149404
150037
  function deleteVerifyDiff(repoPath) {
149405
150038
  try {
149406
- fs46.unlinkSync(verifyDiffPath(repoPath));
150039
+ fs47.unlinkSync(verifyDiffPath(repoPath));
149407
150040
  } catch (err) {
149408
150041
  if (err.code !== "ENOENT")
149409
150042
  throw err;
@@ -149458,8 +150091,8 @@ function generatedMarkerPath(repoRoot6) {
149458
150091
  }
149459
150092
  function stampGeneratedMarker(repoRoot6) {
149460
150093
  const file = generatedMarkerPath(repoRoot6);
149461
- fs47.mkdirSync(path54.dirname(file), { recursive: true });
149462
- fs47.writeFileSync(file, JSON.stringify({ generatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n");
150094
+ fs48.mkdirSync(path54.dirname(file), { recursive: true });
150095
+ fs48.writeFileSync(file, JSON.stringify({ generatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n");
149463
150096
  }
149464
150097
  function resolveConsolidateModels(repoRoot6) {
149465
150098
  return {
@@ -149728,7 +150361,7 @@ async function verifyInProcess(repoRoot6, options = {}) {
149728
150361
  const startedAt = Date.now();
149729
150362
  const contractsDir = options.contractsDir ?? path54.join(repoRoot6, ".truecourse", "contracts");
149730
150363
  const codeDir = options.codeDir ?? autodetectCodeDir(repoRoot6);
149731
- if (!fs47.existsSync(contractsDir)) {
150364
+ if (!fs48.existsSync(contractsDir)) {
149732
150365
  const err = new Error(`Contracts directory not found at ${contractsDir}. Run \`truecourse contracts generate\` first.`);
149733
150366
  tracker?.error("load", err.message);
149734
150367
  throw err;
@@ -149736,7 +150369,16 @@ async function verifyInProcess(repoRoot6, options = {}) {
149736
150369
  tracker?.start("load");
149737
150370
  let result;
149738
150371
  try {
149739
- result = await runWithStash(repoRoot6, options.skipStash ?? false, tracker, () => verify({ contractsDir, codeDir }));
150372
+ result = await runWithStash(repoRoot6, {
150373
+ skipStash: options.skipStash ?? false,
150374
+ message: "truecourse-verify-stash",
150375
+ onStashStart: () => tracker?.detail?.("load", "Stashing pending changes..."),
150376
+ onRestoreStart: () => tracker?.detail?.("load", "Restoring pending changes..."),
150377
+ onRestoreError: (e) => (
150378
+ // eslint-disable-next-line no-console
150379
+ console.error(`[Verify] Failed to restore stashed changes. Run "git stash pop" manually. ${e.message}`)
150380
+ )
150381
+ }, () => verify({ contractsDir, codeDir }));
149740
150382
  } catch (e) {
149741
150383
  tracker?.error("load", e.message);
149742
150384
  throw e;
@@ -149786,7 +150428,7 @@ async function verifyInProcess(repoRoot6, options = {}) {
149786
150428
  bySeverity: summary.bySeverity
149787
150429
  });
149788
150430
  deleteVerifyDiff(repoRoot6);
149789
- fs47.rmSync(legacyVerifyStatePath(repoRoot6), { force: true });
150431
+ fs48.rmSync(legacyVerifyStatePath(repoRoot6), { force: true });
149790
150432
  const state = {
149791
150433
  verifiedAt,
149792
150434
  contractsDir,
@@ -149819,37 +150461,6 @@ async function gitMeta(repoRoot6) {
149819
150461
  return { branch: null, commitHash: null };
149820
150462
  }
149821
150463
  }
149822
- async function runWithStash(repoRoot6, skipStash, tracker, fn) {
149823
- let didStash = false;
149824
- let stashGit;
149825
- if (!skipStash) {
149826
- try {
149827
- stashGit = await getGit(repoRoot6);
149828
- const status = await stashGit.status();
149829
- if (!status.isClean()) {
149830
- const gitRoot = (await stashGit.revparse(["--show-toplevel"])).trim();
149831
- if (path54.resolve(repoRoot6) === path54.resolve(gitRoot)) {
149832
- tracker?.detail?.("load", "Stashing pending changes...");
149833
- const res = await stashGit.stash(["push", "--include-untracked", "-m", "truecourse-verify-stash"]);
149834
- didStash = !res.includes("No local changes");
149835
- }
149836
- }
149837
- } catch {
149838
- }
149839
- }
149840
- try {
149841
- return await fn();
149842
- } finally {
149843
- if (didStash && stashGit) {
149844
- tracker?.detail?.("load", "Restoring pending changes...");
149845
- try {
149846
- await stashGit.stash(["pop"]);
149847
- } catch (e) {
149848
- console.error(`[Verify] Failed to restore stashed changes. Run "git stash pop" manually. ${e.message}`);
149849
- }
149850
- }
149851
- }
149852
- }
149853
150464
  async function gitChangedFiles(repoRoot6) {
149854
150465
  const out = [];
149855
150466
  try {
@@ -149886,7 +150497,7 @@ async function verifyDiffInProcess(repoRoot6, options = {}) {
149886
150497
  tracker?.error("load", err.message);
149887
150498
  throw err;
149888
150499
  }
149889
- if (!fs47.existsSync(contractsDir)) {
150500
+ if (!fs48.existsSync(contractsDir)) {
149890
150501
  const err = new Error(`Contracts directory not found at ${contractsDir}. Run \`truecourse contracts generate\` first.`);
149891
150502
  tracker?.error("load", err.message);
149892
150503
  throw err;
@@ -149965,7 +150576,7 @@ async function inferInProcess(repoRoot6, options = {}) {
149965
150576
  }
149966
150577
  function autodetectCodeDir(repoRoot6) {
149967
150578
  const codeSubdir = path54.join(repoRoot6, "code");
149968
- if (fs47.existsSync(codeSubdir) && fs47.statSync(codeSubdir).isDirectory()) {
150579
+ if (fs48.existsSync(codeSubdir) && fs48.statSync(codeSubdir).isDirectory()) {
149969
150580
  return codeSubdir;
149970
150581
  }
149971
150582
  return repoRoot6;
@@ -150089,6 +150700,7 @@ async function runContractsGenerate(options = {}) {
150089
150700
  gt("Aborted.");
150090
150701
  process.exit(1);
150091
150702
  }
150703
+ await preflightClaudeOrExit();
150092
150704
  const concurrency = defaultConcurrency2();
150093
150705
  const extractModel = resolveModel("contract.extract", void 0, repoRoot6);
150094
150706
  const repairModel = resolveModel("contract.repair", void 0, repoRoot6);
@@ -150151,11 +150763,27 @@ async function runContractsGenerate(options = {}) {
150151
150763
  if (f2.run?.error) console.log(` \u2192 ${f2.run.error}`);
150152
150764
  }
150153
150765
  }
150154
- if (result.validationIssues.length > 0) {
150155
- O2.error(`Validation gate failed (${result.validationIssues.length} issue${result.validationIssues.length === 1 ? "" : "s"}):`);
150156
- for (const issue of result.validationIssues) {
150157
- console.log(` ${issue.artifactKey}: ${issue.message}`);
150766
+ const hardIssues = result.validationIssues.filter((i) => i.severity === "hard");
150767
+ const softIssues = result.validationIssues.filter((i) => i.severity === "soft");
150768
+ if (softIssues.length > 0) {
150769
+ const counts = /* @__PURE__ */ new Map();
150770
+ for (const issue of softIssues) {
150771
+ const line = `${issue.artifactKey}: ${issue.message}`;
150772
+ counts.set(line, (counts.get(line) ?? 0) + 1);
150158
150773
  }
150774
+ O2.warn(
150775
+ `${counts.size} unresolved cross-reference${counts.size === 1 ? "" : "s"} (non-blocking \u2014 the referenced artifact wasn't generated; \`truecourse verify\` will flag any real drift):`
150776
+ );
150777
+ for (const [line, n] of counts) console.log(` ${line}${n > 1 ? ` (\xD7${n})` : ""}`);
150778
+ }
150779
+ if (hardIssues.length > 0) {
150780
+ O2.error(
150781
+ `${hardIssues.length} artifact${hardIssues.length === 1 ? " was" : "s were"} dropped (invalid \`.tc\` \u2014 parse error or duplicate identity):`
150782
+ );
150783
+ for (const issue of hardIssues) console.log(` ${issue.artifactKey}: ${issue.message}`);
150784
+ }
150785
+ const produced = options.diff ? result.write.proposed.length : result.write.written.length;
150786
+ if (produced === 0 && result.validationIssues.length > 0) {
150159
150787
  gt("No contracts were written. Edit the spec or re-run after fixing.");
150160
150788
  process.exit(1);
150161
150789
  }
@@ -150197,7 +150825,7 @@ async function runContractsGenerate(options = {}) {
150197
150825
  async function runContractsList(options = {}) {
150198
150826
  const repoRoot6 = options.cwd ?? process.cwd();
150199
150827
  const contractsDir = path55.join(repoRoot6, ".truecourse", "contracts");
150200
- if (!fs48.existsSync(contractsDir)) {
150828
+ if (!fs49.existsSync(contractsDir)) {
150201
150829
  O2.info("No contracts found. Run `truecourse contracts generate` first.");
150202
150830
  return;
150203
150831
  }
@@ -150205,12 +150833,12 @@ async function runContractsList(options = {}) {
150205
150833
  const fileNodes = [];
150206
150834
  let parseErrors = 0;
150207
150835
  const visit = (dir) => {
150208
- for (const entry of fs48.readdirSync(dir, { withFileTypes: true })) {
150836
+ for (const entry of fs49.readdirSync(dir, { withFileTypes: true })) {
150209
150837
  const full = path55.join(dir, entry.name);
150210
150838
  if (entry.isDirectory()) visit(full);
150211
150839
  else if (entry.isFile() && entry.name.endsWith(".tc")) {
150212
150840
  try {
150213
- fileNodes.push(parser4.parseFile(full, fs48.readFileSync(full, "utf-8")));
150841
+ fileNodes.push(parser4.parseFile(full, fs49.readFileSync(full, "utf-8")));
150214
150842
  } catch {
150215
150843
  parseErrors += 1;
150216
150844
  }
@@ -150250,7 +150878,7 @@ async function runContractsList(options = {}) {
150250
150878
  async function runContractsValidate(options = {}) {
150251
150879
  const repoRoot6 = options.cwd ?? process.cwd();
150252
150880
  const contractsDir = path55.join(repoRoot6, ".truecourse", "contracts");
150253
- if (!fs48.existsSync(contractsDir)) {
150881
+ if (!fs49.existsSync(contractsDir)) {
150254
150882
  O2.error("No .truecourse/contracts/ directory found.");
150255
150883
  process.exit(1);
150256
150884
  }
@@ -150258,12 +150886,12 @@ async function runContractsValidate(options = {}) {
150258
150886
  const fileNodes = [];
150259
150887
  const issues = [];
150260
150888
  const visit = (dir) => {
150261
- for (const entry of fs48.readdirSync(dir, { withFileTypes: true })) {
150889
+ for (const entry of fs49.readdirSync(dir, { withFileTypes: true })) {
150262
150890
  const full = path55.join(dir, entry.name);
150263
150891
  if (entry.isDirectory()) visit(full);
150264
150892
  else if (entry.isFile() && entry.name.endsWith(".tc")) {
150265
150893
  try {
150266
- fileNodes.push(parser4.parseFile(full, fs48.readFileSync(full, "utf-8")));
150894
+ fileNodes.push(parser4.parseFile(full, fs49.readFileSync(full, "utf-8")));
150267
150895
  } catch (e) {
150268
150896
  issues.push(`${path55.relative(repoRoot6, full)}: parse error: ${e instanceof Error ? e.message : e}`);
150269
150897
  }
@@ -150375,42 +151003,88 @@ async function runSpecScan(opts = {}) {
150375
151003
  const root = repoRoot(opts);
150376
151004
  mt("Spec scan");
150377
151005
  await requireGitRepo(root);
151006
+ await preflightClaudeOrExit();
150378
151007
  const { renderer, tracker } = withTracker(SCAN_STEPS);
150379
- try {
150380
- const { consolidate: consolidate2 } = await scanInProcess(root, { tracker, source: "cli", llm: opts.llm, io: opts.io });
150381
- renderer.dispose();
150382
- const { extract: extract4, merge: merge2 } = consolidate2;
150383
- O2.step(`docs ${extract4.docsScanned}`);
150384
- O2.step(`blocks ${extract4.blocksAttempted} (${extract4.failures.length} failures)`);
150385
- O2.step(`claims ${extract4.claims.length}`);
150386
- O2.step(`resolved ${merge2.resolvedClaims.length}`);
150387
- O2.step(`decided ${merge2.decidedConflicts.length}`);
150388
- O2.step(`open ${merge2.openConflicts.length}`);
150389
- if (merge2.openConflicts.length > 0) {
150390
- O2.message("");
150391
- O2.message("Open conflicts:");
150392
- for (const c2 of merge2.openConflicts.slice(0, 10)) {
150393
- O2.message(` \u2022 ${c2.subject} (${c2.candidates.length} candidates, default: ${c2.candidates[c2.defaultPick].claim.provenance.file})`);
150394
- O2.message(` id: ${c2.id}`);
150395
- }
150396
- if (merge2.openConflicts.length > 10) {
150397
- O2.message(` \u2026 (+${merge2.openConflicts.length - 10} more \u2014 run \`truecourse spec conflicts list\`)`);
150398
- }
150399
- O2.message("");
150400
- O2.message("Resolve them:");
150401
- O2.message(" \u2022 dashboard: truecourse dashboard (Spec tab)");
150402
- O2.message(" \u2022 per conflict: truecourse spec conflicts show <id>");
150403
- O2.message(" truecourse spec conflicts pick <id> <candidateIndex>");
150404
- O2.message(' truecourse spec conflicts custom <id> --text "\u2026"');
150405
- O2.message(" \u2022 accept defaults: truecourse spec resolve --all-defaults");
150406
- }
150407
- gt(
150408
- merge2.openConflicts.length === 0 ? "No open conflicts \u2014 run `truecourse contracts generate`." : `${merge2.openConflicts.length} open.`
150409
- );
150410
- } catch (e) {
151008
+ const { consolidate: consolidate2 } = await scanInProcess(root, {
151009
+ tracker,
151010
+ source: "cli",
151011
+ llm: opts.llm,
151012
+ io: opts.io
151013
+ }).catch((e) => {
150411
151014
  renderer.dispose();
150412
151015
  pt(`Failed: ${e.message}`);
151016
+ process.exit(1);
151017
+ });
151018
+ renderer.dispose();
151019
+ const { extract: extract4, merge: merge2 } = consolidate2;
151020
+ O2.step(`docs ${extract4.docsScanned}`);
151021
+ O2.step(`blocks ${extract4.blocksAttempted} (${extract4.failures.length} failures)`);
151022
+ O2.step(`claims ${extract4.claims.length}`);
151023
+ O2.step(`resolved ${merge2.resolvedClaims.length}`);
151024
+ O2.step(`decided ${merge2.decidedConflicts.length}`);
151025
+ O2.step(`open ${merge2.openConflicts.length}`);
151026
+ const failures = summarizeExtractionFailures(extract4);
151027
+ const outcome = decideScanOutcome({
151028
+ blocksAttempted: extract4.blocksAttempted,
151029
+ claims: extract4.claims.length,
151030
+ openConflicts: merge2.openConflicts.length,
151031
+ failures
151032
+ });
151033
+ if (failures.total > 0) {
151034
+ O2.message("");
151035
+ O2.warn(`${failures.total} block${failures.total === 1 ? "" : "s"} failed to extract:`);
151036
+ for (const s of failures.samples) {
151037
+ O2.message(` \u2022 ${oneLine(s.message)}${s.count > 1 ? ` (\xD7${s.count})` : ""}`);
151038
+ }
151039
+ }
151040
+ if (outcome.exitCode !== 0) {
151041
+ gt(outcome.outro);
151042
+ process.exit(outcome.exitCode);
151043
+ }
151044
+ if (merge2.openConflicts.length > 0) {
151045
+ O2.message("");
151046
+ O2.message("Open conflicts:");
151047
+ for (const c2 of merge2.openConflicts.slice(0, 10)) {
151048
+ O2.message(` \u2022 ${c2.subject} (${c2.candidates.length} candidates, default: ${c2.candidates[c2.defaultPick].claim.provenance.file})`);
151049
+ O2.message(` id: ${c2.id}`);
151050
+ }
151051
+ if (merge2.openConflicts.length > 10) {
151052
+ O2.message(` \u2026 (+${merge2.openConflicts.length - 10} more \u2014 run \`truecourse spec conflicts list\`)`);
151053
+ }
151054
+ O2.message("");
151055
+ O2.message("Resolve them:");
151056
+ O2.message(" \u2022 dashboard: truecourse dashboard (Spec tab)");
151057
+ O2.message(" \u2022 per conflict: truecourse spec conflicts show <id>");
151058
+ O2.message(" truecourse spec conflicts pick <id> <candidateIndex>");
151059
+ O2.message(' truecourse spec conflicts custom <id> --text "\u2026"');
151060
+ O2.message(" \u2022 accept defaults: truecourse spec resolve --all-defaults");
151061
+ }
151062
+ gt(outcome.outro);
151063
+ }
151064
+ function decideScanOutcome(input) {
151065
+ const { failures } = input;
151066
+ if (failures.allFailed) {
151067
+ return {
151068
+ exitCode: 1,
151069
+ outro: `Aborted \u2014 all ${input.blocksAttempted} blocks failed, no claims extracted.`
151070
+ };
151071
+ }
151072
+ const outro = input.openConflicts > 0 ? `${input.openConflicts} open.` : input.claims === 0 ? "No claims extracted \u2014 nothing to generate yet." : "No open conflicts \u2014 run `truecourse contracts generate`.";
151073
+ return { exitCode: 0, outro };
151074
+ }
151075
+ function summarizeExtractionFailures(result, opts = {}) {
151076
+ const { failures, blocksAttempted } = result;
151077
+ const sampleLimit = opts.sampleLimit ?? 3;
151078
+ const counts = /* @__PURE__ */ new Map();
151079
+ for (const f2 of failures) {
151080
+ counts.set(f2.error, (counts.get(f2.error) ?? 0) + 1);
150413
151081
  }
151082
+ const samples = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, sampleLimit).map(([message, count]) => ({ message, count }));
151083
+ return {
151084
+ total: failures.length,
151085
+ allFailed: blocksAttempted > 0 && failures.length === blocksAttempted,
151086
+ samples
151087
+ };
150414
151088
  }
150415
151089
  async function runSpecResolve(opts = {}) {
150416
151090
  const root = repoRoot(opts);
@@ -150423,6 +151097,7 @@ async function runSpecResolve(opts = {}) {
150423
151097
  }
150424
151098
  mt("Spec resolve \u2014 accepting all defaults");
150425
151099
  await requireGitRepo(root);
151100
+ await preflightClaudeOrExit();
150426
151101
  const { renderer, tracker } = withTracker(RESOLVE_STEPS);
150427
151102
  try {
150428
151103
  const { additions } = await resolveAllDefaultsInProcess(root, { tracker, llm: opts.llm, io: opts.io });
@@ -150587,6 +151262,10 @@ function summarizeConflicts(label, conflicts2) {
150587
151262
  function decisionsRelPath(root) {
150588
151263
  return path56.join(root, ".truecourse", "specs", "decisions.json");
150589
151264
  }
151265
+ function oneLine(s, max = 200) {
151266
+ const collapsed = s.replace(/\s+/g, " ").trim();
151267
+ return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
151268
+ }
150590
151269
 
150591
151270
  // tools/cli/src/commands/spec-conflicts.ts
150592
151271
  init_dist4();
@@ -151003,7 +151682,7 @@ async function runConfigLlmShow(options = {}) {
151003
151682
 
151004
151683
  // tools/cli/src/commands/hooks.ts
151005
151684
  import { execSync as execSync4 } from "node:child_process";
151006
- import fs49 from "node:fs";
151685
+ import fs50 from "node:fs";
151007
151686
  import path59 from "node:path";
151008
151687
 
151009
151688
  // node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml/dist/js-yaml.mjs
@@ -153665,11 +154344,11 @@ function findGitDir(from) {
153665
154344
  let dir = from;
153666
154345
  while (true) {
153667
154346
  const gitPath = path59.join(dir, ".git");
153668
- if (fs49.existsSync(gitPath)) {
153669
- const stat = fs49.statSync(gitPath);
154347
+ if (fs50.existsSync(gitPath)) {
154348
+ const stat = fs50.statSync(gitPath);
153670
154349
  if (stat.isDirectory()) return gitPath;
153671
154350
  if (stat.isFile()) {
153672
- const content = fs49.readFileSync(gitPath, "utf-8").trim();
154351
+ const content = fs50.readFileSync(gitPath, "utf-8").trim();
153673
154352
  const match4 = content.match(/^gitdir:\s*(.+)$/);
153674
154353
  if (match4) return path59.resolve(dir, match4[1]);
153675
154354
  }
@@ -153682,7 +154361,7 @@ function findGitDir(from) {
153682
154361
  function findProjectRoot(from) {
153683
154362
  let dir = from;
153684
154363
  while (true) {
153685
- if (fs49.existsSync(path59.join(dir, ".git"))) return dir;
154364
+ if (fs50.existsSync(path59.join(dir, ".git"))) return dir;
153686
154365
  const parent = path59.dirname(dir);
153687
154366
  if (parent === dir) return null;
153688
154367
  dir = parent;
@@ -153690,10 +154369,10 @@ function findProjectRoot(from) {
153690
154369
  }
153691
154370
  function loadConfig(projectRoot) {
153692
154371
  const configPath = path59.join(projectRoot, ".truecourse", "hooks.yaml");
153693
- if (!fs49.existsSync(configPath)) return null;
154372
+ if (!fs50.existsSync(configPath)) return null;
153694
154373
  let parsed;
153695
154374
  try {
153696
- const raw = fs49.readFileSync(configPath, "utf-8");
154375
+ const raw = fs50.readFileSync(configPath, "utf-8");
153697
154376
  parsed = jsYaml.load(raw) || {};
153698
154377
  } catch (err) {
153699
154378
  console.error(`Error parsing ${configPath}: ${err.message}`);
@@ -153745,10 +154424,10 @@ async function runHooksInstall() {
153745
154424
  console.log(INSTALL_WARNING);
153746
154425
  }
153747
154426
  const hooksDir = path59.join(gitDir, "hooks");
153748
- fs49.mkdirSync(hooksDir, { recursive: true });
154427
+ fs50.mkdirSync(hooksDir, { recursive: true });
153749
154428
  const hookPath = path59.join(hooksDir, "pre-commit");
153750
- if (fs49.existsSync(hookPath)) {
153751
- const existing = fs49.readFileSync(hookPath, "utf-8");
154429
+ if (fs50.existsSync(hookPath)) {
154430
+ const existing = fs50.readFileSync(hookPath, "utf-8");
153752
154431
  if (!existing.includes(HOOK_IDENTIFIER)) {
153753
154432
  console.error(
153754
154433
  "Error: A pre-commit hook already exists and was not installed by TrueCourse."
@@ -153758,16 +154437,16 @@ async function runHooksInstall() {
153758
154437
  process.exit(1);
153759
154438
  }
153760
154439
  }
153761
- fs49.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
154440
+ fs50.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 493 });
153762
154441
  console.log("TrueCourse pre-commit hook installed.");
153763
154442
  console.log(` ${hookPath}`);
153764
154443
  const projectRoot = findProjectRoot(process.cwd());
153765
154444
  if (projectRoot) {
153766
154445
  const cfgDir = path59.join(projectRoot, ".truecourse");
153767
154446
  const cfgPath = path59.join(cfgDir, "hooks.yaml");
153768
- if (!fs49.existsSync(cfgPath)) {
153769
- fs49.mkdirSync(cfgDir, { recursive: true });
153770
- fs49.writeFileSync(cfgPath, HOOKS_YAML_TEMPLATE);
154447
+ if (!fs50.existsSync(cfgPath)) {
154448
+ fs50.mkdirSync(cfgDir, { recursive: true });
154449
+ fs50.writeFileSync(cfgPath, HOOKS_YAML_TEMPLATE);
153771
154450
  console.log(` ${cfgPath} (starter config \u2014 edit to customize, commit to share with the team)`);
153772
154451
  }
153773
154452
  }
@@ -153779,16 +154458,16 @@ function runHooksUninstall() {
153779
154458
  process.exit(1);
153780
154459
  }
153781
154460
  const hookPath = path59.join(gitDir, "hooks", "pre-commit");
153782
- if (!fs49.existsSync(hookPath)) {
154461
+ if (!fs50.existsSync(hookPath)) {
153783
154462
  console.log("No pre-commit hook installed.");
153784
154463
  return;
153785
154464
  }
153786
- const content = fs49.readFileSync(hookPath, "utf-8");
154465
+ const content = fs50.readFileSync(hookPath, "utf-8");
153787
154466
  if (!content.includes(HOOK_IDENTIFIER)) {
153788
154467
  console.error("Error: The pre-commit hook was not installed by TrueCourse. Leaving it in place.");
153789
154468
  process.exit(1);
153790
154469
  }
153791
- fs49.unlinkSync(hookPath);
154470
+ fs50.unlinkSync(hookPath);
153792
154471
  console.log("TrueCourse pre-commit hook removed.");
153793
154472
  }
153794
154473
  function runHooksStatus() {
@@ -153798,7 +154477,7 @@ function runHooksStatus() {
153798
154477
  process.exit(1);
153799
154478
  }
153800
154479
  const hookPath = path59.join(gitDir, "hooks", "pre-commit");
153801
- const installed = fs49.existsSync(hookPath) && fs49.readFileSync(hookPath, "utf-8").includes(HOOK_IDENTIFIER);
154480
+ const installed = fs50.existsSync(hookPath) && fs50.readFileSync(hookPath, "utf-8").includes(HOOK_IDENTIFIER);
153802
154481
  if (installed) {
153803
154482
  console.log("TrueCourse pre-commit hook: installed");
153804
154483
  console.log(` ${hookPath}`);
@@ -153907,7 +154586,7 @@ async function runHooksRun() {
153907
154586
 
153908
154587
  // tools/cli/src/index.ts
153909
154588
  var program2 = new Command();
153910
- program2.name("truecourse").version("0.6.1").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
154589
+ program2.name("truecourse").version("0.6.6-next.0").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
153911
154590
  var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").option("--service", "Run as a background service (skips mode prompt)").option("--console", "Run in this terminal (skips mode prompt)").action(async (options) => {
153912
154591
  if (options.service && options.console) {
153913
154592
  console.error("error: --service and --console are mutually exclusive");