@outcomeeng/spx 0.5.2 → 0.5.3

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 (3) hide show
  1. package/dist/cli.js +1601 -468
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1392,6 +1392,25 @@ function validatePathFilterConfig(raw, path6) {
1392
1392
  if (exclude !== void 0) value.exclude = exclude;
1393
1393
  return { ok: true, value };
1394
1394
  }
1395
+ var PATH_SEGMENT_SEPARATOR = "/";
1396
+ function normalizePathPrefix(value) {
1397
+ return value.replace(/\/+$/, "");
1398
+ }
1399
+ function pathMatchesPrefix(path6, prefix) {
1400
+ const normalizedPath = normalizePathPrefix(path6);
1401
+ const normalizedPrefix = normalizePathPrefix(prefix);
1402
+ return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}${PATH_SEGMENT_SEPARATOR}`);
1403
+ }
1404
+ function applyPathFilter(paths, config) {
1405
+ const includePrefixes = config.include ?? [];
1406
+ const excludePrefixes = config.exclude ?? [];
1407
+ return paths.filter((path6) => {
1408
+ if (includePrefixes.length > 0 && !includePrefixes.some((prefix) => pathMatchesPrefix(path6, prefix))) {
1409
+ return false;
1410
+ }
1411
+ return !excludePrefixes.some((prefix) => pathMatchesPrefix(path6, prefix));
1412
+ });
1413
+ }
1395
1414
 
1396
1415
  // src/domains/audit/config.ts
1397
1416
  var AUDIT_SECTION = "audit";
@@ -1900,8 +1919,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
1900
1919
  var LAYER2 = HIDDEN_PREFIX_LAYER;
1901
1920
  function hiddenPrefixPredicate(path6, config) {
1902
1921
  const segments = path6.split("/");
1903
- const basename2 = segments[segments.length - 1] ?? "";
1904
- const matched = basename2.startsWith(config.hiddenPrefix);
1922
+ const basename3 = segments[segments.length - 1] ?? "";
1923
+ const matched = basename3.startsWith(config.hiddenPrefix);
1905
1924
  return { matched, layer: LAYER2 };
1906
1925
  }
1907
1926
 
@@ -2396,6 +2415,93 @@ var productionRegistry = [
2396
2415
 
2397
2416
  // src/config/descriptor-digest.ts
2398
2417
  import { createHash } from "crypto";
2418
+ var DEFAULT_DESCRIPTOR_PATH = "descriptor section";
2419
+ var SHA256_ALGORITHM = "sha256";
2420
+ var UTF8_ENCODING = "utf8";
2421
+ var HEX_ENCODING = "hex";
2422
+ function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2423
+ const normalized = normalizeDescriptorJsonValue(value, path6, /* @__PURE__ */ new WeakSet());
2424
+ if (!normalized.ok) return normalized;
2425
+ return { ok: true, value: JSON.stringify(normalized.value) };
2426
+ }
2427
+ function digestDescriptorSection(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
2428
+ const canonical = canonicalDescriptorJson(value, path6);
2429
+ if (!canonical.ok) return canonical;
2430
+ const sha256 = createHash(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING)).digest(HEX_ENCODING);
2431
+ return {
2432
+ ok: true,
2433
+ value: {
2434
+ canonicalJson: canonical.value,
2435
+ sha256
2436
+ }
2437
+ };
2438
+ }
2439
+ function normalizeDescriptorJsonValue(value, path6, seen) {
2440
+ switch (typeof value) {
2441
+ case "string":
2442
+ case "boolean":
2443
+ return { ok: true, value };
2444
+ case "number":
2445
+ return Number.isFinite(value) ? { ok: true, value } : { ok: false, error: `${path6} must be a finite number` };
2446
+ case "object":
2447
+ if (value === null) return { ok: true, value };
2448
+ return normalizeObject(value, path6, seen);
2449
+ case "undefined":
2450
+ case "bigint":
2451
+ case "function":
2452
+ case "symbol":
2453
+ return { ok: false, error: `${path6} must be JSON-representable` };
2454
+ }
2455
+ }
2456
+ function normalizeObject(value, path6, seen) {
2457
+ if (seen.has(value)) {
2458
+ return { ok: false, error: `${path6} must not contain circular references` };
2459
+ }
2460
+ seen.add(value);
2461
+ const result = Array.isArray(value) ? normalizeArray(value, path6, seen) : normalizeRecord(value, path6, seen);
2462
+ seen.delete(value);
2463
+ return result;
2464
+ }
2465
+ function normalizeArray(value, path6, seen) {
2466
+ const normalized = [];
2467
+ for (const [index, item] of value.entries()) {
2468
+ const itemResult = normalizeDescriptorJsonValue(item, `${path6}[${index}]`, seen);
2469
+ if (!itemResult.ok) return itemResult;
2470
+ normalized.push(itemResult.value);
2471
+ }
2472
+ return { ok: true, value: normalized };
2473
+ }
2474
+ function normalizeRecord(value, path6, seen) {
2475
+ if (!isPlainRecord(value)) {
2476
+ return { ok: false, error: `${path6} must be a plain object` };
2477
+ }
2478
+ const keys = Reflect.ownKeys(value);
2479
+ if (keys.some((key) => typeof key === "symbol")) {
2480
+ return { ok: false, error: `${path6} must not contain symbol keys` };
2481
+ }
2482
+ const record = value;
2483
+ const normalized = {};
2484
+ for (const key of keys.sort(compareUnicodeCodePointStrings)) {
2485
+ const child = normalizeDescriptorJsonValue(record[key], `${path6}.${key}`, seen);
2486
+ if (!child.ok) return child;
2487
+ normalized[key] = child.value;
2488
+ }
2489
+ return { ok: true, value: normalized };
2490
+ }
2491
+ function isPlainRecord(value) {
2492
+ const prototype = Object.getPrototypeOf(value);
2493
+ return prototype === null || prototype === Object.prototype;
2494
+ }
2495
+ function compareUnicodeCodePointStrings(left, right) {
2496
+ const leftCodePoints = Array.from(left, (value) => value.codePointAt(0) ?? 0);
2497
+ const rightCodePoints = Array.from(right, (value) => value.codePointAt(0) ?? 0);
2498
+ const length = Math.min(leftCodePoints.length, rightCodePoints.length);
2499
+ for (let index = 0; index < length; index += 1) {
2500
+ const delta = leftCodePoints[index] - rightCodePoints[index];
2501
+ if (delta !== 0) return delta;
2502
+ }
2503
+ return leftCodePoints.length - rightCodePoints.length;
2504
+ }
2399
2505
 
2400
2506
  // src/config/index.ts
2401
2507
  var CONFIG_FILE_FORMAT = {
@@ -2731,6 +2837,15 @@ function resolveProductDir(cwd = process.cwd()) {
2731
2837
  };
2732
2838
  }
2733
2839
 
2840
+ // src/interfaces/cli/write-warning.ts
2841
+ function writeWarning(warning) {
2842
+ if (warning === void 0) {
2843
+ return;
2844
+ }
2845
+ process.stderr.write(`${warning}
2846
+ `);
2847
+ }
2848
+
2734
2849
  // src/interfaces/cli/config.ts
2735
2850
  function buildDefaultDeps() {
2736
2851
  return {
@@ -2739,10 +2854,7 @@ function buildDefaultDeps() {
2739
2854
  resolveConfigFromReadResult,
2740
2855
  resolveProductDir: () => {
2741
2856
  const resolved = resolveProductDir();
2742
- if (resolved.warning !== void 0) {
2743
- process.stderr.write(`${resolved.warning}
2744
- `);
2745
- }
2857
+ writeWarning(resolved.warning);
2746
2858
  return resolved.productDir;
2747
2859
  },
2748
2860
  descriptors: productionRegistry
@@ -2851,6 +2963,55 @@ ${output}`;
2851
2963
  return output;
2852
2964
  }
2853
2965
 
2966
+ // src/domains/session/handoff-base-checklist.ts
2967
+ var SESSION_HANDOFF_BASE_ERROR_NAME = "SessionHandoffBaseError";
2968
+ var HANDOFF_BASE_MARK = {
2969
+ MET: "\u2713",
2970
+ UNMET: "\u2717"
2971
+ };
2972
+ var HANDOFF_BASE_PREREQUISITE_LABEL = {
2973
+ CLEAN_WORKING_TREE: "working tree is clean",
2974
+ DETACHED_AT_DEFAULT_TIP: "HEAD is detached at the default-branch tip"
2975
+ };
2976
+ var HANDOFF_BASE_REMEDY = {
2977
+ /** Unclean working tree: commit, or hand off from the root worktree. */
2978
+ COMMIT_OR_ROOT: "commit the changes, or run handoff from the root worktree",
2979
+ /** Off the default-branch tip: detach to it, or hand off from the root worktree. */
2980
+ DETACH_TO_TIP_OR_ROOT: "detach HEAD to the default-branch tip, or run handoff from the root worktree",
2981
+ /** Default branch unresolved: only the root worktree can anchor the base. */
2982
+ ROOT_ONLY: "run handoff from the root worktree"
2983
+ };
2984
+ var HANDOFF_BASE_FACT_LABEL = {
2985
+ DEFAULT_BRANCH: "default branch",
2986
+ DEFAULT_TIP: "origin tip",
2987
+ HEAD: "HEAD",
2988
+ CURRENT_WORKTREE: "current worktree",
2989
+ ROOT_WORKTREE: "root worktree"
2990
+ };
2991
+ var HANDOFF_BASE_UNRESOLVED = "unresolved";
2992
+ var CHECKLIST_INDENT = " ";
2993
+ var REMEDY_SEPARATOR = " \u2014 ";
2994
+ var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this linked worktree.`;
2995
+ function renderFactLine(label, value) {
2996
+ return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
2997
+ }
2998
+ function renderPrerequisiteLine(prerequisite) {
2999
+ const mark = prerequisite.met ? HANDOFF_BASE_MARK.MET : HANDOFF_BASE_MARK.UNMET;
3000
+ const base = `${CHECKLIST_INDENT}${mark} ${prerequisite.label}`;
3001
+ return prerequisite.met ? base : `${base}${REMEDY_SEPARATOR}${prerequisite.remedy}`;
3002
+ }
3003
+ function renderHandoffBaseChecklist(checklist) {
3004
+ return [
3005
+ CHECKLIST_HEADER,
3006
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_BRANCH, checklist.defaultBranch),
3007
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
3008
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
3009
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
3010
+ renderFactLine(HANDOFF_BASE_FACT_LABEL.ROOT_WORKTREE, checklist.rootWorktreePath),
3011
+ ...checklist.prerequisites.map(renderPrerequisiteLine)
3012
+ ].join("\n");
3013
+ }
3014
+
2854
3015
  // src/domains/session/errors.ts
2855
3016
  var SessionError = class extends Error {
2856
3017
  constructor(message) {
@@ -2895,11 +3056,17 @@ var SessionInvalidNextStepError = class extends SessionError {
2895
3056
  }
2896
3057
  };
2897
3058
  var SessionHandoffBaseError = class extends SessionError {
2898
- constructor() {
3059
+ /** The prerequisite checklist to render, or `null` for a non-checklist refusal. */
3060
+ checklist;
3061
+ /** Whether the refusal surfaces no diagnostic — true only for a non-git base. */
3062
+ silent;
3063
+ constructor(options = {}) {
2899
3064
  super(
2900
- "Cannot create a handoff session from this git work context. Run handoff from the root worktree, or from a linked worktree with a clean working tree detached at the tip of origin/<default branch>."
3065
+ "Cannot create a handoff session from this git work context. Run handoff from the root worktree, or from a linked worktree with a clean working tree detached at the tip of the default branch."
2901
3066
  );
2902
- this.name = "SessionHandoffBaseError";
3067
+ this.name = SESSION_HANDOFF_BASE_ERROR_NAME;
3068
+ this.checklist = options.checklist ?? null;
3069
+ this.silent = options.silent ?? false;
2903
3070
  }
2904
3071
  };
2905
3072
  var SessionNotClaimedError = class extends SessionError {
@@ -2976,7 +3143,7 @@ var defaultDeps = {
2976
3143
  };
2977
3144
  }
2978
3145
  };
2979
- var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
3146
+ var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
2980
3147
  var GIT_ROOT_COMMAND = {
2981
3148
  EXECUTABLE: "git",
2982
3149
  REV_PARSE: "rev-parse",
@@ -3061,7 +3228,8 @@ async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = default
3061
3228
  return {
3062
3229
  productDir: cwd,
3063
3230
  isGitRepo: false,
3064
- warning: NOT_GIT_REPO_WARNING
3231
+ warning: NOT_GIT_REPO_WARNING,
3232
+ worktreeRoot: cwd
3065
3233
  };
3066
3234
  }
3067
3235
  const toplevel = extractStdout(toplevelResult.stdout);
@@ -3073,7 +3241,8 @@ async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = default
3073
3241
  if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
3074
3242
  return {
3075
3243
  productDir: toplevel,
3076
- isGitRepo: true
3244
+ isGitRepo: true,
3245
+ worktreeRoot: toplevel
3077
3246
  };
3078
3247
  }
3079
3248
  const commonDir = extractStdout(commonDirResult.stdout);
@@ -3081,22 +3250,18 @@ async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = default
3081
3250
  const gitCommonDirProductRoot = dirname(absoluteCommonDir);
3082
3251
  return {
3083
3252
  productDir: gitCommonDirProductRoot,
3084
- isGitRepo: true
3253
+ isGitRepo: true,
3254
+ worktreeRoot: toplevel
3085
3255
  };
3086
3256
  } catch {
3087
3257
  return {
3088
3258
  productDir: cwd,
3089
3259
  isGitRepo: false,
3090
- warning: NOT_GIT_REPO_WARNING
3260
+ warning: NOT_GIT_REPO_WARNING,
3261
+ worktreeRoot: cwd
3091
3262
  };
3092
3263
  }
3093
3264
  }
3094
- function computeRelativeWorktreePath(commonDir, toplevel) {
3095
- const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve3(toplevel, commonDir);
3096
- const commonDirProductRoot = dirname(absoluteCommonDir);
3097
- const worktreePath = relative2(commonDirProductRoot, toplevel);
3098
- return worktreePath === "" ? "" : worktreePath;
3099
- }
3100
3265
  async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultDeps) {
3101
3266
  const result = await deps.execa(
3102
3267
  GIT_ROOT_COMMAND.EXECUTABLE,
@@ -3149,17 +3314,6 @@ async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultDeps) {
3149
3314
  if (result.exitCode !== 0) return false;
3150
3315
  return extractStdout(result.stdout).length === 0;
3151
3316
  }
3152
- async function isRootWorktree(cwd = process.cwd(), deps = defaultDeps) {
3153
- const [toplevelResult, commonDirResult] = await Promise.all([
3154
- deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_SHOW_TOPLEVEL_ARGS], { cwd, reject: false }),
3155
- deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_COMMON_DIR_ARGS], { cwd, reject: false })
3156
- ]);
3157
- if (toplevelResult.exitCode !== 0) return false;
3158
- if (commonDirResult.exitCode !== 0) return true;
3159
- const toplevel = extractStdout(toplevelResult.stdout);
3160
- const commonDir = extractStdout(commonDirResult.stdout);
3161
- return computeRelativeWorktreePath(commonDir, toplevel) === "";
3162
- }
3163
3317
  async function resolveSessionConfig(options = {}) {
3164
3318
  const { sessionsDir, cwd, deps } = options;
3165
3319
  const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
@@ -3184,6 +3338,15 @@ async function resolveSessionConfig(options = {}) {
3184
3338
  };
3185
3339
  }
3186
3340
 
3341
+ // src/commands/session/resolve-config.ts
3342
+ async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning) {
3343
+ const { config, warning } = await resolveSessionConfig({ sessionsDir });
3344
+ if (warning !== void 0) {
3345
+ onWarning?.(warning);
3346
+ }
3347
+ return config;
3348
+ }
3349
+
3187
3350
  // src/commands/session/archive.ts
3188
3351
  var SESSION_ARCHIVE_OUTPUT = {
3189
3352
  ARCHIVED: "Archived session",
@@ -3237,7 +3400,7 @@ async function archiveSingle(sessionId, config) {
3237
3400
  ${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
3238
3401
  }
3239
3402
  async function archiveCommand(options) {
3240
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
3403
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3241
3404
  return processBatch(options.sessionIds, (id) => archiveSingle(id, config));
3242
3405
  }
3243
3406
 
@@ -3462,7 +3625,7 @@ async function deleteSingle(sessionId, config) {
3462
3625
  return `${SESSION_DELETE_OUTPUT.DELETED}: ${sessionId}`;
3463
3626
  }
3464
3627
  async function deleteCommand(options) {
3465
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
3628
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3466
3629
  return processBatch(options.sessionIds, (id) => deleteSingle(id, config));
3467
3630
  }
3468
3631
 
@@ -3481,22 +3644,53 @@ ${SESSION_FRONT_MATTER_DELIMITER}
3481
3644
  `;
3482
3645
 
3483
3646
  // src/domains/session/handoff-base.ts
