@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.
- package/dist/cli.js +1601 -468
- package/dist/cli.js.map +1 -1
- 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
|
|
1904
|
-
const matched =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
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
|
-
|
|
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 [
|
|
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 (
|
|
3638
|
-
|
|
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
|
-
|
|
3848
|
+
defaultBranch,
|
|
3849
|
+
defaultTipSha,
|
|
3850
|
+
currentWorktreePath,
|
|
3851
|
+
rootWorktreePath
|
|
3651
3852
|
});
|
|
3652
3853
|
}
|
|
3653
3854
|
async function handoffCommand(options) {
|
|
3654
|
-
const { config
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
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
|
|
4819
|
-
|
|
4820
|
-
|
|
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
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
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
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4850
|
-
|
|
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
|
|
4853
|
-
|
|
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
|
|
4856
|
-
const
|
|
4857
|
-
const
|
|
4858
|
-
|
|
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
|
|
4861
|
-
return
|
|
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
|
|
4864
|
-
|
|
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
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
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
|
|
4881
|
-
|
|
5323
|
+
function digestTestPaths(paths) {
|
|
5324
|
+
const normalized = [...new Set(paths)].sort(compareAsciiStrings);
|
|
5325
|
+
return sha256Hex(JSON.stringify(normalized));
|
|
4882
5326
|
}
|
|
4883
|
-
function
|
|
4884
|
-
|
|
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
|
|
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
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
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
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
return
|
|
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
|
-
|
|
4927
|
-
|
|
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
|
|
6188
|
+
const format2 = resolveStatusFormat(options);
|
|
6189
|
+
const output = options.update === true ? await statusCommand({
|
|
4934
6190
|
cwd: process.cwd(),
|
|
4935
|
-
format:
|
|
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
|
|
6276
|
+
import { relative as relative5 } from "path";
|
|
4966
6277
|
|
|
4967
6278
|
// src/validation/config/path-filter.ts
|
|
4968
|
-
import { isAbsolute as isAbsolute2, relative as
|
|
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
|
|
6284
|
+
function normalizePathPrefix2(prefix) {
|
|
4974
6285
|
return prefix.split(/[\\/]/g).join(PATH_PREFIX_SEPARATOR).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
4975
6286
|
}
|
|
4976
|
-
function
|
|
4977
|
-
const normalizedPath =
|
|
4978
|
-
const normalizedPrefix =
|
|
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) ?
|
|
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 (
|
|
5001
|
-
if (
|
|
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) =>
|
|
6340
|
+
if (include.length > 0 && !include.some((prefix) => pathMatchesPrefix2(path6, prefix))) {
|
|
5030
6341
|
return false;
|
|
5031
6342
|
}
|
|
5032
|
-
return !exclude.some((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) =>
|
|
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
|
|
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) =>
|
|
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 ? [
|
|
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:
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
6208
|
-
import { isAbsolute as isAbsolute3, join as
|
|
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
|
|
7524
|
+
var PATH_SEGMENT_SEPARATOR2 = "/";
|
|
6214
7525
|
var GLOB_MARKER = "*";
|
|
6215
7526
|
var HIDDEN_PATH_PREFIX = ".";
|
|
6216
7527
|
var defaultScopeDeps = {
|
|
6217
|
-
readFileSync:
|
|
7528
|
+
readFileSync: readFileSync3,
|
|
6218
7529
|
existsSync: existsSync3,
|
|
6219
7530
|
readdirSync
|
|
6220
7531
|
};
|
|
6221
7532
|
function resolveProjectPath(projectRoot, path6) {
|
|
6222
|
-
return isAbsolute3(path6) ? 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
7652
|
+
import fs6 from "fs";
|
|
6342
7653
|
import { createRequire } from "module";
|
|
6343
|
-
import
|
|
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:
|
|
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:
|
|
7717
|
+
path: path5.dirname(bundledPath),
|
|
6407
7718
|
source: TOOL_DISCOVERY.SOURCES.BUNDLED
|
|
6408
7719
|
}
|
|
6409
7720
|
};
|
|
6410
7721
|
}
|
|
6411
|
-
const projectBinPath =
|
|
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
|
|
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) =>
|
|
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 =
|
|
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
|
|
7855
|
+
import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
|
|
6578
7856
|
import { tmpdir } from "os";
|
|
6579
|
-
import { isAbsolute as isAbsolute4, join as
|
|
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:
|
|
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 =
|
|
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(
|
|
6809
|
-
const configPath =
|
|
6810
|
-
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? 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:
|
|
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
|
|
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
|
|
6868
|
-
import { join as
|
|
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
|
-
|
|
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(
|
|
8053
|
+
return existsSync5(join22(productDir, file));
|
|
6922
8054
|
}
|
|
6923
8055
|
function findDeprecatedSpecNodePath(productDir) {
|
|
6924
8056
|
function visit2(relativeDirectory) {
|
|
6925
|
-
const absoluteDirectory =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
7364
|
-
import { join as
|
|
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
|
|
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
|
|
8504
|
+
dirEntries = await readdir7(absoluteDir, { withFileTypes: true });
|
|
7373
8505
|
} catch (err) {
|
|
7374
|
-
if (
|
|
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 =
|
|
8514
|
+
const absolutePath = join24(absoluteDir, entry.name);
|
|
7383
8515
|
await collectPaths(absolutePath, projectRoot, result, artifactDirs);
|
|
7384
8516
|
} else if (entry.isFile()) {
|
|
7385
|
-
const absolutePath =
|
|
7386
|
-
const rel =
|
|
7387
|
-
result.push(
|
|
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
|
|
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
|
|
8904
|
+
function applyPathFilter2(entries, pathConfig) {
|
|
7773
8905
|
if (pathConfig === void 0) {
|
|
7774
8906
|
return entries;
|
|
7775
8907
|
}
|
|
7776
|
-
const includePrefixes = (pathConfig.include ?? []).map(
|
|
7777
|
-
const excludePrefixes = (pathConfig.exclude ?? []).map(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
9190
|
+
const parent = join25(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
|
|
8059
9191
|
deps.mkdirSync(parent, { recursive: true });
|
|
8060
|
-
return deps.mkdtemp(
|
|
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 =
|
|
9200
|
+
const configPath = join25(tempDir, "tsconfig.json");
|
|
8069
9201
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
8070
|
-
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file :
|
|
9202
|
+
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join25(projectRoot, file));
|
|
8071
9203
|
const tempConfig = {
|
|
8072
|
-
extends:
|
|
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 =
|
|
9221
|
+
const configPath = join25(tempDir, "tsconfig.json");
|
|
8090
9222
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
8091
|
-
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern :
|
|
9223
|
+
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join25(projectRoot, pattern);
|
|
8092
9224
|
const tempConfig = {
|
|
8093
|
-
extends:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
8436
|
-
import { dirname as
|
|
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 =
|
|
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 =
|
|
8459
|
-
await
|
|
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
|
|