3647
+ function detachedDefaultTipSha(facts) {
3648
+ const atTip = facts.branch === null && facts.headSha !== null && facts.defaultTipSha !== null && facts.headSha === facts.defaultTipSha;
3649
+ return atTip ? facts.headSha : null;
3650
+ }
3651
+ function cleanPrerequisite(facts) {
3652
+ return {
3653
+ label: HANDOFF_BASE_PREREQUISITE_LABEL.CLEAN_WORKING_TREE,
3654
+ met: facts.isClean,
3655
+ remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_ROOT
3656
+ };
3657
+ }
3658
+ function detachedAtTipPrerequisite(facts, met) {
3659
+ const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.ROOT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_ROOT;
3660
+ return {
3661
+ label: HANDOFF_BASE_PREREQUISITE_LABEL.DETACHED_AT_DEFAULT_TIP,
3662
+ met,
3663
+ remedy: met ? "" : remedy
3664
+ };
3665
+ }
3666
+ function buildChecklist(facts, prerequisites) {
3667
+ return {
3668
+ defaultBranch: facts.defaultBranch,
3669
+ defaultTipSha: facts.defaultTipSha,
3670
+ headSha: facts.headSha,
3671
+ currentWorktreePath: facts.currentWorktreePath,
3672
+ rootWorktreePath: facts.rootWorktreePath,
3673
+ prerequisites
3674
+ };
3675
+ }
3484
3676
  function resolveHandoffGitRef(facts) {
3677
+ if (!facts.isGitRepo) {
3678
+ throw new SessionHandoffBaseError({ silent: true });
3679
+ }
3485
3680
  if (facts.isRootWorktree) {
3486
3681
  if (facts.branch !== null) return facts.branch;
3487
3682
  if (facts.headSha !== null) return facts.headSha;
3488
3683
  throw new SessionHandoffBaseError();
3489
3684
  }
3490
- if (facts.branch !== null) {
3491
- throw new SessionHandoffBaseError();
3492
- }
3493
- if (!facts.isClean) {
3494
- throw new SessionHandoffBaseError();
3495
- }
3496
- if (facts.headSha === null || facts.defaultTipSha === null || facts.headSha !== facts.defaultTipSha) {
3497
- throw new SessionHandoffBaseError();
3685
+ const tipSha = detachedDefaultTipSha(facts);
3686
+ const prerequisites = [
3687
+ cleanPrerequisite(facts),
3688
+ detachedAtTipPrerequisite(facts, tipSha !== null)
3689
+ ];
3690
+ if (facts.isClean && tipSha !== null) {
3691
+ return tipSha;
3498
3692
  }
3499
- return facts.headSha;
3693
+ throw new SessionHandoffBaseError({ checklist: buildChecklist(facts, prerequisites) });
3500
3694
  }
3501
3695
 
3502
3696
  // src/domains/session/parse-handoff-input.ts
@@ -3627,31 +3821,38 @@ function ensureStringArrayOrDefault(value, fieldName) {
3627
3821
 
3628
3822
  // src/commands/session/handoff.ts
3629
3823
  async function resolveSessionGitRef(cwd, deps) {
3630
- const [isRoot, branch, headSha] = await Promise.all([
3631
- isRootWorktree(cwd, deps),
3824
+ const [branch, headSha, gitRoots] = await Promise.all([
3632
3825
  getCurrentBranch(cwd, deps),
3633
- getHeadSha(cwd, deps)
3826
+ getHeadSha(cwd, deps),
3827
+ detectGitCommonDirProductRoot(cwd, deps)
3634
3828
  ]);
3829
+ const currentWorktreePath = gitRoots.worktreeRoot;
3830
+ const rootWorktreePath = gitRoots.productDir;
3831
+ const isRoot = currentWorktreePath === rootWorktreePath;
3635
3832
  let isClean = false;
3833
+ let defaultBranch = null;
3636
3834
  let defaultTipSha = null;
3637
- if (!isRoot && branch === null) {
3638
- const [clean, defaultBranch] = await Promise.all([
3835
+ if (gitRoots.isGitRepo && !isRoot) {
3836
+ [isClean, defaultBranch] = await Promise.all([
3639
3837
  isWorkingTreeClean(cwd, deps),
3640
3838
  resolveDefaultBranch(cwd, deps)
3641
3839
  ]);
3642
- isClean = clean;
3643
3840
  defaultTipSha = defaultBranch === null ? null : await resolveRefSha(`${ORIGIN_REF_PREFIX}${defaultBranch}`, cwd, deps);
3644
3841
  }
3645
3842
  return resolveHandoffGitRef({
3843
+ isGitRepo: gitRoots.isGitRepo,
3646
3844
  isRootWorktree: isRoot,
3647
3845
  branch,
3648
3846
  headSha,
3649
3847
  isClean,
3650
- defaultTipSha
3848
+ defaultBranch,
3849
+ defaultTipSha,
3850
+ currentWorktreePath,
3851
+ rootWorktreePath
3651
3852
  });
3652
3853
  }
3653
3854
  async function handoffCommand(options) {
3654
- const { config, warning } = await resolveSessionConfig({
3855
+ const { config } = await resolveSessionConfig({
3655
3856
  sessionsDir: options.sessionsDir,
3656
3857
  cwd: options.cwd,
3657
3858
  deps: options.deps
@@ -3692,7 +3893,7 @@ async function handoffCommand(options) {
3692
3893
  await writeFile(sessionPath, fullContent, "utf-8");
3693
3894
  const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
3694
3895
  <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
3695
- return warning ? { output, warning } : { output };
3896
+ return { output };
3696
3897
  }
3697
3898
 
3698
3899
  // src/commands/session/list.ts
@@ -3753,7 +3954,7 @@ function validateStatus(input) {
3753
3954
  );
3754
3955
  }
3755
3956
  async function listCommand(options) {
3756
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
3957
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3757
3958
  const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
3758
3959
  const sessionsByStatus = {};
3759
3960
  for (const status of statuses) {
@@ -3853,7 +4054,7 @@ async function pickupSingle(sessionId, config) {
3853
4054
  ${output}`;
3854
4055
  }
3855
4056
  async function pickupCommand(options) {
3856
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
4057
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3857
4058
  if (options.auto) {
3858
4059
  if (options.sessionIds.length > 0) {
3859
4060
  throw new Error("Session IDs cannot be combined with --auto");
@@ -3947,7 +4148,7 @@ async function pruneCommand(options) {
3947
4148
  validatePruneOptions(options);
3948
4149
  const keep = options.keep ?? DEFAULT_KEEP_COUNT;
3949
4150
  const dryRun = options.dryRun ?? false;
3950
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
4151
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
3951
4152
  const sessions = await loadArchiveSessions(config);
3952
4153
  const toPrune = selectSessionsToDelete(sessions, { keep });
3953
4154
  if (toPrune.length === 0) {
@@ -4029,7 +4230,7 @@ async function releaseSingle(sessionId, config) {
4029
4230
  ${SESSION_RELEASE_OUTPUT.RETURNED_TO_TODO}`;
4030
4231
  }
4031
4232
  async function releaseCommand(options) {
4032
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
4233
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
4033
4234
  let ids = options.sessionIds;
4034
4235
  if (ids.length === 0) {
4035
4236
  const sessions = await loadDoingSessions(config);
@@ -4067,7 +4268,7 @@ async function showSingle(sessionId, config) {
4067
4268
  return formatShowOutput(content, { status: found.status });
4068
4269
  }
4069
4270
  async function showCommand2(options) {
4070
- const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
4271
+ const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
4071
4272
  return processBatch(options.sessionIds, (id) => showSingle(id, config));
4072
4273
  }
4073
4274
 
@@ -4172,7 +4373,8 @@ function registerSessionCommands(sessionCmd) {
4172
4373
  const output = await listCommand({
4173
4374
  status: options.status,
4174
4375
  format: options.json ? "json" : "text",
4175
- sessionsDir: options.sessionsDir
4376
+ sessionsDir: options.sessionsDir,
4377
+ onWarning: writeWarning
4176
4378
  });
4177
4379
  console.log(output);
4178
4380
  } catch (error) {
@@ -4184,7 +4386,8 @@ function registerSessionCommands(sessionCmd) {
4184
4386
  const output = await listCommand({
4185
4387
  status: SESSION_STATUSES[0],
4186
4388
  format: options.json ? "json" : "text",
4187
- sessionsDir: options.sessionsDir
4389
+ sessionsDir: options.sessionsDir,
4390
+ onWarning: writeWarning
4188
4391
  });
4189
4392
  console.log(output);
4190
4393
  } catch (error) {
@@ -4195,7 +4398,8 @@ function registerSessionCommands(sessionCmd) {
4195
4398
  try {
4196
4399
  const output = await showCommand2({
4197
4400
  sessionIds: ids,
4198
- sessionsDir: options.sessionsDir
4401
+ sessionsDir: options.sessionsDir,
4402
+ onWarning: writeWarning
4199
4403
  });
4200
4404
  console.log(output);
4201
4405
  } catch (error) {
@@ -4211,7 +4415,8 @@ function registerSessionCommands(sessionCmd) {
4211
4415
  const output = await pickupCommand({
4212
4416
  sessionIds: ids,
4213
4417
  auto: options.auto,
4214
- sessionsDir: options.sessionsDir
4418
+ sessionsDir: options.sessionsDir,
4419
+ onWarning: writeWarning
4215
4420
  });
4216
4421
  console.log(output);
4217
4422
  } catch (error) {
@@ -4222,7 +4427,8 @@ function registerSessionCommands(sessionCmd) {
4222
4427
  try {
4223
4428
  const output = await releaseCommand({
4224
4429
  sessionIds: ids,
4225
- sessionsDir: options.sessionsDir
4430
+ sessionsDir: options.sessionsDir,
4431
+ onWarning: writeWarning
4226
4432
  });
4227
4433
  console.log(output);
4228
4434
  } catch (error) {
@@ -4236,12 +4442,16 @@ function registerSessionCommands(sessionCmd) {
4236
4442
  content,
4237
4443
  sessionsDir: options.sessionsDir
4238
4444
  });
4239
- if (result.warning !== void 0) {
4240
- process.stderr.write(`${result.warning}
4241
- `);
4242
- }
4243
4445
  console.log(result.output);
4244
4446
  } catch (error) {
4447
+ if (error instanceof SessionHandoffBaseError) {
4448
+ if (error.checklist !== null) {
4449
+ console.error(renderHandoffBaseChecklist(error.checklist));
4450
+ } else if (!error.silent) {
4451
+ console.error("Error:", `${error.name}: ${error.message}`);
4452
+ }
4453
+ process.exit(1);
4454
+ }
4245
4455
  handleError(error);
4246
4456
  }
4247
4457
  });
@@ -4249,7 +4459,8 @@ function registerSessionCommands(sessionCmd) {
4249
4459
  try {
4250
4460
  const output = await deleteCommand({
4251
4461
  sessionIds: ids,
4252
- sessionsDir: options.sessionsDir
4462
+ sessionsDir: options.sessionsDir,
4463
+ onWarning: writeWarning
4253
4464
  });
4254
4465
  console.log(output);
4255
4466
  } catch (error) {
@@ -4262,7 +4473,8 @@ function registerSessionCommands(sessionCmd) {
4262
4473
  const output = await pruneCommand({
4263
4474
  keep,
4264
4475
  dryRun: options.dryRun,
4265
- sessionsDir: options.sessionsDir
4476
+ sessionsDir: options.sessionsDir,
4477
+ onWarning: writeWarning
4266
4478
  });
4267
4479
  console.log(output);
4268
4480
  } catch (error) {
@@ -4277,7 +4489,8 @@ function registerSessionCommands(sessionCmd) {
4277
4489
  try {
4278
4490
  const output = await archiveCommand({
4279
4491
  sessionIds: ids,
4280
- sessionsDir: options.sessionsDir
4492
+ sessionsDir: options.sessionsDir,
4493
+ onWarning: writeWarning
4281
4494
  });
4282
4495
  console.log(output);
4283
4496
  } catch (error) {
@@ -4783,158 +4996,1207 @@ function formatNextNode(node) {
4783
4996
  ].join("\n");
4784
4997
  }
4785
4998
 
4786
- // src/commands/spec/status.ts
4787
- var OUTPUT_FORMAT = {
4788
- TEXT: "text",
4789
- JSON: "json",
4790
- MARKDOWN: "markdown",
4791
- TABLE: "table"
4792
- };
4793
- var SPEC_STATUS_MESSAGE = {
4794
- EMPTY: `No spec-tree nodes found in ${SPEC_TREE_CONFIG.ROOT_DIRECTORY}`
4795
- };
4796
- var DEFAULT_FORMAT = OUTPUT_FORMAT.TEXT;
4797
- var JSON_INDENTATION = 2;
4798
- var STATUS_SEPARATOR = " ";
4799
- var NODE_INDENT = " ";
4800
- var MARKDOWN_NODE_PREFIX = "- ";
4801
- var TABLE_SEPARATOR = "|";
4802
- var TABLE_HEADER_SEPARATOR = "---";
4803
- var TABLE_HEADER = {
4804
- KIND: "Kind",
4805
- PATH: "Path",
4806
- STATE: "State"
4807
- };
4808
- var SPEC_STATUS_TABLE_HEADER = formatTableRow([
4809
- TABLE_HEADER.KIND,
4810
- TABLE_HEADER.PATH,
4811
- TABLE_HEADER.STATE
4812
- ]);
4813
- async function statusCommand(options = {}) {
4814
- const snapshot = await readCommandSnapshot(options);
4815
- const projection = projectSpecTree(snapshot);
4816
- return renderSpecStatus(projection, options.format);
4999
+ // src/commands/testing/discovery.ts
5000
+ import { readdir as readdir6 } from "fs/promises";
5001
+ import { join as join11, relative as relative3, sep } from "path";
5002
+ var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
5003
+ var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
5004
+ var POSIX_SEPARATOR = "/";
5005
+ var ERROR_CODE_NOT_FOUND = "ENOENT";
5006
+ async function discoverTestFiles(productDir) {
5007
+ const found = [];
5008
+ await collectTestFiles(join11(productDir, SPEC_ROOT_DIRECTORY), false, found);
5009
+ return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
5010
+ }
5011
+ async function collectTestFiles(directory, insideTestsDir, found) {
5012
+ let entries;
5013
+ try {
5014
+ entries = await readdir6(directory, { withFileTypes: true });
5015
+ } catch (error) {
5016
+ if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return;
5017
+ throw error;
5018
+ }
5019
+ for (const entry of entries) {
5020
+ const childPath = join11(directory, entry.name);
5021
+ if (entry.isDirectory()) {
5022
+ await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
5023
+ } else if (insideTestsDir && entry.isFile()) {
5024
+ found.push(childPath);
5025
+ }
5026
+ }
4817
5027
  }
4818
- function renderSpecStatus(projection, format2 = DEFAULT_FORMAT) {
4819
- if (projection.nodes.length === 0 && format2 !== OUTPUT_FORMAT.JSON) {
4820
- return SPEC_STATUS_MESSAGE.EMPTY;
5028
+ function toPosixRelative(productDir, absolute) {
5029
+ return relative3(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
5030
+ }
5031
+ function compareAscii(left, right) {
5032
+ if (left < right) return -1;
5033
+ if (left > right) return 1;
5034
+ return 0;
5035
+ }
5036
+ function hasErrorCode(error, code) {
5037
+ return typeof error === "object" && error !== null && error.code === code;
5038
+ }
5039
+
5040
+ // src/domains/testing/aggregation.ts
5041
+ var SUCCESS_EXIT_CODE = 0;
5042
+ function aggregateTestExitCode(invocations) {
5043
+ for (const invocation of invocations) {
5044
+ if (invocation.invoked && invocation.exitCode !== void 0 && invocation.exitCode !== SUCCESS_EXIT_CODE) {
5045
+ return invocation.exitCode;
5046
+ }
4821
5047
  }
4822
- switch (format2) {
4823
- case OUTPUT_FORMAT.JSON:
4824
- return formatJSON(projection);
4825
- case OUTPUT_FORMAT.MARKDOWN:
4826
- return formatMarkdown(projection);
4827
- case OUTPUT_FORMAT.TABLE:
4828
- return formatTable(projection);
4829
- case OUTPUT_FORMAT.TEXT:
4830
- return formatText(projection);
4831
- default: {
4832
- const unsupportedFormat = format2;
4833
- throw new RangeError(`Unsupported spec status output format: ${unsupportedFormat}`);
5048
+ return SUCCESS_EXIT_CODE;
5049
+ }
5050
+
5051
+ // src/domains/testing/grouping.ts
5052
+ function groupTestFiles(testFiles, languages) {
5053
+ const pathsByLanguage = /* @__PURE__ */ new Map();
5054
+ const unmatched = [];
5055
+ for (const testFile of testFiles) {
5056
+ const language = languages.find((candidate) => candidate.matchesTestFile(testFile));
5057
+ if (language === void 0) {
5058
+ unmatched.push(testFile);
5059
+ continue;
5060
+ }
5061
+ const existing = pathsByLanguage.get(language);
5062
+ if (existing === void 0) {
5063
+ pathsByLanguage.set(language, [testFile]);
5064
+ } else {
5065
+ existing.push(testFile);
4834
5066
  }
4835
5067
  }
5068
+ const groups = languages.filter((language) => pathsByLanguage.has(language)).map((language) => ({ language, testPaths: pathsByLanguage.get(language) ?? [] }));
5069
+ return { groups, unmatched };
4836
5070
  }
4837
- async function readCommandSnapshot(options) {
4838
- if (options.source !== void 0) {
4839
- return readSpecTree({ source: options.source });
5071
+
5072
+ // src/commands/testing/dispatch.ts
5073
+ var NO_EXCLUDED_NODE_PATHS = [];
5074
+ async function runTests(options, deps) {
5075
+ const discovered = await discoverTestFiles(options.productDir);
5076
+ const testFiles = options.passingScope === void 0 ? discovered : applyPathFilter(discovered, options.passingScope);
5077
+ const { groups, unmatched } = groupTestFiles(testFiles, options.registry.languages);
5078
+ const invocations = [];
5079
+ const outcomes = [];
5080
+ for (const group of groups) {
5081
+ const invocation = await group.language.runTests(
5082
+ {
5083
+ projectRoot: options.productDir,
5084
+ testPaths: group.testPaths,
5085
+ excludedNodePaths: NO_EXCLUDED_NODE_PATHS
5086
+ },
5087
+ deps.runnerDepsFor(group.language)
5088
+ );
5089
+ invocations.push(invocation);
5090
+ if (invocation.invoked) {
5091
+ outcomes.push({
5092
+ runnerId: group.language.name,
5093
+ testPaths: group.testPaths,
5094
+ exitCode: invocation.exitCode ?? SUCCESS_EXIT_CODE
5095
+ });
5096
+ }
4840
5097
  }
4841
- const productDir = await resolveSpecProductDir(
4842
- options.cwd ?? process.cwd(),
4843
- options.gitDependencies,
4844
- options.onWarning
4845
- );
4846
- const source = createFilesystemSpecTreeSource({ productDir });
4847
- return readSpecTree({ source });
5098
+ return { exitCode: aggregateTestExitCode(invocations), groups, unmatched, outcomes };
4848
5099
  }
4849
- function formatJSON(projection) {
4850
- return JSON.stringify(projection, null, JSON_INDENTATION);
5100
+
5101
+ // src/commands/testing/run-command.ts
5102
+ import { join as join14 } from "path";
5103
+
5104
+ // src/testing/run-state.ts
5105
+ import { createHash as createHash3, randomBytes as nodeRandomBytes2 } from "crypto";
5106
+ import {
5107
+ mkdir as nodeMkdir2,
5108
+ readdir as nodeReaddir2,
5109
+ readFile as nodeReadFile2,
5110
+ rename as nodeRename2,
5111
+ writeFile as nodeWriteFile2
5112
+ } from "fs/promises";
5113
+ import { join as join13 } from "path";
5114
+
5115
+ // src/domains/audit/run-state.ts
5116
+ import { createHash as createHash2, randomBytes as nodeRandomBytes } from "crypto";
5117
+ import {
5118
+ mkdir as nodeMkdir,
5119
+ readdir as nodeReaddir,
5120
+ readFile as nodeReadFile,
5121
+ rename as nodeRename,
5122
+ writeFile as nodeWriteFile
5123
+ } from "fs/promises";
5124
+ import { join as join12 } from "path";
5125
+ var AUDIT_RUN_STATE_STATUS = {
5126
+ APPROVED: "approved",
5127
+ REJECTED: "rejected",
5128
+ FAILED: "failed",
5129
+ INTERRUPTED: "interrupted"
5130
+ };
5131
+ var AUDIT_RUN_STATE_DISPLAY = {
5132
+ [AUDIT_RUN_STATE_STATUS.APPROVED]: "APPROVED",
5133
+ [AUDIT_RUN_STATE_STATUS.REJECTED]: AUDIT_VERDICT_VALUE.REJECT,
5134
+ [AUDIT_RUN_STATE_STATUS.FAILED]: "FAILED",
5135
+ [AUDIT_RUN_STATE_STATUS.INTERRUPTED]: "INTERRUPTED"
5136
+ };
5137
+ var AUDIT_RUN_TIMESTAMP_SEPARATOR = "_";
5138
+ var AUDIT_RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
5139
+ function formatAuditRunTimestamp(date) {
5140
+ const year = date.getUTCFullYear();
5141
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
5142
+ const day = String(date.getUTCDate()).padStart(2, "0");
5143
+ const hours = String(date.getUTCHours()).padStart(2, "0");
5144
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
5145
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
5146
+ const milliseconds = String(date.getUTCMilliseconds()).padStart(AUDIT_RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
5147
+ return `${year}-${month}-${day}${AUDIT_RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
5148
+ }
5149
+
5150
+ // src/testing/run-state.ts
5151
+ var TEST_RUN_STATE_STATUS = {
5152
+ PASSED: "passed",
5153
+ FAILED: "failed",
5154
+ INTERRUPTED: "interrupted"
5155
+ };
5156
+ var TESTING_RUN_STATE_INCOMPLETE_REASON = {
5157
+ MISSING_STATE: "missing-state",
5158
+ IO_ERROR: "io-error",
5159
+ PARSE_INVALID_STATE: "parse-invalid-state",
5160
+ SHAPE_INVALID_STATE: "shape-invalid-state"
5161
+ };
5162
+ var TESTING_RUN_STATE_ERROR = {
5163
+ RUN_DIRECTORY_COLLISION_LIMIT: "testing run directory collision limit exhausted",
5164
+ RUN_DIRECTORY_CREATE_FAILED: "testing run directory create failed",
5165
+ STATE_ALREADY_EXISTS: "testing run state already exists",
5166
+ STATE_WRITE_FAILED: "testing run state write failed"
5167
+ };
5168
+ var TEST_RUN_STATE_FIELDS = {
5169
+ BRANCH_NAME: "branchName",
5170
+ HEAD_SHA: "headSha",
5171
+ TESTING_CONFIG_DIGEST: "testingConfigDigest",
5172
+ RUNNER_OUTCOMES: "runnerOutcomes",
5173
+ DISCOVERED_TEST_PATHS_DIGEST: "discoveredTestPathsDigest",
5174
+ DISCOVERED_TEST_CONTENT_DIGEST: "discoveredTestContentDigest",
5175
+ PRODUCT_INPUT_DIGESTS: "productInputDigests",
5176
+ STARTED_AT: "startedAt",
5177
+ COMPLETED_AT: "completedAt",
5178
+ STATUS: "status"
5179
+ };
5180
+ var TEST_RUNNER_OUTCOME_FIELDS = {
5181
+ RUNNER_ID: "runnerId",
5182
+ TEST_PATHS: "testPaths",
5183
+ EXIT_CODE: "exitCode"
5184
+ };
5185
+ var PRODUCT_INPUT_DIGEST_FIELDS = {
5186
+ DESCRIPTOR_ID: "descriptorId",
5187
+ DIGEST: "digest"
5188
+ };
5189
+ var DEFAULT_TESTING_STORAGE = {
5190
+ spxDir: ".spx",
5191
+ localDir: "local",
5192
+ testingDir: "testing",
5193
+ runsDir: "runs",
5194
+ stateFile: "state.json"
5195
+ };
5196
+ var SHA256_ALGORITHM2 = "sha256";
5197
+ var HEX_ENCODING2 = "hex";
5198
+ var UTF8_ENCODING2 = "utf8";
5199
+ var RUN_ID_BYTES = 6;
5200
+ var TEMP_STATE_ID_BYTES = 6;
5201
+ var RUN_DIRECTORY_CREATE_ATTEMPTS = 10;
5202
+ var TEMP_STATE_FILE_PREFIX = ".state";
5203
+ var TEMP_STATE_FILE_SUFFIX = ".tmp";
5204
+ var JSON_INDENT_SPACES = 2;
5205
+ var SEGMENT_SEPARATOR = "-";
5206
+ var EXCLUSIVE_CREATE_FLAG = "wx";
5207
+ var ERROR_CODE_FILE_EXISTS = "EEXIST";
5208
+ var ERROR_CODE_NOT_FOUND2 = "ENOENT";
5209
+ var RUN_DIRECTORY_NAME_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
5210
+ var defaultFileSystem = {
5211
+ mkdir: async (path6, options) => {
5212
+ await nodeMkdir2(path6, options);
5213
+ },
5214
+ writeFile: nodeWriteFile2,
5215
+ rename: nodeRename2,
5216
+ readFile: nodeReadFile2,
5217
+ readdir: nodeReaddir2
5218
+ };
5219
+ function formatTestRunTimestamp(date) {
5220
+ return formatAuditRunTimestamp(date);
5221
+ }
5222
+ function testingRunsDir(productDir, storage = DEFAULT_TESTING_STORAGE) {
5223
+ return join13(productDir, storage.spxDir, storage.localDir, storage.testingDir, storage.runsDir);
5224
+ }
5225
+ async function createTestRunDirectory(productDir, options = {}) {
5226
+ const fs7 = options.fs ?? defaultFileSystem;
5227
+ const storage = options.storage ?? DEFAULT_TESTING_STORAGE;
5228
+ const maxAttempts = options.maxAttempts ?? RUN_DIRECTORY_CREATE_ATTEMPTS;
5229
+ const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
5230
+ const startedAt = formatTestRunTimestamp(startedDate);
5231
+ const runsDir = testingRunsDir(productDir, storage);
5232
+ const randomBytes = options.randomBytes ?? nodeRandomBytes2;
5233
+ try {
5234
+ await fs7.mkdir(runsDir, { recursive: true });
5235
+ } catch (error) {
5236
+ return { ok: false, error: `${TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_CREATE_FAILED}: ${toErrorMessage(error)}` };
5237
+ }
5238
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
5239
+ const runId = generateHexId(RUN_ID_BYTES, randomBytes);
5240
+ const runDirectoryName = `${startedAt}${SEGMENT_SEPARATOR}${runId}`;
5241
+ const runDir = join13(runsDir, runDirectoryName);
5242
+ try {
5243
+ await fs7.mkdir(runDir);
5244
+ return { ok: true, value: { runsDir, runDir, runDirectoryName, runId, startedAt } };
5245
+ } catch (error) {
5246
+ if (hasErrorCode2(error, ERROR_CODE_FILE_EXISTS)) continue;
5247
+ return { ok: false, error: `${TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_CREATE_FAILED}: ${toErrorMessage(error)}` };
5248
+ }
5249
+ }
5250
+ return { ok: false, error: TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_COLLISION_LIMIT };
4851
5251
  }
4852
- function formatText(projection) {
4853
- return projection.nodes.map((node) => formatTextNode(node)).join("\n");
5252
+ async function writeTerminalTestRunState(runDir, state, options = {}) {
5253
+ const fs7 = options.fs ?? defaultFileSystem;
5254
+ const storage = options.storage ?? DEFAULT_TESTING_STORAGE;
5255
+ const statePath = join13(runDir, storage.stateFile);
5256
+ try {
5257
+ await fs7.readFile(statePath, UTF8_ENCODING2);
5258
+ return { ok: false, error: TESTING_RUN_STATE_ERROR.STATE_ALREADY_EXISTS };
5259
+ } catch (error) {
5260
+ if (!hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5261
+ return { ok: false, error: `${TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED}: ${toErrorMessage(error)}` };
5262
+ }
5263
+ }
5264
+ const tempId = generateHexId(TEMP_STATE_ID_BYTES, options.randomBytes ?? nodeRandomBytes2);
5265
+ const tempPath = join13(runDir, `${TEMP_STATE_FILE_PREFIX}-${tempId}${TEMP_STATE_FILE_SUFFIX}`);
5266
+ const serialized = `${JSON.stringify(state, null, JSON_INDENT_SPACES)}
5267
+ `;
5268
+ try {
5269
+ await fs7.writeFile(tempPath, serialized, { flag: EXCLUSIVE_CREATE_FLAG });
5270
+ await fs7.rename(tempPath, statePath);
5271
+ return { ok: true, value: statePath };
5272
+ } catch (error) {
5273
+ return { ok: false, error: `${TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED}: ${toErrorMessage(error)}` };
5274
+ }
4854
5275
  }
4855
- function formatTextNode(node, depth = 0) {
4856
- const current = `${NODE_INDENT.repeat(depth)}${formatNodeLabel(node)}`;
4857
- const children = node.children.map((child) => formatTextNode(child, depth + 1));
4858
- return [current, ...children].join("\n");
5276
+ async function readTestingRuns(productDir, options = {}) {
5277
+ const fs7 = options.fs ?? defaultFileSystem;
5278
+ const storage = options.storage ?? DEFAULT_TESTING_STORAGE;
5279
+ const runsDir = testingRunsDir(productDir, storage);
5280
+ let entries;
5281
+ try {
5282
+ entries = await fs7.readdir(runsDir, { withFileTypes: true });
5283
+ } catch (error) {
5284
+ if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5285
+ return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
5286
+ }
5287
+ return { ok: false, error: toErrorMessage(error) };
5288
+ }
5289
+ const terminalRuns = [];
5290
+ const incompleteRuns = [];
5291
+ for (const entry of entries.filter(isTestRunDirectoryEntry)) {
5292
+ const runDir = join13(runsDir, entry.name);
5293
+ const statePath = join13(runDir, storage.stateFile);
5294
+ const stateResult = await readTestRunStatePath(statePath, fs7);
5295
+ if (stateResult.ok) {
5296
+ terminalRuns.push({ runDirectoryName: entry.name, runDir, statePath, state: stateResult.value });
5297
+ } else {
5298
+ incompleteRuns.push({
5299
+ runDirectoryName: entry.name,
5300
+ runDir,
5301
+ statePath,
5302
+ reason: stateResult.reason,
5303
+ ...stateResult.error === void 0 ? {} : { error: stateResult.error }
5304
+ });
5305
+ }
5306
+ }
5307
+ return { ok: true, value: { terminalRuns, incompleteRuns } };
4859
5308
  }
4860
- function formatMarkdown(projection) {
4861
- return projection.nodes.map((node) => formatMarkdownNode(node)).join("\n");
5309
+ function selectLatestTerminalTestRunForNode(runs, nodeTestPaths) {
5310
+ return runs.filter((run) => runCoversNode(run, nodeTestPaths)).reduce((latest, candidate) => {
5311
+ if (latest === void 0) return candidate;
5312
+ return compareTerminalRuns(latest, candidate) < 0 ? candidate : latest;
5313
+ }, void 0);
4862
5314
  }
4863
- function formatMarkdownNode(node, depth = 0) {
4864
- const current = `${NODE_INDENT.repeat(depth)}${MARKDOWN_NODE_PREFIX}${formatNodeLabel(node)}`;
4865
- const children = node.children.map((child) => formatMarkdownNode(child, depth + 1));
4866
- return [current, ...children].join("\n");
5315
+ function runCoversNode(run, nodeTestPaths) {
5316
+ return outcomesCoverPaths(run.state.runnerOutcomes, nodeTestPaths);
4867
5317
  }
4868
- function formatTable(projection) {
4869
- const rows = flattenProjectionNodes(projection.nodes).map((node) => [
4870
- KIND_REGISTRY[node.kind].label,
4871
- node.id,
4872
- node.state
4873
- ]);
4874
- return [
4875
- SPEC_STATUS_TABLE_HEADER,
4876
- formatTableRow([TABLE_HEADER_SEPARATOR, TABLE_HEADER_SEPARATOR, TABLE_HEADER_SEPARATOR]),
4877
- ...rows.map(formatTableRow)
4878
- ].join("\n");
5318
+ function outcomesCoverPaths(outcomes, nodeTestPaths) {
5319
+ if (nodeTestPaths.length === 0) return false;
5320
+ const executed = new Set(outcomes.flatMap((outcome) => outcome.testPaths));
5321
+ return nodeTestPaths.every((path6) => executed.has(path6));
4879
5322
  }
4880
- function flattenProjectionNodes(nodes) {
4881
- return nodes.flatMap((node) => [node, ...flattenProjectionNodes(node.children)]);
5323
+ function digestTestPaths(paths) {
5324
+ const normalized = [...new Set(paths)].sort(compareAsciiStrings);
5325
+ return sha256Hex(JSON.stringify(normalized));
4882
5326
  }
4883
- function formatTableRow(values) {
4884
- return `${TABLE_SEPARATOR} ${values.join(` ${TABLE_SEPARATOR} `)} ${TABLE_SEPARATOR}`;
5327
+ function digestTestContents(entries) {
5328
+ const normalized = [...entries].sort((left, right) => compareAsciiStrings(left.path, right.path)).map((entry) => [entry.path, entry.content]);
5329
+ return sha256Hex(JSON.stringify(normalized));
4885
5330
  }
4886
- function formatNodeLabel(node) {
4887
- return [
4888
- KIND_REGISTRY[node.kind].label,
4889
- node.id,
4890
- `[${node.state}]`
4891
- ].join(STATUS_SEPARATOR);
5331
+ function isStalenessMatch(recorded, current) {
5332
+ return recorded.testingConfigDigest === current.testingConfigDigest && recorded.discoveredTestPathsDigest === current.discoveredTestPathsDigest && recorded.discoveredTestContentDigest === current.discoveredTestContentDigest && productInputDigestsEqual(recorded.productInputDigests, current.productInputDigests);
4892
5333
  }
4893
-
4894
- // src/interfaces/cli/spec.ts
4895
- var SPEC_DOMAIN_CLI = {
4896
- COMMAND: "spec",
4897
- STATUS_COMMAND: "status",
4898
- NEXT_COMMAND: "next",
4899
- JSON_OPTION: "--json",
4900
- FORMAT_OPTION_FLAG: "--format",
4901
- FORMAT_OPTION_DEFINITION: "--format <format>"
4902
- };
4903
- var SPEC_STATUS_FORMAT_MESSAGE = {
4904
- INVALID_PREFIX: "Invalid format"
4905
- };
4906
- var VALID_STATUS_FORMATS = [
4907
- OUTPUT_FORMAT.TEXT,
4908
- OUTPUT_FORMAT.JSON,
4909
- OUTPUT_FORMAT.MARKDOWN,
4910
- OUTPUT_FORMAT.TABLE
4911
- ];
4912
- function handleCommandError(error) {
4913
- console.error("Error:", error instanceof Error ? error.message : String(error));
4914
- process.exit(1);
5334
+ function extractStalenessInputs(state) {
5335
+ return {
5336
+ testingConfigDigest: state.testingConfigDigest,
5337
+ discoveredTestPathsDigest: state.discoveredTestPathsDigest,
5338
+ discoveredTestContentDigest: state.discoveredTestContentDigest,
5339
+ productInputDigests: state.productInputDigests
5340
+ };
4915
5341
  }
4916
- function resolveStatusFormat(options) {
4917
- if (options.json === true) {
4918
- return "json";
4919
- }
4920
- if (options.format === void 0) {
4921
- return "text";
4922
- }
4923
- if (VALID_STATUS_FORMATS.includes(options.format)) {
4924
- return options.format;
5342
+ async function readTestRunStatePath(statePath, fs7) {
5343
+ let raw;
5344
+ try {
5345
+ raw = await fs7.readFile(statePath, UTF8_ENCODING2);
5346
+ } catch (error) {
5347
+ if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
5348
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE };
5349
+ }
5350
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR, error: toErrorMessage(error) };
4925
5351
  }
4926
- throw new Error(
4927
- `${SPEC_STATUS_FORMAT_MESSAGE.INVALID_PREFIX} "${options.format}". Must be one of: ${VALID_STATUS_FORMATS.join(", ")}`
5352
+ let parsed;
5353
+ try {
5354
+ parsed = JSON.parse(raw);
5355
+ } catch (error) {
5356
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE, error: toErrorMessage(error) };
5357
+ }
5358
+ const validated = validateTestRunState(parsed);
5359
+ if (!validated.ok) {
5360
+ return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.SHAPE_INVALID_STATE, error: validated.error };
5361
+ }
5362
+ return { ok: true, value: validated.value };
5363
+ }
5364
+ function validateTestRunState(value) {
5365
+ if (!isRecord4(value)) return { ok: false, error: "testing run state must be an object" };
5366
+ const branchName = readString(value, TEST_RUN_STATE_FIELDS.BRANCH_NAME);
5367
+ if (!branchName.ok) return branchName;
5368
+ const headSha = readString(value, TEST_RUN_STATE_FIELDS.HEAD_SHA);
5369
+ if (!headSha.ok) return headSha;
5370
+ const testingConfigDigest = readString(value, TEST_RUN_STATE_FIELDS.TESTING_CONFIG_DIGEST);
5371
+ if (!testingConfigDigest.ok) return testingConfigDigest;
5372
+ const runnerOutcomes = readRunnerOutcomes(value[TEST_RUN_STATE_FIELDS.RUNNER_OUTCOMES]);
5373
+ if (!runnerOutcomes.ok) return runnerOutcomes;
5374
+ const discoveredTestPathsDigest = readString(value, TEST_RUN_STATE_FIELDS.DISCOVERED_TEST_PATHS_DIGEST);
5375
+ if (!discoveredTestPathsDigest.ok) return discoveredTestPathsDigest;
5376
+ const discoveredTestContentDigest = readString(value, TEST_RUN_STATE_FIELDS.DISCOVERED_TEST_CONTENT_DIGEST);
5377
+ if (!discoveredTestContentDigest.ok) return discoveredTestContentDigest;
5378
+ const productInputDigests = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
5379
+ if (!productInputDigests.ok) return productInputDigests;
5380
+ const startedAt = readString(value, TEST_RUN_STATE_FIELDS.STARTED_AT);
5381
+ if (!startedAt.ok) return startedAt;
5382
+ const completedAt = readString(value, TEST_RUN_STATE_FIELDS.COMPLETED_AT);
5383
+ if (!completedAt.ok) return completedAt;
5384
+ const status = readStatus(value[TEST_RUN_STATE_FIELDS.STATUS]);
5385
+ if (!status.ok) return status;
5386
+ return {
5387
+ ok: true,
5388
+ value: {
5389
+ branchName: branchName.value,
5390
+ headSha: headSha.value,
5391
+ testingConfigDigest: testingConfigDigest.value,
5392
+ runnerOutcomes: runnerOutcomes.value,
5393
+ discoveredTestPathsDigest: discoveredTestPathsDigest.value,
5394
+ discoveredTestContentDigest: discoveredTestContentDigest.value,
5395
+ productInputDigests: productInputDigests.value,
5396
+ startedAt: startedAt.value,
5397
+ completedAt: completedAt.value,
5398
+ status: status.value
5399
+ }
5400
+ };
5401
+ }
5402
+ function readRunnerOutcomes(raw) {
5403
+ if (!Array.isArray(raw)) {
5404
+ return { ok: false, error: `${TEST_RUN_STATE_FIELDS.RUNNER_OUTCOMES} must be an array` };
5405
+ }
5406
+ const outcomes = [];
5407
+ for (const entry of raw) {
5408
+ if (!isRecord4(entry)) {
5409
+ return { ok: false, error: `${TEST_RUN_STATE_FIELDS.RUNNER_OUTCOMES} entries must be objects` };
5410
+ }
5411
+ const runnerId = readString(entry, TEST_RUNNER_OUTCOME_FIELDS.RUNNER_ID);
5412
+ if (!runnerId.ok) return runnerId;
5413
+ const testPaths = readStringArray(entry, TEST_RUNNER_OUTCOME_FIELDS.TEST_PATHS);
5414
+ if (!testPaths.ok) return testPaths;
5415
+ const exitCodeRaw = entry[TEST_RUNNER_OUTCOME_FIELDS.EXIT_CODE];
5416
+ if (typeof exitCodeRaw !== "number" || !Number.isInteger(exitCodeRaw)) {
5417
+ return { ok: false, error: `${TEST_RUNNER_OUTCOME_FIELDS.EXIT_CODE} must be an integer` };
5418
+ }
5419
+ outcomes.push({ runnerId: runnerId.value, testPaths: testPaths.value, exitCode: exitCodeRaw });
5420
+ }
5421
+ return { ok: true, value: outcomes };
5422
+ }
5423
+ function readProductInputDigests(raw) {
5424
+ if (!Array.isArray(raw)) {
5425
+ return { ok: false, error: `${TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS} must be an array` };
5426
+ }
5427
+ const digests = [];
5428
+ for (const entry of raw) {
5429
+ if (!isRecord4(entry)) {
5430
+ return { ok: false, error: `${TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS} entries must be objects` };
5431
+ }
5432
+ const descriptorId = readString(entry, PRODUCT_INPUT_DIGEST_FIELDS.DESCRIPTOR_ID);
5433
+ if (!descriptorId.ok) return descriptorId;
5434
+ const digest = readString(entry, PRODUCT_INPUT_DIGEST_FIELDS.DIGEST);
5435
+ if (!digest.ok) return digest;
5436
+ digests.push({ descriptorId: descriptorId.value, digest: digest.value });
5437
+ }
5438
+ return { ok: true, value: digests };
5439
+ }
5440
+ function readString(value, field) {
5441
+ const raw = value[field];
5442
+ return typeof raw === "string" && raw.length > 0 ? { ok: true, value: raw } : { ok: false, error: `${field} must be a non-empty string` };
5443
+ }
5444
+ function readStringArray(value, field) {
5445
+ const raw = value[field];
5446
+ return Array.isArray(raw) && raw.every((entry) => typeof entry === "string" && entry.length > 0) ? { ok: true, value: raw } : { ok: false, error: `${field} must be an array of non-empty strings` };
5447
+ }
5448
+ function readStatus(raw) {
5449
+ return isTestRunStateStatus(raw) ? { ok: true, value: raw } : { ok: false, error: `${TEST_RUN_STATE_FIELDS.STATUS} must be a terminal testing status` };
5450
+ }
5451
+ function isTestRunStateStatus(value) {
5452
+ return typeof value === "string" && Object.values(TEST_RUN_STATE_STATUS).includes(value);
5453
+ }
5454
+ function compareTerminalRuns(left, right) {
5455
+ const completed = compareAsciiStrings(left.state.completedAt, right.state.completedAt);
5456
+ if (completed !== 0) return completed;
5457
+ const started = compareAsciiStrings(left.state.startedAt, right.state.startedAt);
5458
+ if (started !== 0) return started;
5459
+ return compareAsciiStrings(left.runDirectoryName, right.runDirectoryName);
5460
+ }
5461
+ function productInputDigestsEqual(left, right) {
5462
+ return canonicalProductInputDigests(left) === canonicalProductInputDigests(right);
5463
+ }
5464
+ function canonicalProductInputDigests(digests) {
5465
+ const normalized = [...digests].map((digest) => [digest.descriptorId, digest.digest]).sort((left, right) => compareAsciiStrings(left.join(SEGMENT_SEPARATOR), right.join(SEGMENT_SEPARATOR)));
5466
+ return JSON.stringify(normalized);
5467
+ }
5468
+ function compareAsciiStrings(left, right) {
5469
+ if (left < right) return -1;
5470
+ if (left > right) return 1;
5471
+ return 0;
5472
+ }
5473
+ function isTestRunDirectoryEntry(entry) {
5474
+ return entry.isDirectory() && RUN_DIRECTORY_NAME_PATTERN.test(entry.name);
5475
+ }
5476
+ function isRecord4(value) {
5477
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5478
+ }
5479
+ function sha256Hex(value) {
5480
+ return createHash3(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING2);
5481
+ }
5482
+ function generateHexId(size, randomBytes) {
5483
+ return randomBytes(size).toString(HEX_ENCODING2);
5484
+ }
5485
+ function hasErrorCode2(error, code) {
5486
+ return isRecord4(error) && error.code === code;
5487
+ }
5488
+ function toErrorMessage(error) {
5489
+ return error instanceof Error ? error.message : String(error);
5490
+ }
5491
+
5492
+ // src/commands/testing/run-command.ts
5493
+ var NO_GIT_IDENTITY = "(unknown)";
5494
+ var TEXT_ENCODING = "utf8";
5495
+ var NO_PRODUCT_INPUT_DIGESTS = [];
5496
+ function resolveRecordingDependencies(deps) {
5497
+ return {
5498
+ fs: deps.fs ?? defaultFileSystem,
5499
+ now: deps.now ?? (() => /* @__PURE__ */ new Date()),
5500
+ git: deps.git
5501
+ };
5502
+ }
5503
+ async function resolveTestingConfig(productDir) {
5504
+ const loaded = await resolveConfig(productDir, [testingConfigDescriptor]);
5505
+ if (!loaded.ok) {
5506
+ throw new Error(`failed to resolve testing config: ${loaded.error}`);
5507
+ }
5508
+ const config = loaded.value[TESTING_SECTION];
5509
+ const digest = digestDescriptorSection(config);
5510
+ if (!digest.ok) {
5511
+ throw new Error(`failed to digest testing config: ${digest.error}`);
5512
+ }
5513
+ return { config, digest: digest.value.sha256 };
5514
+ }
5515
+ function coveredTestPaths(dispatch) {
5516
+ return dispatch.outcomes.flatMap((outcome) => outcome.testPaths);
5517
+ }
5518
+ async function readCoveredContents(productDir, paths, fs7) {
5519
+ const entries = [];
5520
+ for (const path6 of paths) {
5521
+ entries.push({ path: path6, content: await fs7.readFile(join14(productDir, path6), TEXT_ENCODING) });
5522
+ }
5523
+ return entries;
5524
+ }
5525
+ function deriveStatus(outcomes) {
5526
+ const allPassed = outcomes.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE);
5527
+ return allPassed ? TEST_RUN_STATE_STATUS.PASSED : TEST_RUN_STATE_STATUS.FAILED;
5528
+ }
5529
+ async function currentStalenessInputs(productDir, coveredPaths, deps = {}) {
5530
+ const fs7 = deps.fs ?? defaultFileSystem;
5531
+ const testingConfigDigest = deps.testingConfigDigest ?? (await resolveTestingConfig(productDir)).digest;
5532
+ const contents = await readCoveredContents(productDir, coveredPaths, fs7);
5533
+ return {
5534
+ testingConfigDigest,
5535
+ discoveredTestPathsDigest: digestTestPaths(coveredPaths),
5536
+ discoveredTestContentDigest: digestTestContents(contents),
5537
+ productInputDigests: NO_PRODUCT_INPUT_DIGESTS
5538
+ };
5539
+ }
5540
+ async function recordRun(directory, productDir, dispatch, recording, testingConfigDigest) {
5541
+ const staleness = await currentStalenessInputs(productDir, coveredTestPaths(dispatch), {
5542
+ fs: recording.fs,
5543
+ testingConfigDigest
5544
+ });
5545
+ const branchName = await getCurrentBranch(productDir, recording.git) ?? NO_GIT_IDENTITY;
5546
+ const headSha = await getHeadSha(productDir, recording.git) ?? NO_GIT_IDENTITY;
5547
+ const state = {
5548
+ branchName,
5549
+ headSha,
5550
+ testingConfigDigest: staleness.testingConfigDigest,
5551
+ runnerOutcomes: dispatch.outcomes,
5552
+ discoveredTestPathsDigest: staleness.discoveredTestPathsDigest,
5553
+ discoveredTestContentDigest: staleness.discoveredTestContentDigest,
5554
+ productInputDigests: staleness.productInputDigests,
5555
+ startedAt: directory.startedAt,
5556
+ completedAt: formatTestRunTimestamp(recording.now()),
5557
+ status: deriveStatus(dispatch.outcomes)
5558
+ };
5559
+ const written = await writeTerminalTestRunState(directory.runDir, state, { fs: recording.fs });
5560
+ if (!written.ok) {
5561
+ throw new Error(`failed to record test run: ${written.error}`);
5562
+ }
5563
+ return state;
5564
+ }
5565
+ async function reserveRunDirectory(productDir, recording) {
5566
+ const directory = await createTestRunDirectory(productDir, { fs: recording.fs, now: recording.now });
5567
+ if (!directory.ok) {
5568
+ throw new Error(`failed to create test run directory: ${directory.error}`);
5569
+ }
5570
+ return directory.value;
5571
+ }
5572
+ async function runTestsCommand(options, deps) {
5573
+ const { config, digest } = await resolveTestingConfig(options.productDir);
5574
+ const passingScope = options.passing ? config.passingScope : void 0;
5575
+ const recording = resolveRecordingDependencies(deps);
5576
+ const directory = await reserveRunDirectory(options.productDir, recording);
5577
+ const dispatch = await runTests(
5578
+ { productDir: options.productDir, registry: deps.registry, passingScope },
5579
+ { runnerDepsFor: deps.runnerDepsFor }
5580
+ );
5581
+ const recorded = await recordRun(directory, options.productDir, dispatch, recording, digest);
5582
+ return { dispatch, recorded };
5583
+ }
5584
+ async function runNodeCommand(options, deps) {
5585
+ const recording = resolveRecordingDependencies(deps);
5586
+ const directory = await reserveRunDirectory(options.productDir, recording);
5587
+ const dispatch = await runTests(
5588
+ { productDir: options.productDir, registry: deps.registry, passingScope: { include: [options.nodePath] } },
5589
+ { runnerDepsFor: deps.runnerDepsFor }
5590
+ );
5591
+ const recorded = await recordRun(directory, options.productDir, dispatch, recording);
5592
+ return { dispatch, recorded };
5593
+ }
5594
+
5595
+ // src/commands/spec/node-outcome-resolver.ts
5596
+ var PATH_SEPARATOR = "/";
5597
+ function createNodeOutcomeResolver(deps) {
5598
+ let evidence;
5599
+ const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
5600
+ return async (nodeId) => {
5601
+ const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
5602
+ const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
5603
+ const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
5604
+ const usable = await usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, nodeTestPaths);
5605
+ if (usable !== void 0) {
5606
+ return usable;
5607
+ }
5608
+ const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
5609
+ return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
5610
+ };
5611
+ }
5612
+ async function loadResolverEvidence(deps) {
5613
+ const discoveredTestPaths = await discoverTestFiles(deps.productDir);
5614
+ const runs = await readTestingRuns(deps.productDir, deps);
5615
+ return { discoveredTestPaths, terminalRuns: runs.ok ? runs.value.terminalRuns : [] };
5616
+ }
5617
+ function filterNodeTestPaths(discoveredTestPaths, nodePath) {
5618
+ const prefix = `${nodePath}${PATH_SEPARATOR}`;
5619
+ return discoveredTestPaths.filter((path6) => path6.startsWith(prefix));
5620
+ }
5621
+ async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, nodeTestPaths) {
5622
+ const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
5623
+ if (latest === void 0) {
5624
+ return void 0;
5625
+ }
5626
+ const runCoveredPaths = latest.state.runnerOutcomes.flatMap((outcome) => outcome.testPaths);
5627
+ const presentTestPaths = new Set(discoveredTestPaths);
5628
+ if (!runCoveredPaths.every((path6) => presentTestPaths.has(path6))) {
5629
+ return void 0;
5630
+ }
5631
+ const current = await currentStalenessInputs(deps.productDir, runCoveredPaths, deps);
5632
+ const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
5633
+ return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
5634
+ }
5635
+
5636
+ // src/lib/node-status/classify.ts
5637
+ var NODE_STATUS_STATUS_KEY = "status";
5638
+ var NODE_STATUS_JSON_INDENT = 2;
5639
+ function classifyNodeStatus(facts) {
5640
+ if (!facts.hasTests) return SPEC_TREE_NODE_STATE.DECLARED;
5641
+ if (facts.isExcluded) return SPEC_TREE_NODE_STATE.SPECIFIED;
5642
+ if (facts.testsPass) return SPEC_TREE_NODE_STATE.PASSING;
5643
+ return SPEC_TREE_NODE_STATE.FAILING;
5644
+ }
5645
+ function serializeNodeStatus(state) {
5646
+ return `${JSON.stringify({ [NODE_STATUS_STATUS_KEY]: state }, null, NODE_STATUS_JSON_INDENT)}
5647
+ `;
5648
+ }
5649
+
5650
+ // src/lib/node-status/provider.ts
5651
+ import { join as join16 } from "path";
5652
+
5653
+ // src/lib/node-status/read.ts
5654
+ import { readFileSync as readFileSync2 } from "fs";
5655
+ import { join as join15 } from "path";
5656
+ var NODE_STATUS_FILENAME = "spx.status.json";
5657
+ var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
5658
+ function isNodeError2(error) {
5659
+ return error instanceof Error && "code" in error;
5660
+ }
5661
+ function isSpecTreeNodeState(value) {
5662
+ return typeof value === "string" && NODE_STATUS_VALUES.has(value);
5663
+ }
5664
+ function readNodeStatus(nodeDir) {
5665
+ const filePath = join15(nodeDir, NODE_STATUS_FILENAME);
5666
+ let content;
5667
+ try {
5668
+ content = readFileSync2(filePath, "utf8");
5669
+ } catch (error) {
5670
+ if (isNodeError2(error) && error.code === "ENOENT") {
5671
+ return void 0;
5672
+ }
5673
+ throw error;
5674
+ }
5675
+ const parsed = JSON.parse(content);
5676
+ const status = parsed[NODE_STATUS_STATUS_KEY];
5677
+ if (!isSpecTreeNodeState(status)) {
5678
+ throw new Error(
5679
+ `Invalid ${NODE_STATUS_FILENAME} at ${filePath}: status "${String(status)}" is not a lifecycle state`
5680
+ );
5681
+ }
5682
+ return status;
5683
+ }
5684
+
5685
+ // src/lib/node-status/provider.ts
5686
+ function nodeDirectory(productDir, node) {
5687
+ return join16(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
5688
+ }
5689
+ function createNodeStatusProvider(productDir) {
5690
+ return {
5691
+ stateForNode(node) {
5692
+ return readNodeStatus(nodeDirectory(productDir, node));
5693
+ }
5694
+ };
5695
+ }
5696
+
5697
+ // src/lib/node-status/update.ts
5698
+ import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
5699
+ import { dirname as dirname3, join as join17 } from "path";
5700
+ var NODE_STATUS_TEXT_ENCODING = "utf8";
5701
+ async function updateNodeStatus(options) {
5702
+ const { productDir, resolveOutcome } = options;
5703
+ const snapshot = await readSpecTree({ source: createFilesystemSpecTreeSource({ productDir }) });
5704
+ const ignoreReader = createIgnoreSourceReader(productDir, {
5705
+ ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
5706
+ specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
5707
+ });
5708
+ const nodesWithTests = collectNodesWithTests(snapshot);
5709
+ for (const node of snapshot.allNodes) {
5710
+ const facts = await classifyFacts(node, {
5711
+ hasTests: nodesWithTests.has(node.id),
5712
+ isExcluded: isNodeExcluded(ignoreReader, node),
5713
+ resolveOutcome
5714
+ });
5715
+ await writeNodeStatus(productDir, node.id, classifyNodeStatus(facts));
5716
+ }
5717
+ }
5718
+ async function classifyFacts(node, input) {
5719
+ const testsPass = input.hasTests && !input.isExcluded ? await input.resolveOutcome(node.id) : false;
5720
+ return { hasTests: input.hasTests, isExcluded: input.isExcluded, testsPass };
5721
+ }
5722
+ function collectNodesWithTests(snapshot) {
5723
+ const parents = /* @__PURE__ */ new Set();
5724
+ for (const entry of snapshot.entries) {
5725
+ if (entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE) {
5726
+ parents.add(entry.parentId);
5727
+ }
5728
+ }
5729
+ return parents;
5730
+ }
5731
+ function isNodeExcluded(ignoreReader, node) {
5732
+ const reference = node.ref?.path;
5733
+ if (reference === void 0) return false;
5734
+ return ignoreReader.isUnderIgnoreSource(reference);
5735
+ }
5736
+ async function writeNodeStatus(productDir, nodeId, state) {
5737
+ const filePath = join17(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
5738
+ await mkdir4(dirname3(filePath), { recursive: true });
5739
+ await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
5740
+ }
5741
+
5742
+ // src/commands/spec/status.ts
5743
+ var OUTPUT_FORMAT = {
5744
+ TEXT: "text",
5745
+ JSON: "json",
5746
+ MARKDOWN: "markdown",
5747
+ TABLE: "table"
5748
+ };
5749
+ var SPEC_STATUS_MESSAGE = {
5750
+ EMPTY: `No spec-tree nodes found in ${SPEC_TREE_CONFIG.ROOT_DIRECTORY}`
5751
+ };
5752
+ var DEFAULT_FORMAT = OUTPUT_FORMAT.TEXT;
5753
+ var JSON_INDENTATION = 2;
5754
+ var STATUS_SEPARATOR = " ";
5755
+ var NODE_INDENT = " ";
5756
+ var MARKDOWN_NODE_PREFIX = "- ";
5757
+ var TABLE_SEPARATOR = "|";
5758
+ var TABLE_HEADER_SEPARATOR = "---";
5759
+ var TABLE_HEADER = {
5760
+ KIND: "Kind",
5761
+ PATH: "Path",
5762
+ STATE: "State"
5763
+ };
5764
+ var SPEC_STATUS_TABLE_HEADER = formatTableRow([
5765
+ TABLE_HEADER.KIND,
5766
+ TABLE_HEADER.PATH,
5767
+ TABLE_HEADER.STATE
5768
+ ]);
5769
+ async function statusCommand(options = {}) {
5770
+ if (options.source !== void 0) {
5771
+ return renderSpecStatus(projectSpecTree(await readSpecTree({ source: options.source })), options.format);
5772
+ }
5773
+ const productDir = await resolveSpecProductDir(
5774
+ options.cwd ?? process.cwd(),
5775
+ options.gitDependencies,
5776
+ options.onWarning
5777
+ );
5778
+ if (options.update === true) {
5779
+ await updateNodeStatus({ productDir, resolveOutcome: options.resolveOutcomeFor(productDir) });
5780
+ }
5781
+ const snapshot = await readSpecTree({
5782
+ source: createFilesystemSpecTreeSource({ productDir }),
5783
+ evidence: createNodeStatusProvider(productDir)
5784
+ });
5785
+ return renderSpecStatus(projectSpecTree(snapshot), options.format);
5786
+ }
5787
+ function renderSpecStatus(projection, format2 = DEFAULT_FORMAT) {
5788
+ if (projection.nodes.length === 0 && format2 !== OUTPUT_FORMAT.JSON) {
5789
+ return SPEC_STATUS_MESSAGE.EMPTY;
5790
+ }
5791
+ switch (format2) {
5792
+ case OUTPUT_FORMAT.JSON:
5793
+ return formatJSON(projection);
5794
+ case OUTPUT_FORMAT.MARKDOWN:
5795
+ return formatMarkdown(projection);
5796
+ case OUTPUT_FORMAT.TABLE:
5797
+ return formatTable(projection);
5798
+ case OUTPUT_FORMAT.TEXT:
5799
+ return formatText(projection);
5800
+ default: {
5801
+ const unsupportedFormat = format2;
5802
+ throw new RangeError(`Unsupported spec status output format: ${unsupportedFormat}`);
5803
+ }
5804
+ }
5805
+ }
5806
+ function formatJSON(projection) {
5807
+ return JSON.stringify(projection, null, JSON_INDENTATION);
5808
+ }
5809
+ function formatText(projection) {
5810
+ return projection.nodes.map((node) => formatTextNode(node)).join("\n");
5811
+ }
5812
+ function formatTextNode(node, depth = 0) {
5813
+ const current = `${NODE_INDENT.repeat(depth)}${formatNodeLabel(node)}`;
5814
+ const children = node.children.map((child) => formatTextNode(child, depth + 1));
5815
+ return [current, ...children].join("\n");
5816
+ }
5817
+ function formatMarkdown(projection) {
5818
+ return projection.nodes.map((node) => formatMarkdownNode(node)).join("\n");
5819
+ }
5820
+ function formatMarkdownNode(node, depth = 0) {
5821
+ const current = `${NODE_INDENT.repeat(depth)}${MARKDOWN_NODE_PREFIX}${formatNodeLabel(node)}`;
5822
+ const children = node.children.map((child) => formatMarkdownNode(child, depth + 1));
5823
+ return [current, ...children].join("\n");
5824
+ }
5825
+ function formatTable(projection) {
5826
+ const rows = flattenProjectionNodes(projection.nodes).map((node) => [
5827
+ KIND_REGISTRY[node.kind].label,
5828
+ node.id,
5829
+ node.state
5830
+ ]);
5831
+ return [
5832
+ SPEC_STATUS_TABLE_HEADER,
5833
+ formatTableRow([TABLE_HEADER_SEPARATOR, TABLE_HEADER_SEPARATOR, TABLE_HEADER_SEPARATOR]),
5834
+ ...rows.map(formatTableRow)
5835
+ ].join("\n");
5836
+ }
5837
+ function flattenProjectionNodes(nodes) {
5838
+ return nodes.flatMap((node) => [node, ...flattenProjectionNodes(node.children)]);
5839
+ }
5840
+ function formatTableRow(values) {
5841
+ return `${TABLE_SEPARATOR} ${values.join(` ${TABLE_SEPARATOR} `)} ${TABLE_SEPARATOR}`;
5842
+ }
5843
+ function formatNodeLabel(node) {
5844
+ return [
5845
+ KIND_REGISTRY[node.kind].label,
5846
+ node.id,
5847
+ `[${node.state}]`
5848
+ ].join(STATUS_SEPARATOR);
5849
+ }
5850
+
5851
+ // src/testing/languages/python.ts
5852
+ import { basename } from "path";
5853
+ var PYTHON_TESTING_LANGUAGE_NAME = "python";
5854
+ var PYTHON_TEST_FILE_PREFIX = "test_";
5855
+ var PYTHON_TEST_FILE_EXTENSION = ".py";
5856
+ var PYTHON_TEST_FILE_PATTERNS = [`${PYTHON_TEST_FILE_PREFIX}*${PYTHON_TEST_FILE_EXTENSION}`];
5857
+ var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
5858
+ var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
5859
+ var UV_COMMAND = "uv";
5860
+ var PYTEST_INVOKE_ARGS = ["run", "pytest"];
5861
+ function matchesTestFile(filePath) {
5862
+ return basename(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
5863
+ }
5864
+ function excludeFlag(nodePath) {
5865
+ return `${PYTHON_PYTEST_IGNORE_FLAG_PREFIX}${nodePath}${PYTHON_PYTEST_IGNORE_FLAG_SUFFIX}`;
5866
+ }
5867
+ function detect(projectRoot, deps) {
5868
+ return deps.isLanguagePresent(projectRoot);
5869
+ }
5870
+ async function runTests2(request, deps) {
5871
+ if (!deps.isLanguagePresent(request.projectRoot)) {
5872
+ return { invoked: false };
5873
+ }
5874
+ const args = [
5875
+ ...PYTEST_INVOKE_ARGS,
5876
+ ...request.testPaths,
5877
+ ...request.excludedNodePaths.map(excludeFlag)
5878
+ ];
5879
+ const result = await deps.runCommand(UV_COMMAND, args);
5880
+ return { invoked: true, exitCode: result.exitCode };
5881
+ }
5882
+ var pythonTestingLanguage = {
5883
+ name: PYTHON_TESTING_LANGUAGE_NAME,
5884
+ testFilePatterns: PYTHON_TEST_FILE_PATTERNS,
5885
+ matchesTestFile,
5886
+ excludeFlag,
5887
+ detect,
5888
+ runTests: runTests2
5889
+ };
5890
+
5891
+ // src/testing/languages/typescript.ts
5892
+ var TYPESCRIPT_TESTING_LANGUAGE_NAME = "typescript";
5893
+ var TYPESCRIPT_TEST_FILE_PATTERNS = ["*.test.ts", "*.test.tsx"];
5894
+ var TYPESCRIPT_TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx"];
5895
+ var TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX = "--exclude=spx/";
5896
+ var TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX = "/**";
5897
+ var PACKAGE_MANAGER_COMMAND = "pnpm";
5898
+ var VITEST_INVOKE_ARGS = ["exec", "vitest", "run"];
5899
+ var VITEST_ROOT_FLAG = "--root";
5900
+ function matchesTestFile2(filePath) {
5901
+ return TYPESCRIPT_TEST_FILE_SUFFIXES.some((suffix) => filePath.endsWith(suffix));
5902
+ }
5903
+ function excludeFlag2(nodePath) {
5904
+ return `${TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX}${nodePath}${TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX}`;
5905
+ }
5906
+ function detect2(projectRoot, deps) {
5907
+ return deps.isLanguagePresent(projectRoot);
5908
+ }
5909
+ async function runTests3(request, deps) {
5910
+ if (!deps.isLanguagePresent(request.projectRoot)) {
5911
+ return { invoked: false };
5912
+ }
5913
+ const args = [
5914
+ ...VITEST_INVOKE_ARGS,
5915
+ VITEST_ROOT_FLAG,
5916
+ request.projectRoot,
5917
+ ...request.testPaths,
5918
+ ...request.excludedNodePaths.map(excludeFlag2)
5919
+ ];
5920
+ const result = await deps.runCommand(PACKAGE_MANAGER_COMMAND, args);
5921
+ return { invoked: true, exitCode: result.exitCode };
5922
+ }
5923
+ var typescriptTestingLanguage = {
5924
+ name: TYPESCRIPT_TESTING_LANGUAGE_NAME,
5925
+ testFilePatterns: TYPESCRIPT_TEST_FILE_PATTERNS,
5926
+ matchesTestFile: matchesTestFile2,
5927
+ excludeFlag: excludeFlag2,
5928
+ detect: detect2,
5929
+ runTests: runTests3
5930
+ };
5931
+
5932
+ // src/testing/registry.ts
5933
+ var testingRegistry = {
5934
+ languages: [typescriptTestingLanguage, pythonTestingLanguage]
5935
+ };
5936
+
5937
+ // src/lib/process-lifecycle/exit-codes.ts
5938
+ var SIGINT_EXIT_CODE = 130;
5939
+ var SIGTERM_EXIT_CODE = 143;
5940
+ var EPIPE_EXIT_CODE = 0;
5941
+ var UNCAUGHT_EXIT_CODE = 1;
5942
+
5943
+ // src/lib/process-lifecycle/handlers.ts
5944
+ var SIGINT_NAME = "SIGINT";
5945
+ var SIGTERM_NAME = "SIGTERM";
5946
+ function createHandlers(deps) {
5947
+ let cleanupOnce = false;
5948
+ function killEachChild(signal) {
5949
+ deps.registry.forEach((child) => {
5950
+ if (!child.killed) child.kill(signal);
5951
+ });
5952
+ }
5953
+ function exitOnce(code, killSignal) {
5954
+ if (cleanupOnce) {
5955
+ deps.exitController.exit(code);
5956
+ return;
5957
+ }
5958
+ cleanupOnce = true;
5959
+ if (killSignal !== void 0) killEachChild(killSignal);
5960
+ deps.exitController.exit(code);
5961
+ }
5962
+ return {
5963
+ onSigint() {
5964
+ exitOnce(SIGINT_EXIT_CODE, SIGINT_NAME);
5965
+ },
5966
+ onSigterm() {
5967
+ exitOnce(SIGTERM_EXIT_CODE, SIGTERM_NAME);
5968
+ },
5969
+ onEpipe() {
5970
+ exitOnce(EPIPE_EXIT_CODE, SIGTERM_NAME);
5971
+ },
5972
+ onUncaught(_error) {
5973
+ exitOnce(UNCAUGHT_EXIT_CODE, SIGTERM_NAME);
5974
+ }
5975
+ };
5976
+ }
5977
+
5978
+ // src/lib/process-lifecycle/install.ts
5979
+ import { spawn } from "child_process";
5980
+
5981
+ // src/lib/process-lifecycle/registry.ts
5982
+ function createRegistry() {
5983
+ const tracked = /* @__PURE__ */ new Set();
5984
+ return {
5985
+ add(child) {
5986
+ tracked.add(child);
5987
+ },
5988
+ remove(child) {
5989
+ tracked.delete(child);
5990
+ },
5991
+ forEach(fn) {
5992
+ for (const child of tracked) fn(child);
5993
+ },
5994
+ get size() {
5995
+ return tracked.size;
5996
+ }
5997
+ };
5998
+ }
5999
+
6000
+ // src/lib/process-lifecycle/runner.ts
6001
+ function createLifecycleRunner(deps) {
6002
+ return {
6003
+ spawn(command, args, options) {
6004
+ const child = deps.spawn(command, args, options);
6005
+ deps.registry.add(child);
6006
+ child.on("exit", () => deps.registry.remove(child));
6007
+ return child;
6008
+ }
6009
+ };
6010
+ }
6011
+
6012
+ // src/lib/process-lifecycle/install.ts
6013
+ var moduleRegistry = createRegistry();
6014
+ var moduleExitController = {
6015
+ exit(code) {
6016
+ process.exit(code);
6017
+ }
6018
+ };
6019
+ var installed = false;
6020
+ var lifecycleProcessRunner = createLifecycleRunner({
6021
+ registry: moduleRegistry,
6022
+ spawn
6023
+ });
6024
+ var EPIPE_CODE = "EPIPE";
6025
+ var UNCAUGHT_PREFIX = "Uncaught: ";
6026
+ var NEWLINE = "\n";
6027
+ function formatUncaught(error) {
6028
+ if (error instanceof Error && error.stack !== void 0) {
6029
+ return UNCAUGHT_PREFIX + error.stack + NEWLINE;
6030
+ }
6031
+ return UNCAUGHT_PREFIX + String(error) + NEWLINE;
6032
+ }
6033
+ function logUncaughtToStderr(error) {
6034
+ try {
6035
+ process.stderr.write(formatUncaught(error));
6036
+ } catch {
6037
+ }
6038
+ }
6039
+ function installLifecycle() {
6040
+ if (installed) return;
6041
+ installed = true;
6042
+ const handlers = createHandlers({
6043
+ registry: moduleRegistry,
6044
+ exitController: moduleExitController
6045
+ });
6046
+ process.on("uncaughtException", (error) => {
6047
+ logUncaughtToStderr(error);
6048
+ handlers.onUncaught(error);
6049
+ });
6050
+ process.on("unhandledRejection", (reason) => {
6051
+ logUncaughtToStderr(reason);
6052
+ handlers.onUncaught(reason);
6053
+ });
6054
+ process.on("SIGTERM", () => handlers.onSigterm());
6055
+ process.on("SIGINT", () => handlers.onSigint());
6056
+ process.stdout.on("error", (error) => {
6057
+ if (error.code === EPIPE_CODE) {
6058
+ handlers.onEpipe();
6059
+ return;
6060
+ }
6061
+ handlers.onUncaught(error);
6062
+ });
6063
+ process.stderr.on("error", (error) => {
6064
+ if (error.code === EPIPE_CODE) {
6065
+ handlers.onEpipe();
6066
+ return;
6067
+ }
6068
+ handlers.onUncaught(error);
6069
+ });
6070
+ }
6071
+
6072
+ // src/lib/process-lifecycle/managed-subprocess.ts
6073
+ var MANAGED_SUBPROCESS_STDIO = "pipe";
6074
+ function spawnManagedSubprocess(runner, command, args, options) {
6075
+ return runner.spawn(command, args, {
6076
+ ...options,
6077
+ stdio: MANAGED_SUBPROCESS_STDIO
6078
+ });
6079
+ }
6080
+
6081
+ // src/validation/discovery/language-finder.ts
6082
+ import fs5 from "fs";
6083
+ import path4 from "path";
6084
+ var TYPESCRIPT_MARKER = "tsconfig.json";
6085
+ var PYTHON_MARKER = "pyproject.toml";
6086
+ var ESLINT_CONFIG_FILES = [
6087
+ "eslint.config.ts",
6088
+ "eslint.config.js",
6089
+ "eslint.config.mjs",
6090
+ "eslint.config.cjs"
6091
+ ];
6092
+ var ESLINT_PRODUCTION_CONFIG_FILES = [
6093
+ "eslint.config.production.ts",
6094
+ "eslint.config.production.js",
6095
+ "eslint.config.production.mjs",
6096
+ "eslint.config.production.cjs"
6097
+ ];
6098
+ var defaultLanguageDetectionDeps = {
6099
+ existsSync: fs5.existsSync
6100
+ };
6101
+ function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
6102
+ const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
6103
+ if (!present) {
6104
+ return { present: false };
6105
+ }
6106
+ const eslintConfigFile = ESLINT_CONFIG_FILES.find(
6107
+ (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6108
+ );
6109
+ const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
6110
+ (configFile) => deps.existsSync(path4.join(projectRoot, configFile))
6111
+ );
6112
+ return { present: true, eslintConfigFile, productionEslintConfigFile };
6113
+ }
6114
+ function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
6115
+ const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
6116
+ return { present };
6117
+ }
6118
+
6119
+ // src/interfaces/cli/testing-runner-deps.ts
6120
+ var PROCESS_FAILURE_EXIT_CODE = 1;
6121
+ var NO_PRESENCE_DETECTOR_ERROR = "no presence detector configured for testing language";
6122
+ var PRESENCE_BY_LANGUAGE_NAME = {
6123
+ [typescriptTestingLanguage.name]: (productDir) => detectTypeScript(productDir).present,
6124
+ [pythonTestingLanguage.name]: (productDir) => detectPython(productDir).present
6125
+ };
6126
+ function createCommandRunner(productDir, outStream) {
6127
+ return (command, args) => new Promise((resolveResult) => {
6128
+ const child = spawnManagedSubprocess(lifecycleProcessRunner, command, args, {
6129
+ cwd: productDir
6130
+ });
6131
+ child.stdout?.pipe(outStream);
6132
+ child.stderr?.pipe(process.stderr);
6133
+ child.on("close", (code) => resolveResult({ exitCode: code ?? PROCESS_FAILURE_EXIT_CODE }));
6134
+ child.on("error", () => resolveResult({ exitCode: PROCESS_FAILURE_EXIT_CODE }));
6135
+ });
6136
+ }
6137
+ function createRunnerDepsFor(productDir, outStream = process.stdout) {
6138
+ const runCommand = createCommandRunner(productDir, outStream);
6139
+ return (language) => {
6140
+ const isLanguagePresent = PRESENCE_BY_LANGUAGE_NAME[language.name];
6141
+ if (isLanguagePresent === void 0) {
6142
+ throw new Error(`${NO_PRESENCE_DETECTOR_ERROR}: ${language.name}`);
6143
+ }
6144
+ return { isLanguagePresent, runCommand };
6145
+ };
6146
+ }
6147
+
6148
+ // src/interfaces/cli/spec.ts
6149
+ var SPEC_DOMAIN_CLI = {
6150
+ COMMAND: "spec",
6151
+ STATUS_COMMAND: "status",
6152
+ NEXT_COMMAND: "next",
6153
+ JSON_OPTION: "--json",
6154
+ FORMAT_OPTION_FLAG: "--format",
6155
+ FORMAT_OPTION_DEFINITION: "--format <format>",
6156
+ UPDATE_OPTION: "--update"
6157
+ };
6158
+ var SPEC_STATUS_FORMAT_MESSAGE = {
6159
+ INVALID_PREFIX: "Invalid format"
6160
+ };
6161
+ var VALID_STATUS_FORMATS = [
6162
+ OUTPUT_FORMAT.TEXT,
6163
+ OUTPUT_FORMAT.JSON,
6164
+ OUTPUT_FORMAT.MARKDOWN,
6165
+ OUTPUT_FORMAT.TABLE
6166
+ ];
6167
+ function handleCommandError(error) {
6168
+ console.error("Error:", error instanceof Error ? error.message : String(error));
6169
+ process.exit(1);
6170
+ }
6171
+ function resolveStatusFormat(options) {
6172
+ if (options.json === true) {
6173
+ return "json";
6174
+ }
6175
+ if (options.format === void 0) {
6176
+ return "text";
6177
+ }
6178
+ if (VALID_STATUS_FORMATS.includes(options.format)) {
6179
+ return options.format;
6180
+ }
6181
+ throw new Error(
6182
+ `${SPEC_STATUS_FORMAT_MESSAGE.INVALID_PREFIX} "${options.format}". Must be one of: ${VALID_STATUS_FORMATS.join(", ")}`
4928
6183
  );
4929
6184
  }
4930
6185
  function registerSpecCommands(specCmd) {
4931
- specCmd.command(SPEC_DOMAIN_CLI.STATUS_COMMAND).description("Get product status").option(SPEC_DOMAIN_CLI.JSON_OPTION, "Output as JSON").option(SPEC_DOMAIN_CLI.FORMAT_OPTION_DEFINITION, "Output format (text|json|markdown|table)").action(async (options) => {
6186
+ specCmd.command(SPEC_DOMAIN_CLI.STATUS_COMMAND).description("Get product status").option(SPEC_DOMAIN_CLI.JSON_OPTION, "Output as JSON").option(SPEC_DOMAIN_CLI.FORMAT_OPTION_DEFINITION, "Output format (text|json|markdown|table)").option(SPEC_DOMAIN_CLI.UPDATE_OPTION, "Refresh each node's spx.status.json before reporting").action(async (options) => {
4932
6187
  try {
4933
- const output = await statusCommand({
6188
+ const format2 = resolveStatusFormat(options);
6189
+ const output = options.update === true ? await statusCommand({
4934
6190
  cwd: process.cwd(),
4935
- format: resolveStatusFormat(options),
4936
- onWarning: writeWarning
4937
- });
6191
+ format: format2,
6192
+ onWarning: writeWarning,
6193
+ update: true,
6194
+ resolveOutcomeFor: (productDir) => createNodeOutcomeResolver({
6195
+ productDir,
6196
+ registry: testingRegistry,
6197
+ runnerDepsFor: createRunnerDepsFor(productDir, process.stderr)
6198
+ })
6199
+ }) : await statusCommand({ cwd: process.cwd(), format: format2, onWarning: writeWarning });
4938
6200
  console.log(output);
4939
6201
  } catch (error) {
4940
6202
  handleCommandError(error);
@@ -4949,9 +6211,6 @@ function registerSpecCommands(specCmd) {
4949
6211
  }
4950
6212
  });
4951
6213
  }
4952
- function writeWarning(warning) {
4953
- console.error(warning);
4954
- }
4955
6214
  var specDomain = {
4956
6215
  name: "spec",
4957
6216
  description: "Manage spec workflow",
@@ -4961,21 +6220,73 @@ var specDomain = {
4961
6220
  }
4962
6221
  };
4963
6222
 
6223
+ // src/interfaces/cli/testing.ts
6224
+ var TESTING_CLI = {
6225
+ commandName: "test",
6226
+ description: "Run spec-tree tests across product languages",
6227
+ passingSubcommand: "passing",
6228
+ passingDescription: "Run only the tests within the configured passing scope"
6229
+ };
6230
+ var UNMATCHED_TEST_FILES_WARNING = "Skipped test files with no registered runner";
6231
+ var TESTING_PRODUCT_DIR_WARNING = {
6232
+ NOT_GIT_REPOSITORY: `Warning: Not in a git repository. Reading ${SPEC_TREE_CONFIG.ROOT_DIRECTORY} tests relative to the current working directory.`
6233
+ };
6234
+ async function resolveTestProductDir() {
6235
+ const { productDir, isGitRepo } = await detectWorktreeProductRoot(process.cwd());
6236
+ writeWarning(isGitRepo ? void 0 : TESTING_PRODUCT_DIR_WARNING.NOT_GIT_REPOSITORY);
6237
+ return productDir;
6238
+ }
6239
+ async function runTestsThroughCommand(productDir, passing) {
6240
+ try {
6241
+ const result = await runTestsCommand(
6242
+ { productDir, passing },
6243
+ { registry: testingRegistry, runnerDepsFor: createRunnerDepsFor(productDir) }
6244
+ );
6245
+ return result.dispatch;
6246
+ } catch (error) {
6247
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
6248
+ `);
6249
+ process.exit(PROCESS_FAILURE_EXIT_CODE);
6250
+ }
6251
+ }
6252
+ function reportAndExit(result) {
6253
+ if (result.unmatched.length > 0) {
6254
+ writeWarning(`${UNMATCHED_TEST_FILES_WARNING}:
6255
+ ${result.unmatched.join("\n")}`);
6256
+ }
6257
+ process.exit(result.exitCode);
6258
+ }
6259
+ var testingDomain = {
6260
+ name: TESTING_CLI.commandName,
6261
+ description: TESTING_CLI.description,
6262
+ register: (program2) => {
6263
+ const testCmd = program2.command(TESTING_CLI.commandName).description(TESTING_CLI.description);
6264
+ testCmd.action(async () => {
6265
+ const productDir = await resolveTestProductDir();
6266
+ reportAndExit(await runTestsThroughCommand(productDir, false));
6267
+ });
6268
+ testCmd.command(TESTING_CLI.passingSubcommand).description(TESTING_CLI.passingDescription).action(async () => {
6269
+ const productDir = await resolveTestProductDir();
6270
+ reportAndExit(await runTestsThroughCommand(productDir, true));
6271
+ });
6272
+ }
6273
+ };
6274
+
4964
6275
  // src/commands/validation/markdown.ts
4965
- import { relative as relative4 } from "path";
6276
+ import { relative as relative5 } from "path";
4966
6277
 
4967
6278
  // src/validation/config/path-filter.ts
4968
- import { isAbsolute as isAbsolute2, relative as relative3 } from "path";
6279
+ import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
4969
6280
  var PATH_PREFIX_SEPARATOR = "/";
4970
6281
  function hasEffectiveValidationPathMetadata(filter) {
4971
6282
  return "hasIncludeFilter" in filter && typeof filter.hasIncludeFilter === "boolean" && "noMatchingIncludes" in filter && typeof filter.noMatchingIncludes === "boolean";
4972
6283
  }
4973
- function normalizePathPrefix(prefix) {
6284
+ function normalizePathPrefix2(prefix) {
4974
6285
  return prefix.split(/[\\/]/g).join(PATH_PREFIX_SEPARATOR).replace(/^\.\//, "").replace(/\/+$/, "");
4975
6286
  }
4976
- function pathMatchesPrefix(path6, prefix) {
4977
- const normalizedPath = normalizePathPrefix(path6);
4978
- const normalizedPrefix = normalizePathPrefix(prefix);
6287
+ function pathMatchesPrefix2(path6, prefix) {
6288
+ const normalizedPath = normalizePathPrefix2(path6);
6289
+ const normalizedPrefix = normalizePathPrefix2(prefix);
4979
6290
  return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}${PATH_PREFIX_SEPARATOR}`);
4980
6291
  }
4981
6292
  function unique(values) {
@@ -4985,7 +6296,7 @@ function nonEmpty(values) {
4985
6296
  return values?.filter((value) => value.length > 0) ?? [];
4986
6297
  }
4987
6298
  function toProjectRelativeValidationPath(projectRoot, path6) {
4988
- return isAbsolute2(path6) ? relative3(projectRoot, path6) : path6;
6299
+ return isAbsolute2(path6) ? relative4(projectRoot, path6) : path6;
4989
6300
  }
4990
6301
  function intersectIncludes(baseInclude, toolInclude) {
4991
6302
  const base = nonEmpty(baseInclude);
@@ -4997,8 +6308,8 @@ function intersectIncludes(baseInclude, toolInclude) {
4997
6308
  if (tool.length === 0) return { include: base, hasIncludeFilter: true, noMatches: false };
4998
6309
  const intersections = base.flatMap(
4999
6310
  (basePrefix) => tool.flatMap((toolPrefix) => {
5000
- if (pathMatchesPrefix(basePrefix, toolPrefix)) return [basePrefix];
5001
- if (pathMatchesPrefix(toolPrefix, basePrefix)) return [toolPrefix];
6311
+ if (pathMatchesPrefix2(basePrefix, toolPrefix)) return [basePrefix];
6312
+ if (pathMatchesPrefix2(toolPrefix, basePrefix)) return [toolPrefix];
5002
6313
  return [];
5003
6314
  })
5004
6315
  );
@@ -5026,10 +6337,10 @@ function pathPassesValidationFilter(path6, filter) {
5026
6337
  }
5027
6338
  const include = nonEmpty(filter.include);
5028
6339
  const exclude = nonEmpty(filter.exclude);
5029
- if (include.length > 0 && !include.some((prefix) => pathMatchesPrefix(path6, prefix))) {
6340
+ if (include.length > 0 && !include.some((prefix) => pathMatchesPrefix2(path6, prefix))) {
5030
6341
  return false;
5031
6342
  }
5032
- return !exclude.some((prefix) => pathMatchesPrefix(path6, prefix));
6343
+ return !exclude.some((prefix) => pathMatchesPrefix2(path6, prefix));
5033
6344
  }
5034
6345
  function applyValidationPathFilterToScope(scopeConfig, filter) {
5035
6346
  if (!hasValidationPathFilter(filter)) {
@@ -5043,7 +6354,7 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
5043
6354
  );
5044
6355
  const scopedFilePatterns = scopeConfig.filePatterns.filter((pattern) => pathPassesValidationFilter(pattern, filter));
5045
6356
  const includeFallbacksWithinScope = includeFallbacks.filter(
5046
- (include) => scopeConfig.directories.some((directory) => pathMatchesPrefix(include, directory))
6357
+ (include) => scopeConfig.directories.some((directory) => pathMatchesPrefix2(include, directory))
5047
6358
  );
5048
6359
  const noMatchingIncludes = hasEffectiveMetadata && filter.noMatchingIncludes || hasIncludeFilter && scopedDirectories.length === 0 && scopedFilePatterns.length === 0 && includeFallbacksWithinScope.length === 0;
5049
6360
  const directories = noMatchingIncludes ? [] : scopedDirectories.length > 0 ? scopedDirectories : includeFallbacksWithinScope;
@@ -5061,7 +6372,7 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
5061
6372
 
5062
6373
  // src/validation/steps/markdown.ts
5063
6374
  import { existsSync as existsSync2, statSync } from "fs";
5064
- import { basename, dirname as dirname3, join as join11, relative as pathRelative } from "path";
6375
+ import { basename as basename2, dirname as dirname4, join as join18, relative as pathRelative } from "path";
5065
6376
  import { main as markdownlintMain } from "markdownlint-cli2";
5066
6377
  import relativeLinksRule from "markdownlint-rule-relative-links";
5067
6378
  var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
@@ -5100,7 +6411,7 @@ function buildMarkdownlintConfig(directoryName) {
5100
6411
  };
5101
6412
  }
5102
6413
  function getDefaultDirectories(projectRoot) {
5103
- return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join11(projectRoot, name)).filter((dir) => existsSync2(dir));
6414
+ return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join18(projectRoot, name)).filter((dir) => existsSync2(dir));
5104
6415
  }
5105
6416
  function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
5106
6417
  if (isExistingDirectory(path6, deps)) {
@@ -5159,7 +6470,7 @@ async function validateMarkdown(options) {
5159
6470
  async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
5160
6471
  const errors = [];
5161
6472
  const directory = targetDirectory(target);
5162
- const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
6473
+ const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename2(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
5163
6474
  const { customRules, ...markdownlintConfig } = config;
5164
6475
  const optionsOverride = {
5165
6476
  config: {
@@ -5183,7 +6494,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
5183
6494
  if (parsed) {
5184
6495
  errors.push({
5185
6496
  ...parsed,
5186
- file: join11(directory, parsed.file)
6497
+ file: join18(directory, parsed.file)
5187
6498
  });
5188
6499
  }
5189
6500
  }
@@ -5191,7 +6502,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
5191
6502
  return errors;
5192
6503
  }
5193
6504
  function targetDirectory(target) {
5194
- return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname3(target.path) : target.path;
6505
+ return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname4(target.path) : target.path;
5195
6506
  }
5196
6507
  function hasMarkdownExtension(path6) {
5197
6508
  const lastDot = path6.lastIndexOf(".");
@@ -5219,7 +6530,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
5219
6530
  return rootSegment;
5220
6531
  }
5221
6532
  }
5222
- return basename(directory);
6533
+ return basename2(directory);
5223
6534
  }
5224
6535
 
5225
6536
  // src/commands/validation/messages.ts
@@ -5295,7 +6606,7 @@ async function markdownCommand(options) {
5295
6606
  path: path6
5296
6607
  }));
5297
6608
  const targets = unfilteredTargets.filter(
5298
- (target) => pathPassesValidationFilter(relative4(cwd, target.path), pathFilter)
6609
+ (target) => pathPassesValidationFilter(relative5(cwd, target.path), pathFilter)
5299
6610
  );
5300
6611
  const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
5301
6612
  const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
@@ -6204,22 +7515,22 @@ var ParseErrorCode;
6204
7515
  })(ParseErrorCode || (ParseErrorCode = {}));
6205
7516
 
6206
7517
  // src/validation/config/scope.ts
6207
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
6208
- import { isAbsolute as isAbsolute3, join as join12 } from "path";
7518
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
7519
+ import { isAbsolute as isAbsolute3, join as join19 } from "path";
6209
7520
  var TSCONFIG_FILES = {
6210
7521
  full: "tsconfig.json",
6211
7522
  production: "tsconfig.production.json"
6212
7523
  };
6213
- var PATH_SEGMENT_SEPARATOR = "/";
7524
+ var PATH_SEGMENT_SEPARATOR2 = "/";
6214
7525
  var GLOB_MARKER = "*";
6215
7526
  var HIDDEN_PATH_PREFIX = ".";
6216
7527
  var defaultScopeDeps = {
6217
- readFileSync: readFileSync2,
7528
+ readFileSync: readFileSync3,
6218
7529
  existsSync: existsSync3,
6219
7530
  readdirSync
6220
7531
  };
6221
7532
  function resolveProjectPath(projectRoot, path6) {
6222
- return isAbsolute3(path6) ? path6 : join12(projectRoot, path6);
7533
+ return isAbsolute3(path6) ? path6 : join19(projectRoot, path6);
6223
7534
  }
6224
7535
  function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
6225
7536
  try {
@@ -6263,7 +7574,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
6263
7574
  if (hasDirectTsFiles) return true;
6264
7575
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
6265
7576
  for (const subdir of subdirs.slice(0, 5)) {
6266
- if (hasTypeScriptFilesRecursive(join12(dirPath, subdir.name), maxDepth - 1, deps)) {
7577
+ if (hasTypeScriptFilesRecursive(join19(dirPath, subdir.name), maxDepth - 1, deps)) {
6267
7578
  return true;
6268
7579
  }
6269
7580
  }
@@ -6286,7 +7597,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
6286
7597
  });
6287
7598
  if (!isExcluded) {
6288
7599
  try {
6289
- const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join12(projectRoot, dir), 2, deps);
7600
+ const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join19(projectRoot, dir), 2, deps);
6290
7601
  if (hasTypeScriptFiles) {
6291
7602
  directories.add(dir);
6292
7603
  }
@@ -6297,7 +7608,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
6297
7608
  }
6298
7609
  if (config.include) {
6299
7610
  for (const pattern of config.include) {
6300
- if (pattern.includes(PATH_SEGMENT_SEPARATOR)) {
7611
+ if (pattern.includes(PATH_SEGMENT_SEPARATOR2)) {
6301
7612
  const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
6302
7613
  if (topLevelDir) {
6303
7614
  directories.add(topLevelDir);
@@ -6308,7 +7619,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
6308
7619
  return Array.from(directories).sort();
6309
7620
  }
6310
7621
  function getLiteralTopLevelPatternDirectory(pattern) {
6311
- const topLevelDir = pattern.split(PATH_SEGMENT_SEPARATOR)[0];
7622
+ const topLevelDir = pattern.split(PATH_SEGMENT_SEPARATOR2)[0];
6312
7623
  if (!topLevelDir || topLevelDir.includes(GLOB_MARKER) || topLevelDir.startsWith(HIDDEN_PATH_PREFIX)) {
6313
7624
  return null;
6314
7625
  }
@@ -6317,13 +7628,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
6317
7628
  function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
6318
7629
  return patterns.filter((pattern) => {
6319
7630
  const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
6320
- return topLevelDir === null || deps.existsSync(join12(projectRoot, topLevelDir));
7631
+ return topLevelDir === null || deps.existsSync(join19(projectRoot, topLevelDir));
6321
7632
  }).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
6322
7633
  }
6323
7634
  function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
6324
7635
  const config = resolveTypeScriptConfig(scope, projectRoot, deps);
6325
7636
  const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
6326
- const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join12(projectRoot, dir)));
7637
+ const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join19(projectRoot, dir)));
6327
7638
  return existingDirectories;
6328
7639
  }
6329
7640
  function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
@@ -6338,9 +7649,9 @@ function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
6338
7649
 
6339
7650
  // src/validation/discovery/tool-finder.ts
6340
7651
  import { execSync as execSync2 } from "child_process";
6341
- import fs5 from "fs";
7652
+ import fs6 from "fs";
6342
7653
  import { createRequire } from "module";
6343
- import path4 from "path";
7654
+ import path5 from "path";
6344
7655
 
6345
7656
  // src/validation/discovery/constants.ts
6346
7657
  var TOOL_DISCOVERY = {
@@ -6382,7 +7693,7 @@ var defaultToolDiscoveryDeps = {
6382
7693
  return null;
6383
7694
  }
6384
7695
  },
6385
- existsSync: fs5.existsSync,
7696
+ existsSync: fs6.existsSync,
6386
7697
  whichSync: (tool) => {
6387
7698
  try {
6388
7699
  const result = execSync2(`which ${tool}`, {
@@ -6403,12 +7714,12 @@ async function discoverTool(tool, options = {}) {
6403
7714
  found: true,
6404
7715
  location: {
6405
7716
  tool,
6406
- path: path4.dirname(bundledPath),
7717
+ path: path5.dirname(bundledPath),
6407
7718
  source: TOOL_DISCOVERY.SOURCES.BUNDLED
6408
7719
  }
6409
7720
  };
6410
7721
  }
6411
- const projectBinPath = path4.join(projectRoot, "node_modules", ".bin", tool);
7722
+ const projectBinPath = path5.join(projectRoot, "node_modules", ".bin", tool);
6412
7723
  if (deps.existsSync(projectBinPath)) {
6413
7724
  return {
6414
7725
  found: true,
@@ -6445,42 +7756,9 @@ function formatSkipMessage(stepName, result) {
6445
7756
  return TOOL_DISCOVERY.MESSAGES.SKIP_FORMAT(stepName, result.notFound.tool);
6446
7757
  }
6447
7758
 
6448
- // src/validation/discovery/language-finder.ts
6449
- import fs6 from "fs";
6450
- import path5 from "path";
6451
- var TYPESCRIPT_MARKER = "tsconfig.json";
6452
- var ESLINT_CONFIG_FILES = [
6453
- "eslint.config.ts",
6454
- "eslint.config.js",
6455
- "eslint.config.mjs",
6456
- "eslint.config.cjs"
6457
- ];
6458
- var ESLINT_PRODUCTION_CONFIG_FILES = [
6459
- "eslint.config.production.ts",
6460
- "eslint.config.production.js",
6461
- "eslint.config.production.mjs",
6462
- "eslint.config.production.cjs"
6463
- ];
6464
- var defaultLanguageDetectionDeps = {
6465
- existsSync: fs6.existsSync
6466
- };
6467
- function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
6468
- const present = deps.existsSync(path5.join(projectRoot, TYPESCRIPT_MARKER));
6469
- if (!present) {
6470
- return { present: false };
6471
- }
6472
- const eslintConfigFile = ESLINT_CONFIG_FILES.find(
6473
- (configFile) => deps.existsSync(path5.join(projectRoot, configFile))
6474
- );
6475
- const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
6476
- (configFile) => deps.existsSync(path5.join(projectRoot, configFile))
6477
- );
6478
- return { present: true, eslintConfigFile, productionEslintConfigFile };
6479
- }
6480
-
6481
7759
  // src/validation/steps/circular.ts
6482
7760
  import madge from "madge";
6483
- import { join as join13 } from "path";
7761
+ import { join as join20 } from "path";
6484
7762
  var CIRCULAR_DEPS_KEYS = {
6485
7763
  MADGE: "madge"
6486
7764
  };
@@ -6489,11 +7767,11 @@ var defaultCircularDeps = {
6489
7767
  };
6490
7768
  async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
6491
7769
  try {
6492
- const analyzeDirectories = typescriptScope.directories.map((directory) => join13(projectRoot, directory));
7770
+ const analyzeDirectories = typescriptScope.directories.map((directory) => join20(projectRoot, directory));
6493
7771
  if (analyzeDirectories.length === 0) {
6494
7772
  return { success: true };
6495
7773
  }
6496
- const tsConfigFile = join13(projectRoot, TSCONFIG_FILES[scope]);
7774
+ const tsConfigFile = join20(projectRoot, TSCONFIG_FILES[scope]);
6497
7775
  const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
6498
7776
  const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
6499
7777
  const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -6574,155 +7852,9 @@ ${cycles}`;
6574
7852
 
6575
7853
  // src/validation/steps/knip.ts
6576
7854
  import { existsSync as existsSync4 } from "fs";
6577
- import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
7855
+ import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
6578
7856
  import { tmpdir } from "os";
6579
- import { isAbsolute as isAbsolute4, join as join14 } from "path";
6580
-
6581
- // src/lib/process-lifecycle/exit-codes.ts
6582
- var SIGINT_EXIT_CODE = 130;
6583
- var SIGTERM_EXIT_CODE = 143;
6584
- var EPIPE_EXIT_CODE = 0;
6585
- var UNCAUGHT_EXIT_CODE = 1;
6586
-
6587
- // src/lib/process-lifecycle/handlers.ts
6588
- var SIGINT_NAME = "SIGINT";
6589
- var SIGTERM_NAME = "SIGTERM";
6590
- function createHandlers(deps) {
6591
- let cleanupOnce = false;
6592
- function killEachChild(signal) {
6593
- deps.registry.forEach((child) => {
6594
- if (!child.killed) child.kill(signal);
6595
- });
6596
- }
6597
- function exitOnce(code, killSignal) {
6598
- if (cleanupOnce) {
6599
- deps.exitController.exit(code);
6600
- return;
6601
- }
6602
- cleanupOnce = true;
6603
- if (killSignal !== void 0) killEachChild(killSignal);
6604
- deps.exitController.exit(code);
6605
- }
6606
- return {
6607
- onSigint() {
6608
- exitOnce(SIGINT_EXIT_CODE, SIGINT_NAME);
6609
- },
6610
- onSigterm() {
6611
- exitOnce(SIGTERM_EXIT_CODE, SIGTERM_NAME);
6612
- },
6613
- onEpipe() {
6614
- exitOnce(EPIPE_EXIT_CODE, SIGTERM_NAME);
6615
- },
6616
- onUncaught(_error) {
6617
- exitOnce(UNCAUGHT_EXIT_CODE, SIGTERM_NAME);
6618
- }
6619
- };
6620
- }
6621
-
6622
- // src/lib/process-lifecycle/install.ts
6623
- import { spawn } from "child_process";
6624
-
6625
- // src/lib/process-lifecycle/registry.ts
6626
- function createRegistry() {
6627
- const tracked = /* @__PURE__ */ new Set();
6628
- return {
6629
- add(child) {
6630
- tracked.add(child);
6631
- },
6632
- remove(child) {
6633
- tracked.delete(child);
6634
- },
6635
- forEach(fn) {
6636
- for (const child of tracked) fn(child);
6637
- },
6638
- get size() {
6639
- return tracked.size;
6640
- }
6641
- };
6642
- }
6643
-
6644
- // src/lib/process-lifecycle/runner.ts
6645
- function createLifecycleRunner(deps) {
6646
- return {
6647
- spawn(command, args, options) {
6648
- const child = deps.spawn(command, args, options);
6649
- deps.registry.add(child);
6650
- child.on("exit", () => deps.registry.remove(child));
6651
- return child;
6652
- }
6653
- };
6654
- }
6655
-
6656
- // src/lib/process-lifecycle/install.ts
6657
- var moduleRegistry = createRegistry();
6658
- var moduleExitController = {
6659
- exit(code) {
6660
- process.exit(code);
6661
- }
6662
- };
6663
- var installed = false;
6664
- var lifecycleProcessRunner = createLifecycleRunner({
6665
- registry: moduleRegistry,
6666
- spawn
6667
- });
6668
- var EPIPE_CODE = "EPIPE";
6669
- var UNCAUGHT_PREFIX = "Uncaught: ";
6670
- var NEWLINE = "\n";
6671
- function formatUncaught(error) {
6672
- if (error instanceof Error && error.stack !== void 0) {
6673
- return UNCAUGHT_PREFIX + error.stack + NEWLINE;
6674
- }
6675
- return UNCAUGHT_PREFIX + String(error) + NEWLINE;
6676
- }
6677
- function logUncaughtToStderr(error) {
6678
- try {
6679
- process.stderr.write(formatUncaught(error));
6680
- } catch {
6681
- }
6682
- }
6683
- function installLifecycle() {
6684
- if (installed) return;
6685
- installed = true;
6686
- const handlers = createHandlers({
6687
- registry: moduleRegistry,
6688
- exitController: moduleExitController
6689
- });
6690
- process.on("uncaughtException", (error) => {
6691
- logUncaughtToStderr(error);
6692
- handlers.onUncaught(error);
6693
- });
6694
- process.on("unhandledRejection", (reason) => {
6695
- logUncaughtToStderr(reason);
6696
- handlers.onUncaught(reason);
6697
- });
6698
- process.on("SIGTERM", () => handlers.onSigterm());
6699
- process.on("SIGINT", () => handlers.onSigint());
6700
- process.stdout.on("error", (error) => {
6701
- if (error.code === EPIPE_CODE) {
6702
- handlers.onEpipe();
6703
- return;
6704
- }
6705
- handlers.onUncaught(error);
6706
- });
6707
- process.stderr.on("error", (error) => {
6708
- if (error.code === EPIPE_CODE) {
6709
- handlers.onEpipe();
6710
- return;
6711
- }
6712
- handlers.onUncaught(error);
6713
- });
6714
- }
6715
-
6716
- // src/lib/process-lifecycle/managed-subprocess.ts
6717
- var MANAGED_SUBPROCESS_STDIO = "pipe";
6718
- function spawnManagedSubprocess(runner, command, args, options) {
6719
- return runner.spawn(command, args, {
6720
- ...options,
6721
- stdio: MANAGED_SUBPROCESS_STDIO
6722
- });
6723
- }
6724
-
6725
- // src/validation/steps/knip.ts
7857
+ import { isAbsolute as isAbsolute4, join as join21 } from "path";
6726
7858
  var defaultKnipProcessRunner = lifecycleProcessRunner;
6727
7859
  var KNIP_COMMAND_TOKENS = {
6728
7860
  COMMAND: "knip",
@@ -6733,7 +7865,7 @@ var defaultKnipDeps = {
6733
7865
  existsSync: existsSync4,
6734
7866
  mkdtemp,
6735
7867
  rm,
6736
- writeFile: writeFile2
7868
+ writeFile: writeFile3
6737
7869
  };
6738
7870
  async function validateKnip2(context, runner = defaultKnipProcessRunner, deps = defaultKnipDeps) {
6739
7871
  try {
@@ -6750,7 +7882,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
6750
7882
  }
6751
7883
  async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
6752
7884
  const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
6753
- const localBin = join14(projectRoot, "node_modules", ".bin", "knip");
7885
+ const localBin = join21(projectRoot, "node_modules", ".bin", "knip");
6754
7886
  const binary = deps.existsSync(localBin) ? localBin : "npx";
6755
7887
  const baseArgs = scopedTsconfig === void 0 ? [] : [
6756
7888
  KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
@@ -6805,12 +7937,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
6805
7937
  });
6806
7938
  }
6807
7939
  async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
6808
- const tempDir = await deps.mkdtemp(join14(tmpdir(), "validate-knip-"));
6809
- const configPath = join14(tempDir, TSCONFIG_FILES.full);
6810
- const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join14(projectRoot, pattern);
7940
+ const tempDir = await deps.mkdtemp(join21(tmpdir(), "validate-knip-"));
7941
+ const configPath = join21(tempDir, TSCONFIG_FILES.full);
7942
+ const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join21(projectRoot, pattern);
6811
7943
  const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
6812
7944
  const config = {
6813
- extends: join14(projectRoot, TSCONFIG_FILES.full),
7945
+ extends: join21(projectRoot, TSCONFIG_FILES.full),
6814
7946
  include: project.map(toProjectPathPattern),
6815
7947
  exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
6816
7948
  };
@@ -6860,12 +7992,12 @@ async function knipCommand(options) {
6860
7992
 
6861
7993
  // src/validation/steps/eslint.ts
6862
7994
  import { existsSync as existsSync6 } from "fs";
6863
- import { join as join16 } from "path";
7995
+ import { join as join23 } from "path";
6864
7996
 
6865
7997
  // src/validation/lint-policy.ts
6866
7998
  import { execFileSync } from "child_process";
6867
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
6868
- import { join as join15 } from "path";
7999
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
8000
+ import { join as join22 } from "path";
6869
8001
 
6870
8002
  // src/validation/lint-policy-constants.ts
6871
8003
  var LINT_POLICY_MANIFESTS = {
@@ -6912,17 +8044,17 @@ var DEPRECATED_SPEC_NODE_SUFFIX_PATTERN = /\.(capability|feature|story)$/;
6912
8044
  var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS.LOCAL_MAIN];
6913
8045
  function readManifest(productDir, file, key) {
6914
8046
  return parseLintPolicyManifest(
6915
- readFileSync3(join15(productDir, file), "utf-8"),
8047
+ readFileSync4(join22(productDir, file), "utf-8"),
6916
8048
  file,
6917
8049
  key
6918
8050
  );
6919
8051
  }
6920
8052
  function manifestExists(productDir, file) {
6921
- return existsSync5(join15(productDir, file));
8053
+ return existsSync5(join22(productDir, file));
6922
8054
  }
6923
8055
  function findDeprecatedSpecNodePath(productDir) {
6924
8056
  function visit2(relativeDirectory) {
6925
- const absoluteDirectory = join15(productDir, relativeDirectory);
8057
+ const absoluteDirectory = join22(productDir, relativeDirectory);
6926
8058
  for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
6927
8059
  if (!entry.isDirectory()) {
6928
8060
  continue;
@@ -6938,7 +8070,7 @@ function findDeprecatedSpecNodePath(productDir) {
6938
8070
  }
6939
8071
  return void 0;
6940
8072
  }
6941
- const specTreeRootPath = join15(productDir, SPEC_TREE_ROOT);
8073
+ const specTreeRootPath = join22(productDir, SPEC_TREE_ROOT);
6942
8074
  if (!existsSync5(specTreeRootPath)) {
6943
8075
  return void 0;
6944
8076
  }
@@ -6964,7 +8096,7 @@ function assertManifestEntries(productDir, file, entries, suffixPattern, suffixD
6964
8096
  if (!suffixPattern.test(entry)) {
6965
8097
  throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
6966
8098
  }
6967
- const absoluteEntry = join15(productDir, entry);
8099
+ const absoluteEntry = join22(productDir, entry);
6968
8100
  if (!existsSync5(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
6969
8101
  throw new Error(`${file} entry does not exist as a directory: ${entry}`);
6970
8102
  }
@@ -7224,7 +8356,7 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
7224
8356
  scopeConfig: context.scopeConfig
7225
8357
  });
7226
8358
  return new Promise((resolve6) => {
7227
- const localBin = join16(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
8359
+ const localBin = join23(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
7228
8360
  const binary = existsSync6(localBin) ? localBin : "npx";
7229
8361
  const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
7230
8362
  const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
@@ -7326,7 +8458,7 @@ async function lintCommand(options) {
7326
8458
 
7327
8459
  // src/validation/literal/index.ts
7328
8460
  import { readFile as readFile8 } from "fs/promises";
7329
- import { isAbsolute as isAbsolute5, relative as relative6, resolve as resolve5 } from "path";
8461
+ import { isAbsolute as isAbsolute5, relative as relative7, resolve as resolve5 } from "path";
7330
8462
 
7331
8463
  // src/lib/file-inclusion/predicates/ignore-source.ts
7332
8464
  var IGNORE_SOURCE_LAYER = "ignore-source";
@@ -7360,18 +8492,18 @@ var ignoreSourceLayer = makeLayer(
7360
8492
  );
7361
8493
 
7362
8494
  // src/lib/file-inclusion/pipeline.ts
7363
- import { readdir as readdir6 } from "fs/promises";
7364
- import { join as join17, relative as relative5, sep } from "path";
8495
+ import { readdir as readdir7 } from "fs/promises";
8496
+ import { join as join24, relative as relative6, sep as sep2 } from "path";
7365
8497
  var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
7366
- function isNodeError2(err) {
8498
+ function isNodeError3(err) {
7367
8499
  return err instanceof Error && "code" in err;
7368
8500
  }
7369
8501
  async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
7370
8502
  let dirEntries;
7371
8503
  try {
7372
- dirEntries = await readdir6(absoluteDir, { withFileTypes: true });
8504
+ dirEntries = await readdir7(absoluteDir, { withFileTypes: true });
7373
8505
  } catch (err) {
7374
- if (isNodeError2(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
8506
+ if (isNodeError3(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
7375
8507
  return;
7376
8508
  }
7377
8509
  throw err;
@@ -7379,12 +8511,12 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
7379
8511
  for (const entry of dirEntries) {
7380
8512
  if (entry.isDirectory()) {
7381
8513
  if (artifactDirs.has(entry.name)) continue;
7382
- const absolutePath = join17(absoluteDir, entry.name);
8514
+ const absolutePath = join24(absoluteDir, entry.name);
7383
8515
  await collectPaths(absolutePath, projectRoot, result, artifactDirs);
7384
8516
  } else if (entry.isFile()) {
7385
- const absolutePath = join17(absoluteDir, entry.name);
7386
- const rel = relative5(projectRoot, absolutePath);
7387
- result.push(sep === "/" ? rel : rel.split(sep).join("/"));
8517
+ const absolutePath = join24(absoluteDir, entry.name);
8518
+ const rel = relative6(projectRoot, absolutePath);
8519
+ result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
7388
8520
  }
7389
8521
  }
7390
8522
  }
@@ -7765,16 +8897,16 @@ var DEFAULT_LITERAL_COLLECT_OPTIONS = {
7765
8897
  minStringLength: literalConfigDescriptor.defaults.minStringLength,
7766
8898
  minNumberDigits: literalConfigDescriptor.defaults.minNumberDigits
7767
8899
  };
7768
- function normalizePathPrefix2(prefix) {
8900
+ function normalizePathPrefix3(prefix) {
7769
8901
  const posix = prefix.split(/[\\/]/g).join(PATH_PREFIX_SEPARATOR2);
7770
8902
  return posix.endsWith(PATH_PREFIX_SEPARATOR2) ? posix : `${posix}${PATH_PREFIX_SEPARATOR2}`;
7771
8903
  }
7772
- function applyPathFilter(entries, pathConfig) {
8904
+ function applyPathFilter2(entries, pathConfig) {
7773
8905
  if (pathConfig === void 0) {
7774
8906
  return entries;
7775
8907
  }
7776
- const includePrefixes = (pathConfig.include ?? []).map(normalizePathPrefix2);
7777
- const excludePrefixes = (pathConfig.exclude ?? []).map(normalizePathPrefix2);
8908
+ const includePrefixes = (pathConfig.include ?? []).map(normalizePathPrefix3);
8909
+ const excludePrefixes = (pathConfig.exclude ?? []).map(normalizePathPrefix3);
7778
8910
  return entries.filter((entry) => {
7779
8911
  if (includePrefixes.length > 0 && !includePrefixes.some((p) => entry.path.startsWith(p))) {
7780
8912
  return false;
@@ -7790,7 +8922,7 @@ async function validateLiteralReuse(input) {
7790
8922
  const request = input.files ? {
7791
8923
  explicit: input.files.map((f) => {
7792
8924
  const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
7793
- return relative6(input.productDir, abs).split(/[\\/]/g).join("/");
8925
+ return relative7(input.productDir, abs).split(/[\\/]/g).join("/");
7794
8926
  })
7795
8927
  } : { walkRoot: input.productDir };
7796
8928
  const scope = await runPipeline(
@@ -7800,7 +8932,7 @@ async function validateLiteralReuse(input) {
7800
8932
  DEFAULT_SCOPE_CONFIG,
7801
8933
  EMPTY_IGNORE_READER
7802
8934
  );
7803
- const filtered = applyPathFilter(scope.included, input.pathConfig);
8935
+ const filtered = applyPathFilter2(scope.included, input.pathConfig);
7804
8936
  const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
7805
8937
  const collectOptions = {
7806
8938
  visitorKeys: defaultVisitorKeys,
@@ -7811,7 +8943,7 @@ async function validateLiteralReuse(input) {
7811
8943
  const testOccurrencesByFile = /* @__PURE__ */ new Map();
7812
8944
  const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
7813
8945
  for (const abs of candidateFiles) {
7814
- const rel = relative6(input.productDir, abs).split(/[\\/]/g).join("/");
8946
+ const rel = relative7(input.productDir, abs).split(/[\\/]/g).join("/");
7815
8947
  const content = await readSafe(abs);
7816
8948
  if (content === null) continue;
7817
8949
  const occurrences = collectLiterals(content, rel, collectOptions);
@@ -8042,7 +9174,7 @@ function formatLoc(loc) {
8042
9174
  // src/validation/steps/typescript.ts
8043
9175
  import { existsSync as existsSync7, mkdirSync, rmSync, writeFileSync } from "fs";
8044
9176
  import { mkdtemp as mkdtemp2 } from "fs/promises";
8045
- import { isAbsolute as isAbsolute6, join as join18 } from "path";
9177
+ import { isAbsolute as isAbsolute6, join as join25 } from "path";
8046
9178
  var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
8047
9179
  var defaultTypeScriptDeps = {
8048
9180
  mkdtemp: mkdtemp2,
@@ -8055,9 +9187,9 @@ var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
8055
9187
  var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
8056
9188
  var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
8057
9189
  async function createTemporaryTsconfigDir(projectRoot, deps) {
8058
- const parent = join18(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
9190
+ const parent = join25(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
8059
9191
  deps.mkdirSync(parent, { recursive: true });
8060
- return deps.mkdtemp(join18(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
9192
+ return deps.mkdtemp(join25(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
8061
9193
  }
8062
9194
  function buildTypeScriptArgs(context) {
8063
9195
  const { scope, configFile } = context;
@@ -8065,11 +9197,11 @@ function buildTypeScriptArgs(context) {
8065
9197
  }
8066
9198
  async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
8067
9199
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
8068
- const configPath = join18(tempDir, "tsconfig.json");
9200
+ const configPath = join25(tempDir, "tsconfig.json");
8069
9201
  const baseConfigFile = TSCONFIG_FILES[scope];
8070
- const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join18(projectRoot, file));
9202
+ const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join25(projectRoot, file));
8071
9203
  const tempConfig = {
8072
- extends: join18(projectRoot, baseConfigFile),
9204
+ extends: join25(projectRoot, baseConfigFile),
8073
9205
  files: absoluteFiles,
8074
9206
  include: [],
8075
9207
  exclude: [],
@@ -8086,11 +9218,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
8086
9218
  }
8087
9219
  async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
8088
9220
  const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
8089
- const configPath = join18(tempDir, "tsconfig.json");
9221
+ const configPath = join25(tempDir, "tsconfig.json");
8090
9222
  const baseConfigFile = TSCONFIG_FILES[scope];
8091
- const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join18(projectRoot, pattern);
9223
+ const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join25(projectRoot, pattern);
8092
9224
  const tempConfig = {
8093
- extends: join18(projectRoot, baseConfigFile),
9225
+ extends: join25(projectRoot, baseConfigFile),
8094
9226
  include: scopeConfig.filePatterns.map(toProjectPathPattern),
8095
9227
  exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
8096
9228
  compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
@@ -8118,7 +9250,7 @@ async function validateTypeScript(context, options = {}) {
8118
9250
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
8119
9251
  try {
8120
9252
  return await new Promise((resolve6) => {
8121
- const tscBin = join18(projectRoot, "node_modules", ".bin", "tsc");
9253
+ const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
8122
9254
  const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
8123
9255
  const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
8124
9256
  const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
@@ -8151,7 +9283,7 @@ async function validateTypeScript(context, options = {}) {
8151
9283
  return { success: true, skipped: true };
8152
9284
  }
8153
9285
  const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
8154
- const tscBin = join18(projectRoot, "node_modules", ".bin", "tsc");
9286
+ const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
8155
9287
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
8156
9288
  tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
8157
9289
  return new Promise((resolve6) => {
@@ -8173,7 +9305,7 @@ async function validateTypeScript(context, options = {}) {
8173
9305
  });
8174
9306
  });
8175
9307
  } else {
8176
- const tscBin = join18(projectRoot, "node_modules", ".bin", "tsc");
9308
+ const tscBin = join25(projectRoot, "node_modules", ".bin", "tsc");
8177
9309
  tool = deps.existsSync(tscBin) ? tscBin : "npx";
8178
9310
  const rawArgs = buildTypeScriptArgs({ scope, configFile });
8179
9311
  tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
@@ -8432,8 +9564,8 @@ function truncate(value) {
8432
9564
  }
8433
9565
 
8434
9566
  // src/validation/literal/allowlist-existing.ts
8435
- import { rename as rename4, writeFile as writeFile3 } from "fs/promises";
8436
- import { dirname as dirname4, join as join19 } from "path";
9567
+ import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
9568
+ import { dirname as dirname5, join as join26 } from "path";
8437
9569
  var EXIT_OK = 0;
8438
9570
  var EXIT_ERROR = 1;
8439
9571
  var INCLUDE_FIELD = "include";
@@ -8453,10 +9585,10 @@ var productionReader = {
8453
9585
  };
8454
9586
  var productionWriter = {
8455
9587
  async write(filePath, content) {
8456
- const dir = dirname4(filePath);
9588
+ const dir = dirname5(filePath);
8457
9589
  const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
8458
- const tmpPath = join19(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
8459
- await writeFile3(tmpPath, content, "utf8");
9590
+ const tmpPath = join26(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
9591
+ await writeFile4(tmpPath, content, "utf8");
8460
9592
  await rename4(tmpPath, filePath);
8461
9593
  }
8462
9594
  };
@@ -8783,6 +9915,7 @@ var CLI_DOMAINS = [
8783
9915
  configDomain,
8784
9916
  sessionDomain,
8785
9917
  specDomain,
9918
+ testingDomain,
8786
9919
  validationDomain
8787
9920
  ];
8788
9921