@outcomeeng/spx 0.5.3 → 0.5.4
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/README.md +17 -2
- package/dist/cli.js +1785 -1170
- package/dist/cli.js.map +1 -1
- package/package.json +3 -4
package/dist/cli.js
CHANGED
|
@@ -2,264 +2,6 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { createRequire as createRequire2 } from "module";
|
|
4
4
|
|
|
5
|
-
// src/domains/audit/paths.ts
|
|
6
|
-
import { existsSync } from "fs";
|
|
7
|
-
import { relative, resolve } from "path";
|
|
8
|
-
var AUDIT_PATH_DEFECT = {
|
|
9
|
-
ESCAPES_ROOT: "path escapes product directory",
|
|
10
|
-
MISSING_FILE: "missing file"
|
|
11
|
-
};
|
|
12
|
-
function validatePaths(verdict, productDir) {
|
|
13
|
-
const defects = [];
|
|
14
|
-
const root = resolve(productDir);
|
|
15
|
-
for (const gate of verdict.gates) {
|
|
16
|
-
for (const finding of gate.findings) {
|
|
17
|
-
checkPath(finding.spec_file, root, defects);
|
|
18
|
-
checkPath(finding.test_file, root, defects);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return defects;
|
|
22
|
-
}
|
|
23
|
-
function checkPath(filePath, root, defects) {
|
|
24
|
-
if (!filePath) return;
|
|
25
|
-
const rel = relative(root, resolve(root, filePath));
|
|
26
|
-
if (rel.startsWith("..")) {
|
|
27
|
-
defects.push(`${AUDIT_PATH_DEFECT.ESCAPES_ROOT}: ${filePath}`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (!existsSync(resolve(root, filePath))) {
|
|
31
|
-
defects.push(`${AUDIT_PATH_DEFECT.MISSING_FILE}: ${filePath}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/domains/audit/reader.ts
|
|
36
|
-
import { readFile } from "fs/promises";
|
|
37
|
-
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
38
|
-
var AUDIT_VERDICT_VALUE = {
|
|
39
|
-
APPROVED: "APPROVED",
|
|
40
|
-
REJECT: "REJECT"
|
|
41
|
-
};
|
|
42
|
-
var AUDIT_GATE_STATUS = {
|
|
43
|
-
FAIL: "FAIL",
|
|
44
|
-
PASS: "PASS",
|
|
45
|
-
SKIPPED: "SKIPPED"
|
|
46
|
-
};
|
|
47
|
-
var AUDIT_VERDICT_XML = {
|
|
48
|
-
ROOT: "audit_verdict",
|
|
49
|
-
HEADER: "header",
|
|
50
|
-
SPEC_NODE: "spec_node",
|
|
51
|
-
VERDICT: "verdict",
|
|
52
|
-
TIMESTAMP: "timestamp",
|
|
53
|
-
GATES: "gates",
|
|
54
|
-
GATE: "gate",
|
|
55
|
-
NAME: "name",
|
|
56
|
-
STATUS: "status",
|
|
57
|
-
SKIPPED_REASON: "skipped_reason",
|
|
58
|
-
FINDINGS: "findings",
|
|
59
|
-
FINDING: "finding",
|
|
60
|
-
COUNT: "count",
|
|
61
|
-
PARSED_COUNT: "@_count",
|
|
62
|
-
SPEC_FILE: "spec_file",
|
|
63
|
-
TEST_FILE: "test_file"
|
|
64
|
-
};
|
|
65
|
-
var ARRAY_TAGS = /* @__PURE__ */ new Set([AUDIT_VERDICT_XML.GATE, AUDIT_VERDICT_XML.FINDING]);
|
|
66
|
-
var PARSER_OPTIONS = {
|
|
67
|
-
ignoreAttributes: false,
|
|
68
|
-
attributeNamePrefix: "@_",
|
|
69
|
-
parseTagValue: false,
|
|
70
|
-
parseAttributeValue: false,
|
|
71
|
-
isArray: (name) => ARRAY_TAGS.has(name)
|
|
72
|
-
};
|
|
73
|
-
async function readVerdictFile(filePath) {
|
|
74
|
-
let xml;
|
|
75
|
-
try {
|
|
76
|
-
xml = await readFile(filePath, "utf-8");
|
|
77
|
-
} catch (cause) {
|
|
78
|
-
throw new Error(`Failed to read verdict file: ${filePath}`, { cause });
|
|
79
|
-
}
|
|
80
|
-
const validation = XMLValidator.validate(xml);
|
|
81
|
-
if (validation !== true) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`Malformed XML in verdict file: ${filePath}: ${validation.err.msg}`
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
const parser = new XMLParser(PARSER_OPTIONS);
|
|
87
|
-
const parsed = parser.parse(xml);
|
|
88
|
-
return buildVerdict(parsed);
|
|
89
|
-
}
|
|
90
|
-
function isObject(value) {
|
|
91
|
-
return typeof value === "object" && value !== null;
|
|
92
|
-
}
|
|
93
|
-
function asString(value) {
|
|
94
|
-
return typeof value === "string" ? value : void 0;
|
|
95
|
-
}
|
|
96
|
-
function buildVerdict(parsed) {
|
|
97
|
-
const rootUnknown = parsed[AUDIT_VERDICT_XML.ROOT];
|
|
98
|
-
if (!isObject(rootUnknown)) {
|
|
99
|
-
return { gates: [] };
|
|
100
|
-
}
|
|
101
|
-
const headerUnknown = rootUnknown[AUDIT_VERDICT_XML.HEADER];
|
|
102
|
-
const gatesUnknown = rootUnknown[AUDIT_VERDICT_XML.GATES];
|
|
103
|
-
return {
|
|
104
|
-
header: isObject(headerUnknown) ? buildHeader(headerUnknown) : void 0,
|
|
105
|
-
gates: isObject(gatesUnknown) ? buildGates(gatesUnknown) : []
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
function buildHeader(raw) {
|
|
109
|
-
return {
|
|
110
|
-
spec_node: asString(raw[AUDIT_VERDICT_XML.SPEC_NODE]),
|
|
111
|
-
verdict: asString(raw[AUDIT_VERDICT_XML.VERDICT]),
|
|
112
|
-
timestamp: asString(raw[AUDIT_VERDICT_XML.TIMESTAMP])
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function buildGates(raw) {
|
|
116
|
-
const gatesUnknown = raw[AUDIT_VERDICT_XML.GATE];
|
|
117
|
-
if (!Array.isArray(gatesUnknown)) return [];
|
|
118
|
-
return gatesUnknown.filter(isObject).map(buildGate);
|
|
119
|
-
}
|
|
120
|
-
function buildGate(raw) {
|
|
121
|
-
const findingsUnknown = raw[AUDIT_VERDICT_XML.FINDINGS];
|
|
122
|
-
return {
|
|
123
|
-
name: asString(raw[AUDIT_VERDICT_XML.NAME]),
|
|
124
|
-
status: asString(raw[AUDIT_VERDICT_XML.STATUS]),
|
|
125
|
-
skipped_reason: asString(raw[AUDIT_VERDICT_XML.SKIPPED_REASON]),
|
|
126
|
-
count: isObject(findingsUnknown) ? asString(findingsUnknown[AUDIT_VERDICT_XML.PARSED_COUNT]) : void 0,
|
|
127
|
-
findings: isObject(findingsUnknown) ? buildFindings(findingsUnknown) : []
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
function buildFindings(raw) {
|
|
131
|
-
const findingsUnknown = raw[AUDIT_VERDICT_XML.FINDING];
|
|
132
|
-
if (!Array.isArray(findingsUnknown)) return [];
|
|
133
|
-
return findingsUnknown.filter(isObject).map(buildFinding);
|
|
134
|
-
}
|
|
135
|
-
function buildFinding(raw) {
|
|
136
|
-
return {
|
|
137
|
-
spec_file: asString(raw[AUDIT_VERDICT_XML.SPEC_FILE]),
|
|
138
|
-
test_file: asString(raw[AUDIT_VERDICT_XML.TEST_FILE])
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/domains/audit/semantic.ts
|
|
143
|
-
function validateSemantics(verdict) {
|
|
144
|
-
const defects = [];
|
|
145
|
-
const hasFail = verdict.gates.some((g) => g.status === AUDIT_GATE_STATUS.FAIL);
|
|
146
|
-
const hasSkipped = verdict.gates.some((g) => g.status === AUDIT_GATE_STATUS.SKIPPED);
|
|
147
|
-
if (verdict.header?.verdict === AUDIT_VERDICT_VALUE.APPROVED && (hasFail || hasSkipped)) {
|
|
148
|
-
defects.push("incoherent verdict: APPROVED but at least one gate is not PASS");
|
|
149
|
-
} else if (verdict.header?.verdict === AUDIT_VERDICT_VALUE.REJECT && !hasFail && !hasSkipped) {
|
|
150
|
-
defects.push("incoherent verdict: REJECT but all gates are PASS");
|
|
151
|
-
}
|
|
152
|
-
for (const gate of verdict.gates) {
|
|
153
|
-
const label = gate.name ? `gate "${gate.name}"` : "gate";
|
|
154
|
-
if (gate.status === AUDIT_GATE_STATUS.FAIL && gate.findings.length === 0) {
|
|
155
|
-
defects.push(`failed gate has no findings: ${label}`);
|
|
156
|
-
}
|
|
157
|
-
if (gate.status === AUDIT_GATE_STATUS.SKIPPED && !gate.skipped_reason) {
|
|
158
|
-
defects.push(`skipped gate missing reason: ${label}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return defects;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// src/domains/audit/structural.ts
|
|
165
|
-
var VALID_VERDICTS = new Set(Object.values(AUDIT_VERDICT_VALUE));
|
|
166
|
-
var VALID_GATE_STATUSES = new Set(Object.values(AUDIT_GATE_STATUS));
|
|
167
|
-
var STRUCTURAL_DEFECT_TEXT = {
|
|
168
|
-
MISSING_REQUIRED_ELEMENT: "missing required element",
|
|
169
|
-
INVALID_ENUM_VALUE: "invalid enum value"
|
|
170
|
-
};
|
|
171
|
-
function validateStructure(verdict) {
|
|
172
|
-
const defects = [];
|
|
173
|
-
if (!verdict.header) {
|
|
174
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <header>`);
|
|
175
|
-
return defects;
|
|
176
|
-
}
|
|
177
|
-
if (!verdict.header.spec_node) {
|
|
178
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <spec_node>`);
|
|
179
|
-
}
|
|
180
|
-
if (!verdict.header.verdict) {
|
|
181
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <verdict> in <header>`);
|
|
182
|
-
} else if (!VALID_VERDICTS.has(verdict.header.verdict)) {
|
|
183
|
-
defects.push(
|
|
184
|
-
`${STRUCTURAL_DEFECT_TEXT.INVALID_ENUM_VALUE}: verdict "${verdict.header.verdict}" is not one of APPROVED, REJECT`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
if (!verdict.header.timestamp) {
|
|
188
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <timestamp>`);
|
|
189
|
-
}
|
|
190
|
-
if (verdict.gates.length === 0) {
|
|
191
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: at least one <gate> in <gates>`);
|
|
192
|
-
}
|
|
193
|
-
for (const gate of verdict.gates) {
|
|
194
|
-
const label = gate.name ? `gate "${gate.name}"` : "gate";
|
|
195
|
-
if (gate.status !== void 0 && !VALID_GATE_STATUSES.has(gate.status)) {
|
|
196
|
-
defects.push(
|
|
197
|
-
`${STRUCTURAL_DEFECT_TEXT.INVALID_ENUM_VALUE}: ${label} status "${gate.status}" is not one of PASS, FAIL, SKIPPED`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (gate.count !== void 0) {
|
|
201
|
-
const declared = parseInt(gate.count, 10);
|
|
202
|
-
const actual = gate.findings.length;
|
|
203
|
-
if (isNaN(declared) || declared !== actual) {
|
|
204
|
-
defects.push(
|
|
205
|
-
`count mismatch: ${label} declares count="${gate.count}" but has ${actual} finding(s)`
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return defects;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// src/domains/audit/verify.ts
|
|
214
|
-
async function runVerifyPipeline(filePath, productDir) {
|
|
215
|
-
let verdict;
|
|
216
|
-
try {
|
|
217
|
-
verdict = await readVerdictFile(filePath);
|
|
218
|
-
} catch (error) {
|
|
219
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
-
return { lines: [`reader: ${message}`], exitCode: 1 };
|
|
221
|
-
}
|
|
222
|
-
const structuralDefects = validateStructure(verdict);
|
|
223
|
-
if (structuralDefects.length > 0) {
|
|
224
|
-
return { lines: structuralDefects.map((d) => `structural: ${d}`), exitCode: 1 };
|
|
225
|
-
}
|
|
226
|
-
const semanticDefects = validateSemantics(verdict);
|
|
227
|
-
if (semanticDefects.length > 0) {
|
|
228
|
-
return { lines: semanticDefects.map((d) => `semantic: ${d}`), exitCode: 1 };
|
|
229
|
-
}
|
|
230
|
-
const pathDefects = validatePaths(verdict, productDir);
|
|
231
|
-
if (pathDefects.length > 0) {
|
|
232
|
-
return { lines: pathDefects.map((d) => `paths: ${d}`), exitCode: 1 };
|
|
233
|
-
}
|
|
234
|
-
return { lines: [], exitCode: 0, verdict: verdict.header?.verdict };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/commands/audit/verify.ts
|
|
238
|
-
async function runVerifyCommand(filePath, productDir, writeLine) {
|
|
239
|
-
const result = await runVerifyPipeline(filePath, productDir);
|
|
240
|
-
if (result.exitCode === 0) {
|
|
241
|
-
writeLine(result.verdict ?? "");
|
|
242
|
-
} else {
|
|
243
|
-
for (const line of result.lines) {
|
|
244
|
-
writeLine(line);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return result.exitCode;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// src/interfaces/cli/audit.ts
|
|
251
|
-
var auditDomain = {
|
|
252
|
-
name: "audit",
|
|
253
|
-
description: "Audit verdict verification",
|
|
254
|
-
register: (program2) => {
|
|
255
|
-
const auditCmd = program2.command("audit").description("Audit verdict verification");
|
|
256
|
-
auditCmd.command("verify <file>").description("Verify an audit verdict XML file").action(async (file) => {
|
|
257
|
-
const exitCode = await runVerifyCommand(file, process.cwd(), console.log);
|
|
258
|
-
process.exit(exitCode);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
5
|
// src/commands/claude/init.ts
|
|
264
6
|
import { execa } from "execa";
|
|
265
7
|
async function initCommand(options = {}) {
|
|
@@ -762,149 +504,988 @@ async function writeSettings(filePath, settings, deps = { fs: realFs }) {
|
|
|
762
504
|
`settings-${Date.now()}-${Math.random().toString(36).substring(7)}.json`
|
|
763
505
|
);
|
|
764
506
|
try {
|
|
765
|
-
const content = JSON.stringify(settings, null, 2) + "\n";
|
|
766
|
-
await deps.fs.writeFile(tempPath, content);
|
|
767
|
-
await deps.fs.rename(tempPath, filePath);
|
|
507
|
+
const content = JSON.stringify(settings, null, 2) + "\n";
|
|
508
|
+
await deps.fs.writeFile(tempPath, content);
|
|
509
|
+
await deps.fs.rename(tempPath, filePath);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
try {
|
|
512
|
+
await deps.fs.unlink(tempPath);
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
if (error instanceof Error) {
|
|
516
|
+
throw new Error(`Failed to write settings: ${error.message}`);
|
|
517
|
+
}
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/commands/claude/settings/consolidate.ts
|
|
523
|
+
import os2 from "os";
|
|
524
|
+
import path3 from "path";
|
|
525
|
+
async function consolidateCommand(options = {}) {
|
|
526
|
+
const root = options.root ? path3.resolve(options.root.replace(/^~/, os2.homedir())) : path3.join(os2.homedir(), "Code");
|
|
527
|
+
const globalSettingsPath = options.globalSettings || path3.join(os2.homedir(), ".claude", "settings.json");
|
|
528
|
+
const shouldWrite = options.write || false;
|
|
529
|
+
const outputFile = options.outputFile;
|
|
530
|
+
const previewOnly = !shouldWrite && !outputFile;
|
|
531
|
+
const settingsFiles = await findSettingsFiles(root);
|
|
532
|
+
if (settingsFiles.length === 0) {
|
|
533
|
+
return `No settings files found in ${root}
|
|
534
|
+
|
|
535
|
+
Searched for: **/.claude/settings.local.json`;
|
|
536
|
+
}
|
|
537
|
+
const localPermissions = await parseAllSettings(settingsFiles);
|
|
538
|
+
let globalSettings = await parseSettingsFile(globalSettingsPath);
|
|
539
|
+
if (!globalSettings) {
|
|
540
|
+
globalSettings = {
|
|
541
|
+
permissions: {
|
|
542
|
+
allow: [],
|
|
543
|
+
deny: [],
|
|
544
|
+
ask: []
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (!globalSettings.permissions) {
|
|
549
|
+
globalSettings.permissions = {
|
|
550
|
+
allow: [],
|
|
551
|
+
deny: [],
|
|
552
|
+
ask: []
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const { merged, result } = mergePermissions(
|
|
556
|
+
globalSettings.permissions,
|
|
557
|
+
localPermissions
|
|
558
|
+
);
|
|
559
|
+
if (shouldWrite) {
|
|
560
|
+
try {
|
|
561
|
+
result.backupPath = await createBackup(globalSettingsPath);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof Error && !error.message.includes("not found")) {
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (shouldWrite) {
|
|
569
|
+
const updatedSettings = {
|
|
570
|
+
...globalSettings,
|
|
571
|
+
permissions: merged
|
|
572
|
+
};
|
|
573
|
+
await writeSettings(globalSettingsPath, updatedSettings);
|
|
574
|
+
} else if (outputFile) {
|
|
575
|
+
const updatedSettings = {
|
|
576
|
+
...globalSettings,
|
|
577
|
+
permissions: merged
|
|
578
|
+
};
|
|
579
|
+
const resolvedOutputPath = path3.resolve(outputFile.replace(/^~/, os2.homedir()));
|
|
580
|
+
await writeSettings(resolvedOutputPath, updatedSettings);
|
|
581
|
+
result.outputPath = resolvedOutputPath;
|
|
582
|
+
}
|
|
583
|
+
return formatReport(result, previewOnly, globalSettingsPath, outputFile);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/interfaces/cli/claude.ts
|
|
587
|
+
function registerClaudeCommands(claudeCmd) {
|
|
588
|
+
claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
|
|
589
|
+
try {
|
|
590
|
+
const output = await initCommand({ cwd: process.cwd() });
|
|
591
|
+
console.log(output);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
console.error(
|
|
594
|
+
"Error:",
|
|
595
|
+
error instanceof Error ? error.message : String(error)
|
|
596
|
+
);
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
const settingsCmd = claudeCmd.command("settings").description("Manage Claude Code settings");
|
|
601
|
+
settingsCmd.command("consolidate").description(
|
|
602
|
+
"Consolidate permissions from project-specific settings into global settings"
|
|
603
|
+
).option("--write", "Write changes to global settings file (default: preview only)").option(
|
|
604
|
+
"--output-file <path>",
|
|
605
|
+
"Write merged settings to specified file instead of global settings"
|
|
606
|
+
).option(
|
|
607
|
+
"--root <path>",
|
|
608
|
+
"Root directory to scan for settings files (default: ~/Code)"
|
|
609
|
+
).option(
|
|
610
|
+
"--global-settings <path>",
|
|
611
|
+
"Path to global settings file (default: ~/.claude/settings.json)"
|
|
612
|
+
).action(
|
|
613
|
+
async (options) => {
|
|
614
|
+
try {
|
|
615
|
+
if (options.write && options.outputFile) {
|
|
616
|
+
console.error(
|
|
617
|
+
"Error: --write and --output-file are mutually exclusive\nUse --write to modify global settings, or --output-file to write to a different location"
|
|
618
|
+
);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
const output = await consolidateCommand({
|
|
622
|
+
write: options.write,
|
|
623
|
+
outputFile: options.outputFile,
|
|
624
|
+
root: options.root,
|
|
625
|
+
globalSettings: options.globalSettings
|
|
626
|
+
});
|
|
627
|
+
console.log(output);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
console.error(
|
|
630
|
+
"Error:",
|
|
631
|
+
error instanceof Error ? error.message : String(error)
|
|
632
|
+
);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
var claudeDomain = {
|
|
639
|
+
name: "claude",
|
|
640
|
+
description: "Manage Claude Code settings and plugins",
|
|
641
|
+
register: (program2) => {
|
|
642
|
+
const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
|
|
643
|
+
registerClaudeCommands(claudeCmd);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/domains/compact/index.ts
|
|
648
|
+
import { join as join3 } from "path";
|
|
649
|
+
|
|
650
|
+
// src/domains/session/agent-session.ts
|
|
651
|
+
import { createHash } from "crypto";
|
|
652
|
+
var AGENT_SESSION_ENV = {
|
|
653
|
+
CLAUDE_SESSION_ID: "CLAUDE_SESSION_ID",
|
|
654
|
+
CODEX_THREAD_ID: "CODEX_THREAD_ID"
|
|
655
|
+
};
|
|
656
|
+
var AGENT_SESSION_TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
657
|
+
var AGENT_SESSION_TOKEN_UNSAFE_PATTERN = /[^A-Za-z0-9_-]+/g;
|
|
658
|
+
var AGENT_SESSION_TOKEN_EDGE_SEPARATOR_PATTERN = /^-+|-+$/g;
|
|
659
|
+
var AGENT_SESSION_TOKEN_SEPARATOR = "-";
|
|
660
|
+
var AGENT_SESSION_TOKEN_HASH_ALGORITHM = "sha256";
|
|
661
|
+
var AGENT_SESSION_TOKEN_HASH_ENCODING = "hex";
|
|
662
|
+
var AGENT_SESSION_TOKEN_HASH_LENGTH = 12;
|
|
663
|
+
var EMPTY_TOKEN = "";
|
|
664
|
+
function nonEmptyEnvValue(value) {
|
|
665
|
+
return value === void 0 || value.length === 0 ? void 0 : value;
|
|
666
|
+
}
|
|
667
|
+
function hashAgentSessionToken(value) {
|
|
668
|
+
return createHash(AGENT_SESSION_TOKEN_HASH_ALGORITHM).update(value).digest(AGENT_SESSION_TOKEN_HASH_ENCODING).slice(0, AGENT_SESSION_TOKEN_HASH_LENGTH);
|
|
669
|
+
}
|
|
670
|
+
function normalizeAgentSessionToken(value) {
|
|
671
|
+
if (AGENT_SESSION_TOKEN_PATTERN.test(value)) return value;
|
|
672
|
+
const normalized = value.replace(AGENT_SESSION_TOKEN_UNSAFE_PATTERN, AGENT_SESSION_TOKEN_SEPARATOR).replace(AGENT_SESSION_TOKEN_EDGE_SEPARATOR_PATTERN, EMPTY_TOKEN);
|
|
673
|
+
const hash = hashAgentSessionToken(value);
|
|
674
|
+
return normalized.length === 0 ? hash : `${normalized}${AGENT_SESSION_TOKEN_SEPARATOR}${hash}`;
|
|
675
|
+
}
|
|
676
|
+
function resolveAgentSessionId(env) {
|
|
677
|
+
const raw = nonEmptyEnvValue(env[AGENT_SESSION_ENV.CLAUDE_SESSION_ID]) ?? nonEmptyEnvValue(env[AGENT_SESSION_ENV.CODEX_THREAD_ID]);
|
|
678
|
+
return raw === void 0 ? void 0 : normalizeAgentSessionToken(raw);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/lib/state-store/index.ts
|
|
682
|
+
import { createHash as createHash2, randomBytes as nodeRandomBytes } from "crypto";
|
|
683
|
+
import {
|
|
684
|
+
appendFile as nodeAppendFile,
|
|
685
|
+
mkdir as nodeMkdir,
|
|
686
|
+
readdir as nodeReaddir,
|
|
687
|
+
readFile as nodeReadFile,
|
|
688
|
+
writeFile as nodeWriteFile
|
|
689
|
+
} from "fs/promises";
|
|
690
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
691
|
+
|
|
692
|
+
// src/git/root.ts
|
|
693
|
+
import { execa as execa2 } from "execa";
|
|
694
|
+
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
|
695
|
+
|
|
696
|
+
// src/git/environment.ts
|
|
697
|
+
function withoutGitEnvironment(env) {
|
|
698
|
+
const cleaned = { ...env };
|
|
699
|
+
for (const key of Object.keys(cleaned)) {
|
|
700
|
+
if (key.startsWith("GIT_")) {
|
|
701
|
+
delete cleaned[key];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return cleaned;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/git/root.ts
|
|
708
|
+
var defaultGitDependencies = {
|
|
709
|
+
execa: async (command, args, options) => {
|
|
710
|
+
const result = await execa2(command, args, {
|
|
711
|
+
...options,
|
|
712
|
+
env: withoutGitEnvironment(process.env),
|
|
713
|
+
extendEnv: false
|
|
714
|
+
});
|
|
715
|
+
return {
|
|
716
|
+
exitCode: result.exitCode ?? 0,
|
|
717
|
+
stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
|
|
718
|
+
stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
|
|
723
|
+
var GIT_ROOT_COMMAND = {
|
|
724
|
+
EXECUTABLE: "git",
|
|
725
|
+
REV_PARSE: "rev-parse",
|
|
726
|
+
ABBREV_REF: "--abbrev-ref",
|
|
727
|
+
GIT_COMMON_DIR: "--git-common-dir",
|
|
728
|
+
HEAD: "HEAD",
|
|
729
|
+
SHOW_TOPLEVEL: "--show-toplevel",
|
|
730
|
+
SYMBOLIC_REF: "symbolic-ref",
|
|
731
|
+
SHOW_REF: "show-ref",
|
|
732
|
+
VERIFY: "--verify",
|
|
733
|
+
QUIET: "--quiet",
|
|
734
|
+
SHORT: "--short",
|
|
735
|
+
ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
|
|
736
|
+
STATUS: "status",
|
|
737
|
+
WORKTREE: "worktree",
|
|
738
|
+
LIST: "list",
|
|
739
|
+
PORCELAIN: "--porcelain",
|
|
740
|
+
PATH_FORMAT_ABSOLUTE: "--path-format=absolute",
|
|
741
|
+
CONFIG: "config",
|
|
742
|
+
CONFIG_GET: "--get",
|
|
743
|
+
CONFIG_TYPE_BOOL: "--type=bool",
|
|
744
|
+
CORE_BARE_KEY: "core.bare",
|
|
745
|
+
REMOTE: "remote",
|
|
746
|
+
GET_URL: "get-url",
|
|
747
|
+
ORIGIN: "origin"
|
|
748
|
+
};
|
|
749
|
+
var GIT_URL_SUFFIX = ".git";
|
|
750
|
+
var GIT_DIR_BASENAME = ".git";
|
|
751
|
+
var ORIGIN_REF_PREFIX = "origin/";
|
|
752
|
+
var REMOTE_ORIGIN_REF_PREFIX = "refs/remotes/origin/";
|
|
753
|
+
var GIT_CORE_BARE_TRUE = "true";
|
|
754
|
+
var GIT_SHOW_TOPLEVEL_ARGS = [
|
|
755
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
756
|
+
GIT_ROOT_COMMAND.SHOW_TOPLEVEL
|
|
757
|
+
];
|
|
758
|
+
var GIT_COMMON_DIR_ARGS = [
|
|
759
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
760
|
+
GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
|
|
761
|
+
GIT_ROOT_COMMAND.GIT_COMMON_DIR
|
|
762
|
+
];
|
|
763
|
+
var GIT_CORE_BARE_ARGS = [
|
|
764
|
+
GIT_ROOT_COMMAND.CONFIG,
|
|
765
|
+
GIT_ROOT_COMMAND.CONFIG_GET,
|
|
766
|
+
GIT_ROOT_COMMAND.CONFIG_TYPE_BOOL,
|
|
767
|
+
GIT_ROOT_COMMAND.CORE_BARE_KEY
|
|
768
|
+
];
|
|
769
|
+
var GIT_REMOTE_GET_URL_ORIGIN_ARGS = [
|
|
770
|
+
GIT_ROOT_COMMAND.REMOTE,
|
|
771
|
+
GIT_ROOT_COMMAND.GET_URL,
|
|
772
|
+
GIT_ROOT_COMMAND.ORIGIN
|
|
773
|
+
];
|
|
774
|
+
var GIT_CURRENT_BRANCH_ARGS = [
|
|
775
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
776
|
+
GIT_ROOT_COMMAND.ABBREV_REF,
|
|
777
|
+
GIT_ROOT_COMMAND.HEAD
|
|
778
|
+
];
|
|
779
|
+
var GIT_HEAD_SHA_ARGS = [
|
|
780
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
781
|
+
GIT_ROOT_COMMAND.HEAD
|
|
782
|
+
];
|
|
783
|
+
var GIT_ORIGIN_HEAD_REF_ARGS = [
|
|
784
|
+
GIT_ROOT_COMMAND.SYMBOLIC_REF,
|
|
785
|
+
GIT_ROOT_COMMAND.SHORT,
|
|
786
|
+
GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
|
|
787
|
+
];
|
|
788
|
+
var GIT_STATUS_PORCELAIN_ARGS = [
|
|
789
|
+
GIT_ROOT_COMMAND.STATUS,
|
|
790
|
+
GIT_ROOT_COMMAND.PORCELAIN
|
|
791
|
+
];
|
|
792
|
+
var GIT_WORKTREE_LIST_PORCELAIN_ARGS = [
|
|
793
|
+
GIT_ROOT_COMMAND.WORKTREE,
|
|
794
|
+
GIT_ROOT_COMMAND.LIST,
|
|
795
|
+
GIT_ROOT_COMMAND.PORCELAIN
|
|
796
|
+
];
|
|
797
|
+
var GIT_WORKTREE_PORCELAIN_ROOT_PREFIX = "worktree ";
|
|
798
|
+
var GIT_WORKTREE_PORCELAIN_BARE_LINE = "bare";
|
|
799
|
+
var GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE = "prunable";
|
|
800
|
+
var GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX = `${GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE} `;
|
|
801
|
+
var TRAILING_PATH_SEPARATORS_PATTERN = /[\\/]+$/;
|
|
802
|
+
async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
803
|
+
try {
|
|
804
|
+
const result = await deps.execa(
|
|
805
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
806
|
+
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
807
|
+
{ cwd, reject: false }
|
|
808
|
+
);
|
|
809
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
810
|
+
return {
|
|
811
|
+
productDir: extractStdout(result.stdout),
|
|
812
|
+
isGitRepo: true
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
productDir: cwd,
|
|
817
|
+
isGitRepo: false,
|
|
818
|
+
warning: NOT_GIT_REPO_WARNING
|
|
819
|
+
};
|
|
820
|
+
} catch {
|
|
821
|
+
return {
|
|
822
|
+
productDir: cwd,
|
|
823
|
+
isGitRepo: false,
|
|
824
|
+
warning: NOT_GIT_REPO_WARNING
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function extractStdout(stdout) {
|
|
829
|
+
if (!stdout) return "";
|
|
830
|
+
const str = typeof stdout === "string" ? stdout : String(stdout);
|
|
831
|
+
return str.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
|
|
832
|
+
}
|
|
833
|
+
function trimStdout(stdout) {
|
|
834
|
+
if (!stdout) return "";
|
|
835
|
+
const str = typeof stdout === "string" ? stdout : String(stdout);
|
|
836
|
+
return str.trim();
|
|
837
|
+
}
|
|
838
|
+
async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
839
|
+
try {
|
|
840
|
+
const toplevelResult = await deps.execa(
|
|
841
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
842
|
+
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
843
|
+
{ cwd, reject: false }
|
|
844
|
+
);
|
|
845
|
+
if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
|
|
846
|
+
return {
|
|
847
|
+
productDir: cwd,
|
|
848
|
+
isGitRepo: false,
|
|
849
|
+
warning: NOT_GIT_REPO_WARNING,
|
|
850
|
+
worktreeRoot: cwd
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const toplevel = extractStdout(toplevelResult.stdout);
|
|
854
|
+
const commonDirResult = await deps.execa(
|
|
855
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
856
|
+
[...GIT_COMMON_DIR_ARGS],
|
|
857
|
+
{ cwd, reject: false }
|
|
858
|
+
);
|
|
859
|
+
if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
|
|
860
|
+
return {
|
|
861
|
+
productDir: toplevel,
|
|
862
|
+
isGitRepo: true,
|
|
863
|
+
worktreeRoot: toplevel
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const commonDir = extractStdout(commonDirResult.stdout);
|
|
867
|
+
const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(toplevel, commonDir);
|
|
868
|
+
const gitCommonDirProductRoot = dirname(absoluteCommonDir);
|
|
869
|
+
return {
|
|
870
|
+
productDir: gitCommonDirProductRoot,
|
|
871
|
+
isGitRepo: true,
|
|
872
|
+
worktreeRoot: toplevel
|
|
873
|
+
};
|
|
874
|
+
} catch {
|
|
875
|
+
return {
|
|
876
|
+
productDir: cwd,
|
|
877
|
+
isGitRepo: false,
|
|
878
|
+
warning: NOT_GIT_REPO_WARNING,
|
|
879
|
+
worktreeRoot: cwd
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
884
|
+
const result = await deps.execa(
|
|
885
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
886
|
+
[...GIT_ORIGIN_HEAD_REF_ARGS],
|
|
887
|
+
{ cwd, reject: false }
|
|
888
|
+
);
|
|
889
|
+
if (result.exitCode !== 0) return null;
|
|
890
|
+
const ref = extractStdout(result.stdout);
|
|
891
|
+
if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
|
|
892
|
+
const branch = ref.slice(ORIGIN_REF_PREFIX.length);
|
|
893
|
+
return branch.length === 0 ? null : branch;
|
|
894
|
+
}
|
|
895
|
+
async function getCurrentBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
896
|
+
const result = await deps.execa(
|
|
897
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
898
|
+
[...GIT_CURRENT_BRANCH_ARGS],
|
|
899
|
+
{ cwd, reject: false }
|
|
900
|
+
);
|
|
901
|
+
if (result.exitCode !== 0) return null;
|
|
902
|
+
const branch = extractStdout(result.stdout);
|
|
903
|
+
if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
|
|
904
|
+
return branch;
|
|
905
|
+
}
|
|
906
|
+
async function getHeadSha(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
907
|
+
const result = await deps.execa(
|
|
908
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
909
|
+
[...GIT_HEAD_SHA_ARGS],
|
|
910
|
+
{ cwd, reject: false }
|
|
911
|
+
);
|
|
912
|
+
if (result.exitCode !== 0) return null;
|
|
913
|
+
const sha = extractStdout(result.stdout);
|
|
914
|
+
return sha.length === 0 ? null : sha;
|
|
915
|
+
}
|
|
916
|
+
async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
917
|
+
const result = await deps.execa(
|
|
918
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
919
|
+
[GIT_ROOT_COMMAND.REV_PARSE, ref],
|
|
920
|
+
{ cwd, reject: false }
|
|
921
|
+
);
|
|
922
|
+
if (result.exitCode !== 0) return null;
|
|
923
|
+
const sha = extractStdout(result.stdout);
|
|
924
|
+
return sha.length === 0 ? null : sha;
|
|
925
|
+
}
|
|
926
|
+
async function originBranchExists(branch, cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
927
|
+
if (branch === GIT_ROOT_COMMAND.HEAD) return false;
|
|
928
|
+
const result = await deps.execa(
|
|
929
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
930
|
+
[
|
|
931
|
+
GIT_ROOT_COMMAND.SHOW_REF,
|
|
932
|
+
GIT_ROOT_COMMAND.VERIFY,
|
|
933
|
+
GIT_ROOT_COMMAND.QUIET,
|
|
934
|
+
`${REMOTE_ORIGIN_REF_PREFIX}${branch}`
|
|
935
|
+
],
|
|
936
|
+
{ cwd, reject: false }
|
|
937
|
+
);
|
|
938
|
+
return result.exitCode === 0;
|
|
939
|
+
}
|
|
940
|
+
async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
941
|
+
const result = await deps.execa(
|
|
942
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
943
|
+
[...GIT_STATUS_PORCELAIN_ARGS],
|
|
944
|
+
{ cwd, reject: false }
|
|
945
|
+
);
|
|
946
|
+
if (result.exitCode !== 0) return false;
|
|
947
|
+
return extractStdout(result.stdout).length === 0;
|
|
948
|
+
}
|
|
949
|
+
function repositoryName(originUrl) {
|
|
950
|
+
if (originUrl === null) return null;
|
|
951
|
+
const trimmed = originUrl.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
|
|
952
|
+
const lastSeparator = Math.max(
|
|
953
|
+
trimmed.lastIndexOf("/"),
|
|
954
|
+
trimmed.lastIndexOf("\\"),
|
|
955
|
+
trimmed.lastIndexOf(":")
|
|
956
|
+
);
|
|
957
|
+
const segment = trimmed.slice(lastSeparator + 1);
|
|
958
|
+
const name = segment.endsWith(GIT_URL_SUFFIX) ? segment.slice(0, -GIT_URL_SUFFIX.length) : segment;
|
|
959
|
+
return name.length === 0 ? null : name;
|
|
960
|
+
}
|
|
961
|
+
function normalizeGitPath(path6) {
|
|
962
|
+
return path6.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
|
|
963
|
+
}
|
|
964
|
+
function normalizedGitPathKey(path6) {
|
|
965
|
+
return normalizeGitPath(path6).replace(/\\/g, "/");
|
|
966
|
+
}
|
|
967
|
+
function isObservedWorktreeRoot(worktreeRoots, candidate) {
|
|
968
|
+
const candidateKey = normalizedGitPathKey(candidate);
|
|
969
|
+
return worktreeRoots.some((root) => normalizedGitPathKey(root) === candidateKey);
|
|
970
|
+
}
|
|
971
|
+
function isPrunableWorktreeRecordLine(line) {
|
|
972
|
+
return line === GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE || line.startsWith(GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX);
|
|
973
|
+
}
|
|
974
|
+
function parseWorktreeRoots(stdout) {
|
|
975
|
+
const roots = [];
|
|
976
|
+
for (const record of stdout.split(/\n\n+/)) {
|
|
977
|
+
const lines = record.split("\n");
|
|
978
|
+
if (lines.includes(GIT_WORKTREE_PORCELAIN_BARE_LINE) || lines.some(isPrunableWorktreeRecordLine)) continue;
|
|
979
|
+
const rootLine = lines.find((line) => line.startsWith(GIT_WORKTREE_PORCELAIN_ROOT_PREFIX));
|
|
980
|
+
if (rootLine === void 0) continue;
|
|
981
|
+
const root = normalizeGitPath(rootLine.slice(GIT_WORKTREE_PORCELAIN_ROOT_PREFIX.length));
|
|
982
|
+
if (root.length > 0) roots.push(root);
|
|
983
|
+
}
|
|
984
|
+
return roots;
|
|
985
|
+
}
|
|
986
|
+
function observedWorktreeRoots(worktreeListResult) {
|
|
987
|
+
if (worktreeListResult.exitCode !== 0) return [];
|
|
988
|
+
return [...new Set(parseWorktreeRoots(worktreeListResult.stdout))];
|
|
989
|
+
}
|
|
990
|
+
function isMainCheckout(facts) {
|
|
991
|
+
const commonDirParent = dirname(facts.commonDir);
|
|
992
|
+
if (!facts.commonDirIsBare) return commonDirParent === facts.worktreeRoot;
|
|
993
|
+
if (commonDirParent !== dirname(facts.worktreeRoot)) return false;
|
|
994
|
+
const name = repositoryName(facts.originUrl);
|
|
995
|
+
return name !== null && basename(facts.worktreeRoot) === name && facts.worktreeListRead && isObservedWorktreeRoot(facts.worktreeRoots, facts.worktreeRoot);
|
|
996
|
+
}
|
|
997
|
+
function mainCheckoutPath(facts) {
|
|
998
|
+
const commonDirParent = dirname(facts.commonDir);
|
|
999
|
+
if (!facts.commonDirIsBare) return commonDirParent;
|
|
1000
|
+
const name = repositoryName(facts.originUrl);
|
|
1001
|
+
if (name === null) return null;
|
|
1002
|
+
if (!facts.worktreeListRead) return null;
|
|
1003
|
+
const candidate = join(commonDirParent, name);
|
|
1004
|
+
return isObservedWorktreeRoot(facts.worktreeRoots, candidate) ? candidate : null;
|
|
1005
|
+
}
|
|
1006
|
+
async function gatherGitFacts(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
1007
|
+
try {
|
|
1008
|
+
const [toplevelResult, commonDirResult, originResult, worktreeListResult] = await Promise.all([
|
|
1009
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_SHOW_TOPLEVEL_ARGS], { cwd, reject: false }),
|
|
1010
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_COMMON_DIR_ARGS], { cwd, reject: false }),
|
|
1011
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_REMOTE_GET_URL_ORIGIN_ARGS], { cwd, reject: false }),
|
|
1012
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_WORKTREE_LIST_PORCELAIN_ARGS], { cwd, reject: false })
|
|
1013
|
+
]);
|
|
1014
|
+
if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) return null;
|
|
1015
|
+
const worktreeRoot = extractStdout(toplevelResult.stdout);
|
|
1016
|
+
const worktreeListRead = worktreeListResult.exitCode === 0;
|
|
1017
|
+
const worktreeRoots = observedWorktreeRoots(worktreeListResult);
|
|
1018
|
+
const originUrl = originResult.exitCode === 0 && originResult.stdout ? trimStdout(originResult.stdout) : null;
|
|
1019
|
+
if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
|
|
1020
|
+
return {
|
|
1021
|
+
worktreeRoot,
|
|
1022
|
+
worktreeRoots,
|
|
1023
|
+
worktreeListRead,
|
|
1024
|
+
commonDir: join(worktreeRoot, GIT_DIR_BASENAME),
|
|
1025
|
+
commonDirIsBare: false,
|
|
1026
|
+
originUrl
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const rawCommonDir = extractStdout(commonDirResult.stdout);
|
|
1030
|
+
const commonDir = isAbsolute(rawCommonDir) ? rawCommonDir : resolve(worktreeRoot, rawCommonDir);
|
|
1031
|
+
const bareResult = await deps.execa(
|
|
1032
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
1033
|
+
[...GIT_CORE_BARE_ARGS],
|
|
1034
|
+
{ cwd, reject: false }
|
|
1035
|
+
);
|
|
1036
|
+
const commonDirIsBare = bareResult.exitCode === 0 && extractStdout(bareResult.stdout) === GIT_CORE_BARE_TRUE;
|
|
1037
|
+
return { worktreeRoot, worktreeRoots, worktreeListRead, commonDir, commonDirIsBare, originUrl };
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/lib/state-store/index.ts
|
|
1044
|
+
var STATE_STORE_PATH = {
|
|
1045
|
+
SPX_DIR: ".spx",
|
|
1046
|
+
BRANCH_SCOPE: "branch",
|
|
1047
|
+
WORKTREE_SCOPE: "worktree",
|
|
1048
|
+
SESSIONS_SCOPE: "sessions",
|
|
1049
|
+
RUNS_DIR: "runs",
|
|
1050
|
+
RUN_FILE_PREFIX: "run-",
|
|
1051
|
+
JSONL_EXTENSION: ".jsonl"
|
|
1052
|
+
};
|
|
1053
|
+
var STATE_STORE_DOMAIN = {
|
|
1054
|
+
AUDIT: "audit",
|
|
1055
|
+
COMPACT: "compact",
|
|
1056
|
+
REVIEW: "review",
|
|
1057
|
+
TEST: "test"
|
|
1058
|
+
};
|
|
1059
|
+
var STATE_STORE_ERROR = {
|
|
1060
|
+
INVALID_TOKEN: "state-store token must be a safe path segment",
|
|
1061
|
+
INVALID_BRANCH_SLUG: "state-store branch slug must be normalized before storage",
|
|
1062
|
+
RUN_FILE_CREATE_FAILED: "state-store run file create failed",
|
|
1063
|
+
RUN_FILE_COLLISION_LIMIT: "state-store run file collision limit exhausted",
|
|
1064
|
+
RECORD_ALREADY_EXISTS: "state-store record already exists",
|
|
1065
|
+
RECORD_WRITE_FAILED: "state-store record write failed",
|
|
1066
|
+
RECORD_READ_FAILED: "state-store record read failed"
|
|
1067
|
+
};
|
|
1068
|
+
var HEX_ENCODING = "hex";
|
|
1069
|
+
var RUN_ID_BYTES = 6;
|
|
1070
|
+
var RUN_FILE_CREATE_ATTEMPTS = 10;
|
|
1071
|
+
var RUN_TIMESTAMP_SEPARATOR = "_";
|
|
1072
|
+
var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
|
|
1073
|
+
var SLUG_SEPARATOR = "-";
|
|
1074
|
+
var EMPTY_STRING = "";
|
|
1075
|
+
var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
|
|
1076
|
+
var TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
1077
|
+
var EXCLUSIVE_CREATE_FLAG = "wx";
|
|
1078
|
+
var WRITE_EXISTING_FLAG = "r+";
|
|
1079
|
+
var ERROR_CODE_FILE_EXISTS = "EEXIST";
|
|
1080
|
+
var ERROR_CODE_NOT_FOUND = "ENOENT";
|
|
1081
|
+
var JSONL_LINE_SEPARATOR = "\n";
|
|
1082
|
+
var ERROR_DETAIL_SEPARATOR = ": ";
|
|
1083
|
+
var defaultFileSystem = {
|
|
1084
|
+
mkdir: async (path6, options) => {
|
|
1085
|
+
await nodeMkdir(path6, options);
|
|
1086
|
+
},
|
|
1087
|
+
writeFile: nodeWriteFile,
|
|
1088
|
+
appendFile: nodeAppendFile,
|
|
1089
|
+
readFile: nodeReadFile,
|
|
1090
|
+
readdir: nodeReaddir
|
|
1091
|
+
};
|
|
1092
|
+
function validateScopeToken(token) {
|
|
1093
|
+
return TOKEN_PATTERN.test(token) ? { ok: true, value: token } : { ok: false, error: STATE_STORE_ERROR.INVALID_TOKEN };
|
|
1094
|
+
}
|
|
1095
|
+
async function resolveWorktreeScopeDir(options = {}) {
|
|
1096
|
+
const gitResult = await detectWorktreeProductRoot(options.cwd, options.deps);
|
|
1097
|
+
return worktreeScopeDir(gitResult.productDir);
|
|
1098
|
+
}
|
|
1099
|
+
async function resolveSessionsScopeDir(options = {}) {
|
|
1100
|
+
const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
|
|
1101
|
+
return {
|
|
1102
|
+
sessionsDir: sessionsScopeDir(gitResult.productDir),
|
|
1103
|
+
warning: gitResult.warning
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function worktreeScopeDir(productDir) {
|
|
1107
|
+
return { ok: true, value: join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREE_SCOPE) };
|
|
1108
|
+
}
|
|
1109
|
+
function sessionsScopeDir(productDir) {
|
|
1110
|
+
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
1111
|
+
}
|
|
1112
|
+
function composeScopeDir(baseScopeDir, ...tokens) {
|
|
1113
|
+
const segments = [];
|
|
1114
|
+
for (const token of tokens) {
|
|
1115
|
+
const validated = validateScopeToken(token);
|
|
1116
|
+
if (!validated.ok) return validated;
|
|
1117
|
+
segments.push(validated.value);
|
|
1118
|
+
}
|
|
1119
|
+
return { ok: true, value: join2(baseScopeDir, ...segments) };
|
|
1120
|
+
}
|
|
1121
|
+
function domainDir(scopeDir, domainName) {
|
|
1122
|
+
return composeScopeDir(scopeDir, domainName);
|
|
1123
|
+
}
|
|
1124
|
+
function runsDir(scopeDir, domainName) {
|
|
1125
|
+
const domain = domainDir(scopeDir, domainName);
|
|
1126
|
+
if (!domain.ok) return domain;
|
|
1127
|
+
return { ok: true, value: join2(domain.value, STATE_STORE_PATH.RUNS_DIR) };
|
|
1128
|
+
}
|
|
1129
|
+
function formatRunTimestamp(date) {
|
|
1130
|
+
const year = date.getUTCFullYear();
|
|
1131
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1132
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
1133
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
1134
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
1135
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
1136
|
+
const milliseconds = String(date.getUTCMilliseconds()).padStart(RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
|
|
1137
|
+
return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
|
|
1138
|
+
}
|
|
1139
|
+
function generateRunId(randomBytes = nodeRandomBytes) {
|
|
1140
|
+
return randomBytes(RUN_ID_BYTES).toString(HEX_ENCODING);
|
|
1141
|
+
}
|
|
1142
|
+
function runFileName(runToken) {
|
|
1143
|
+
return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
|
|
1144
|
+
}
|
|
1145
|
+
function isRunFileName(name) {
|
|
1146
|
+
return name.startsWith(STATE_STORE_PATH.RUN_FILE_PREFIX) && name.endsWith(STATE_STORE_PATH.JSONL_EXTENSION) && RUN_TOKEN_PATTERN.test(name.slice(
|
|
1147
|
+
STATE_STORE_PATH.RUN_FILE_PREFIX.length,
|
|
1148
|
+
-STATE_STORE_PATH.JSONL_EXTENSION.length
|
|
1149
|
+
));
|
|
1150
|
+
}
|
|
1151
|
+
async function createJsonlRunFile(scopeDir, domainName, options = {}) {
|
|
1152
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1153
|
+
const domainRunsDir = runsDir(scopeDir, domainName);
|
|
1154
|
+
if (!domainRunsDir.ok) return domainRunsDir;
|
|
1155
|
+
const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
|
|
1156
|
+
const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1157
|
+
const startedAt = formatRunTimestamp(startedDate);
|
|
1158
|
+
const randomBytes = options.randomBytes ?? nodeRandomBytes;
|
|
1159
|
+
try {
|
|
1160
|
+
await fs7.mkdir(domainRunsDir.value, { recursive: true });
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
return {
|
|
1163
|
+
ok: false,
|
|
1164
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1168
|
+
const runId = generateRunId(randomBytes);
|
|
1169
|
+
const runToken = `${startedAt}${SLUG_SEPARATOR}${runId}`;
|
|
1170
|
+
const name = runFileName(runToken);
|
|
1171
|
+
const path6 = join2(domainRunsDir.value, name);
|
|
1172
|
+
try {
|
|
1173
|
+
await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
|
|
1174
|
+
return {
|
|
1175
|
+
ok: true,
|
|
1176
|
+
value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, runToken, runId, startedAt }
|
|
1177
|
+
};
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
|
|
1180
|
+
return {
|
|
1181
|
+
ok: false,
|
|
1182
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return { ok: false, error: STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT };
|
|
1187
|
+
}
|
|
1188
|
+
async function writeJsonlRunRecord(runFilePath, record, options = {}) {
|
|
1189
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1190
|
+
try {
|
|
1191
|
+
const existing = await fs7.readFile(runFilePath, "utf8");
|
|
1192
|
+
if (existing.trim().length > 0) return { ok: false, error: STATE_STORE_ERROR.RECORD_ALREADY_EXISTS };
|
|
768
1193
|
} catch (error) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
throw new Error(`Failed to write settings: ${error.message}`);
|
|
1194
|
+
if (!hasErrorCode(error, ERROR_CODE_NOT_FOUND)) {
|
|
1195
|
+
return {
|
|
1196
|
+
ok: false,
|
|
1197
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
|
|
1198
|
+
};
|
|
775
1199
|
}
|
|
776
|
-
throw error;
|
|
777
1200
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const shouldWrite = options.write || false;
|
|
787
|
-
const outputFile = options.outputFile;
|
|
788
|
-
const previewOnly = !shouldWrite && !outputFile;
|
|
789
|
-
const settingsFiles = await findSettingsFiles(root);
|
|
790
|
-
if (settingsFiles.length === 0) {
|
|
791
|
-
return `No settings files found in ${root}
|
|
792
|
-
|
|
793
|
-
Searched for: **/.claude/settings.local.json`;
|
|
1201
|
+
try {
|
|
1202
|
+
await fs7.writeFile(runFilePath, serializeJsonlRecord(record), { flag: WRITE_EXISTING_FLAG });
|
|
1203
|
+
return { ok: true, value: runFilePath };
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
return {
|
|
1206
|
+
ok: false,
|
|
1207
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
|
|
1208
|
+
};
|
|
794
1209
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1210
|
+
}
|
|
1211
|
+
async function appendJsonlRecord(filePath, record, options = {}) {
|
|
1212
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1213
|
+
try {
|
|
1214
|
+
await fs7.mkdir(dirname2(filePath), { recursive: true });
|
|
1215
|
+
await fs7.appendFile(filePath, serializeJsonlRecord(record));
|
|
1216
|
+
return { ok: true, value: filePath };
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
return {
|
|
1219
|
+
ok: false,
|
|
1220
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
|
|
804
1221
|
};
|
|
805
1222
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1223
|
+
}
|
|
1224
|
+
async function readLatestJsonlRecord(filePath, options = {}) {
|
|
1225
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1226
|
+
let content;
|
|
1227
|
+
try {
|
|
1228
|
+
content = await fs7.readFile(filePath, "utf8");
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
|
|
1231
|
+
return {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_READ_FAILED, toErrorMessage(error))
|
|
811
1234
|
};
|
|
812
1235
|
}
|
|
813
|
-
const
|
|
814
|
-
globalSettings.permissions,
|
|
815
|
-
localPermissions
|
|
816
|
-
);
|
|
817
|
-
if (shouldWrite) {
|
|
1236
|
+
for (const line of nonEmptyJsonlLinesNewestFirst(content)) {
|
|
818
1237
|
try {
|
|
819
|
-
|
|
820
|
-
} catch
|
|
821
|
-
|
|
822
|
-
throw error;
|
|
823
|
-
}
|
|
1238
|
+
return { ok: true, value: JSON.parse(line) };
|
|
1239
|
+
} catch {
|
|
1240
|
+
continue;
|
|
824
1241
|
}
|
|
825
1242
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1243
|
+
return { ok: true, value: void 0 };
|
|
1244
|
+
}
|
|
1245
|
+
function latestNonEmptyJsonlLine(content) {
|
|
1246
|
+
return nonEmptyJsonlLinesNewestFirst(content)[0];
|
|
1247
|
+
}
|
|
1248
|
+
function compareAsciiStrings(left, right) {
|
|
1249
|
+
if (left < right) return -1;
|
|
1250
|
+
if (left > right) return 1;
|
|
1251
|
+
return 0;
|
|
1252
|
+
}
|
|
1253
|
+
function formatStateStoreError(code, detail) {
|
|
1254
|
+
return detail === void 0 ? code : `${code}${ERROR_DETAIL_SEPARATOR}${detail}`;
|
|
1255
|
+
}
|
|
1256
|
+
function parseStateStoreError(error) {
|
|
1257
|
+
for (const code of Object.values(STATE_STORE_ERROR)) {
|
|
1258
|
+
if (error === code) return { code };
|
|
1259
|
+
const prefix = `${code}${ERROR_DETAIL_SEPARATOR}`;
|
|
1260
|
+
if (error.startsWith(prefix)) return { code, detail: error.slice(prefix.length) };
|
|
840
1261
|
}
|
|
841
|
-
return
|
|
1262
|
+
return void 0;
|
|
1263
|
+
}
|
|
1264
|
+
function serializeJsonlRecord(record) {
|
|
1265
|
+
return `${JSON.stringify(record)}${JSONL_LINE_SEPARATOR}`;
|
|
1266
|
+
}
|
|
1267
|
+
function nonEmptyJsonlLinesNewestFirst(content) {
|
|
1268
|
+
const lines = content.split(JSONL_LINE_SEPARATOR);
|
|
1269
|
+
const latestLines = [];
|
|
1270
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1271
|
+
const line = lines[index]?.trim() ?? EMPTY_STRING;
|
|
1272
|
+
if (line.length > 0) latestLines.push(line);
|
|
1273
|
+
}
|
|
1274
|
+
return latestLines;
|
|
1275
|
+
}
|
|
1276
|
+
function hasErrorCode(error, code) {
|
|
1277
|
+
return typeof error === "object" && error !== null && !Array.isArray(error) && "code" in error && error.code === code;
|
|
1278
|
+
}
|
|
1279
|
+
function toErrorMessage(error) {
|
|
1280
|
+
return error instanceof Error ? error.message : String(error);
|
|
842
1281
|
}
|
|
843
1282
|
|
|
844
|
-
// src/
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1283
|
+
// src/domains/compact/index.ts
|
|
1284
|
+
var COMPACT_STORE_PATH = {
|
|
1285
|
+
STASH_FILE: "stash.jsonl"
|
|
1286
|
+
};
|
|
1287
|
+
var COMPACT_RECORD_FIELDS = {
|
|
1288
|
+
ACTIVE_NODE: "active_node",
|
|
1289
|
+
HAS_FOUNDATION: "has_foundation"
|
|
1290
|
+
};
|
|
1291
|
+
var COMPACT_MARKER = {
|
|
1292
|
+
FOUNDATION: "SPEC_TREE_FOUNDATION",
|
|
1293
|
+
CONTEXT: "SPEC_TREE_CONTEXT",
|
|
1294
|
+
TARGET_ATTRIBUTE: "target",
|
|
1295
|
+
ESCAPED_TARGET_QUOTE: '\\"',
|
|
1296
|
+
UNESCAPED_TARGET_QUOTE: '"'
|
|
1297
|
+
};
|
|
1298
|
+
var COMPACT_ERROR = {
|
|
1299
|
+
RECORD_SHAPE_INVALID: "compact record shape invalid"
|
|
1300
|
+
};
|
|
1301
|
+
var EMPTY_ACTIVE_NODE = "";
|
|
1302
|
+
var JSONL_LINE_SEPARATOR2 = "\n";
|
|
1303
|
+
var ATTRIBUTE_ASSIGNMENT = "=";
|
|
1304
|
+
var ESCAPE_CHARACTER = "\\";
|
|
1305
|
+
var CONTEXT_TARGET_PREFIX = `${COMPACT_MARKER.TARGET_ATTRIBUTE}${ATTRIBUTE_ASSIGNMENT}`;
|
|
1306
|
+
var NODE_PATH_PREFIX = "spx/";
|
|
1307
|
+
function resolveCompactSessionToken(sessionId, env) {
|
|
1308
|
+
if (sessionId !== void 0 && sessionId.length > 0) {
|
|
1309
|
+
return normalizeAgentSessionToken(sessionId);
|
|
1310
|
+
}
|
|
1311
|
+
return resolveAgentSessionId(env);
|
|
1312
|
+
}
|
|
1313
|
+
function extractCompactRecord(transcript) {
|
|
1314
|
+
let hasFoundation = false;
|
|
1315
|
+
let activeNode = EMPTY_ACTIVE_NODE;
|
|
1316
|
+
for (const value of transcriptStringValues(transcript)) {
|
|
1317
|
+
if (value.includes(COMPACT_MARKER.FOUNDATION)) hasFoundation = true;
|
|
1318
|
+
activeNode = extractLastContextTarget(value) ?? activeNode;
|
|
1319
|
+
}
|
|
1320
|
+
if (!hasFoundation) return void 0;
|
|
1321
|
+
return {
|
|
1322
|
+
[COMPACT_RECORD_FIELDS.ACTIVE_NODE]: activeNode,
|
|
1323
|
+
[COMPACT_RECORD_FIELDS.HAS_FOUNDATION]: true
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function compactStashPath(worktreeScopeDir2, sessionToken) {
|
|
1327
|
+
const compactScope = composeScopeDir(worktreeScopeDir2, sessionToken, STATE_STORE_DOMAIN.COMPACT);
|
|
1328
|
+
if (!compactScope.ok) return compactScope;
|
|
1329
|
+
return { ok: true, value: join3(compactScope.value, COMPACT_STORE_PATH.STASH_FILE) };
|
|
1330
|
+
}
|
|
1331
|
+
function parseCompactRecord(value) {
|
|
1332
|
+
if (typeof value === "object" && value !== null && COMPACT_RECORD_FIELDS.ACTIVE_NODE in value && COMPACT_RECORD_FIELDS.HAS_FOUNDATION in value && typeof value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] === "string" && value[COMPACT_RECORD_FIELDS.HAS_FOUNDATION] === true) {
|
|
1333
|
+
return {
|
|
1334
|
+
ok: true,
|
|
1335
|
+
value: {
|
|
1336
|
+
active_node: value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] ?? EMPTY_ACTIVE_NODE,
|
|
1337
|
+
has_foundation: true
|
|
892
1338
|
}
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
return { ok: false, error: COMPACT_ERROR.RECORD_SHAPE_INVALID };
|
|
1342
|
+
}
|
|
1343
|
+
function transcriptStringValues(transcript) {
|
|
1344
|
+
const values = [];
|
|
1345
|
+
for (const rawLine of transcript.split(JSONL_LINE_SEPARATOR2)) {
|
|
1346
|
+
const line = rawLine.trim();
|
|
1347
|
+
if (line.length === 0) continue;
|
|
1348
|
+
try {
|
|
1349
|
+
collectStringValues(JSON.parse(line), values);
|
|
1350
|
+
} catch {
|
|
1351
|
+
continue;
|
|
893
1352
|
}
|
|
894
|
-
|
|
1353
|
+
}
|
|
1354
|
+
return values;
|
|
895
1355
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1356
|
+
function collectStringValues(value, values) {
|
|
1357
|
+
if (typeof value === "string") {
|
|
1358
|
+
values.push(value);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (Array.isArray(value)) {
|
|
1362
|
+
for (const entry of value) collectStringValues(entry, values);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof value === "object" && value !== null) {
|
|
1366
|
+
for (const entry of Object.values(value)) collectStringValues(entry, values);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function extractLastContextTarget(value) {
|
|
1370
|
+
let activeNode;
|
|
1371
|
+
let searchFrom = 0;
|
|
1372
|
+
while (searchFrom < value.length) {
|
|
1373
|
+
const contextIndex = value.indexOf(COMPACT_MARKER.CONTEXT, searchFrom);
|
|
1374
|
+
if (contextIndex < 0) return activeNode;
|
|
1375
|
+
const target = extractContextTargetAt(value, contextIndex + COMPACT_MARKER.CONTEXT.length);
|
|
1376
|
+
if (target !== void 0) activeNode = target;
|
|
1377
|
+
searchFrom = contextIndex + COMPACT_MARKER.CONTEXT.length;
|
|
1378
|
+
}
|
|
1379
|
+
return activeNode;
|
|
1380
|
+
}
|
|
1381
|
+
function extractContextTargetAt(value, searchFrom) {
|
|
1382
|
+
const targetIndex = value.indexOf(CONTEXT_TARGET_PREFIX, searchFrom);
|
|
1383
|
+
if (targetIndex < 0) return void 0;
|
|
1384
|
+
const quoteIndex = findOpeningTargetQuote(value, targetIndex + CONTEXT_TARGET_PREFIX.length);
|
|
1385
|
+
if (quoteIndex === void 0) return void 0;
|
|
1386
|
+
const target = readTargetPath(value, quoteIndex + COMPACT_MARKER.UNESCAPED_TARGET_QUOTE.length);
|
|
1387
|
+
return target.startsWith(NODE_PATH_PREFIX) ? target : void 0;
|
|
1388
|
+
}
|
|
1389
|
+
function findOpeningTargetQuote(value, startIndex) {
|
|
1390
|
+
let index = startIndex;
|
|
1391
|
+
while (value[index] === ESCAPE_CHARACTER) index += 1;
|
|
1392
|
+
return value[index] === COMPACT_MARKER.UNESCAPED_TARGET_QUOTE ? index : void 0;
|
|
1393
|
+
}
|
|
1394
|
+
function readTargetPath(value, startIndex) {
|
|
1395
|
+
let endIndex = startIndex;
|
|
1396
|
+
while (endIndex < value.length && isTargetPathCharacter(value[endIndex] ?? EMPTY_ACTIVE_NODE)) {
|
|
1397
|
+
endIndex += 1;
|
|
1398
|
+
}
|
|
1399
|
+
return value.slice(startIndex, endIndex);
|
|
1400
|
+
}
|
|
1401
|
+
function isTargetPathCharacter(character) {
|
|
1402
|
+
const code = character.charCodeAt(0);
|
|
1403
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/";
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/commands/compact/retrieve.ts
|
|
1407
|
+
var EMPTY_OUTPUT = "";
|
|
1408
|
+
async function compactRetrieveCommand(options) {
|
|
1409
|
+
const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
|
|
1410
|
+
if (sessionToken === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1411
|
+
const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
|
|
1412
|
+
if (!worktreeScope.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1413
|
+
const stashPath = compactStashPath(worktreeScope.value, sessionToken);
|
|
1414
|
+
if (!stashPath.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1415
|
+
const latest = await readLatestJsonlRecord(stashPath.value);
|
|
1416
|
+
if (!latest.ok || latest.value === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1417
|
+
const record = parseCompactRecord(latest.value);
|
|
1418
|
+
if (!record.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1419
|
+
return { exitCode: 0, output: `${JSON.stringify(record.value)}
|
|
1420
|
+
` };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/commands/compact/store.ts
|
|
1424
|
+
import { readFile } from "fs/promises";
|
|
1425
|
+
var UTF8_ENCODING = "utf8";
|
|
1426
|
+
async function compactStoreCommand(options) {
|
|
1427
|
+
const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
|
|
1428
|
+
if (sessionToken === void 0) return 1;
|
|
1429
|
+
let transcript;
|
|
1430
|
+
try {
|
|
1431
|
+
transcript = await readFile(options.transcript, UTF8_ENCODING);
|
|
1432
|
+
} catch {
|
|
1433
|
+
return 1;
|
|
1434
|
+
}
|
|
1435
|
+
const record = extractCompactRecord(transcript);
|
|
1436
|
+
if (record === void 0) return 0;
|
|
1437
|
+
const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
|
|
1438
|
+
if (!worktreeScope.ok) return 1;
|
|
1439
|
+
const stashPath = compactStashPath(worktreeScope.value, sessionToken);
|
|
1440
|
+
if (!stashPath.ok) return 1;
|
|
1441
|
+
const written = await appendJsonlRecord(stashPath.value, record);
|
|
1442
|
+
return written.ok ? 0 : 1;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// src/interfaces/cli/compact.ts
|
|
1446
|
+
var COMPACT_CLI = {
|
|
1447
|
+
commandName: "compact",
|
|
1448
|
+
storeCommandName: "store",
|
|
1449
|
+
retrieveCommandName: "retrieve",
|
|
1450
|
+
description: "Store and retrieve compact resume state",
|
|
1451
|
+
transcriptFlag: "--transcript",
|
|
1452
|
+
transcriptOption: "--transcript <path>",
|
|
1453
|
+
sessionIdFlag: "--session-id",
|
|
1454
|
+
sessionIdOption: "--session-id <id>",
|
|
1455
|
+
sessionIdDescription: "Agent session identity (overrides the agent-session environment)"
|
|
1456
|
+
};
|
|
1457
|
+
var compactDomain = {
|
|
1458
|
+
name: COMPACT_CLI.commandName,
|
|
1459
|
+
description: COMPACT_CLI.description,
|
|
899
1460
|
register: (program2) => {
|
|
900
|
-
const
|
|
901
|
-
|
|
1461
|
+
const compactCmd = program2.command(COMPACT_CLI.commandName).description(COMPACT_CLI.description);
|
|
1462
|
+
compactCmd.command(COMPACT_CLI.storeCommandName).description("Store compact resume state").requiredOption(COMPACT_CLI.transcriptOption, "Transcript JSONL path").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
|
|
1463
|
+
process.exit(
|
|
1464
|
+
await compactStoreCommand({
|
|
1465
|
+
transcript: options.transcript,
|
|
1466
|
+
sessionId: options.sessionId,
|
|
1467
|
+
cwd: process.cwd(),
|
|
1468
|
+
env: process.env
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
});
|
|
1472
|
+
compactCmd.command(COMPACT_CLI.retrieveCommandName).description("Retrieve compact resume state").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
|
|
1473
|
+
const result = await compactRetrieveCommand({
|
|
1474
|
+
sessionId: options.sessionId,
|
|
1475
|
+
cwd: process.cwd(),
|
|
1476
|
+
env: process.env
|
|
1477
|
+
});
|
|
1478
|
+
if (result.output.length > 0) {
|
|
1479
|
+
process.stdout.write(result.output);
|
|
1480
|
+
}
|
|
1481
|
+
process.exitCode = result.exitCode;
|
|
1482
|
+
});
|
|
902
1483
|
}
|
|
903
1484
|
};
|
|
904
1485
|
|
|
905
1486
|
// src/config/index.ts
|
|
906
1487
|
import { readFile as readFile2 } from "fs/promises";
|
|
907
|
-
import { join as
|
|
1488
|
+
import { join as join5 } from "path";
|
|
908
1489
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
909
1490
|
import { parse as parseYaml, parseDocument as parseYamlDocument, stringify as stringifyYaml } from "yaml";
|
|
910
1491
|
|
|
@@ -1415,57 +1996,20 @@ function applyPathFilter(paths, config) {
|
|
|
1415
1996
|
// src/domains/audit/config.ts
|
|
1416
1997
|
var AUDIT_SECTION = "audit";
|
|
1417
1998
|
var AUDIT_CONFIG_FIELDS = {
|
|
1418
|
-
STORAGE: "storage",
|
|
1419
|
-
SPX_DIR: "spxDir",
|
|
1420
|
-
NODES_DIR: "nodesDir",
|
|
1421
|
-
AUDIT_DIR: "auditDir",
|
|
1422
|
-
RUNS_DIR: "runsDir",
|
|
1423
|
-
VERDICT_FILE: "verdictFile",
|
|
1424
|
-
VERDICT_FILE_SUFFIX: "verdictFileSuffix",
|
|
1425
|
-
STATE_FILE: "stateFile",
|
|
1426
1999
|
BASE_REF: "baseRef",
|
|
1427
|
-
BRANCH_SLUG: "branchSlug",
|
|
1428
|
-
MAX_BYTES: "maxBytes",
|
|
1429
2000
|
AUDITORS: "auditors",
|
|
1430
2001
|
TARGETS: "targets"
|
|
1431
2002
|
};
|
|
1432
|
-
var AUDIT_BRANCH_SLUG_MIN_MAX_BYTES = 10;
|
|
1433
2003
|
var DEFAULT_AUDIT_CONFIG = {
|
|
1434
|
-
storage: {
|
|
1435
|
-
spxDir: ".spx",
|
|
1436
|
-
nodesDir: "nodes",
|
|
1437
|
-
auditDir: "audit",
|
|
1438
|
-
runsDir: "runs",
|
|
1439
|
-
verdictFile: "verdict.audit.xml",
|
|
1440
|
-
verdictFileSuffix: ".audit.xml",
|
|
1441
|
-
stateFile: "state.json"
|
|
1442
|
-
},
|
|
1443
2004
|
baseRef: "main",
|
|
1444
|
-
branchSlug: {
|
|
1445
|
-
maxBytes: 120
|
|
1446
|
-
},
|
|
1447
2005
|
auditors: [],
|
|
1448
2006
|
targets: resolveDefaultTargets()
|
|
1449
2007
|
};
|
|
1450
2008
|
var AUDIT_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
1451
|
-
AUDIT_CONFIG_FIELDS.STORAGE,
|
|
1452
2009
|
AUDIT_CONFIG_FIELDS.BASE_REF,
|
|
1453
|
-
AUDIT_CONFIG_FIELDS.BRANCH_SLUG,
|
|
1454
2010
|
AUDIT_CONFIG_FIELDS.AUDITORS,
|
|
1455
2011
|
AUDIT_CONFIG_FIELDS.TARGETS
|
|
1456
2012
|
]);
|
|
1457
|
-
var AUDIT_STORAGE_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
1458
|
-
AUDIT_CONFIG_FIELDS.SPX_DIR,
|
|
1459
|
-
AUDIT_CONFIG_FIELDS.NODES_DIR,
|
|
1460
|
-
AUDIT_CONFIG_FIELDS.AUDIT_DIR,
|
|
1461
|
-
AUDIT_CONFIG_FIELDS.RUNS_DIR,
|
|
1462
|
-
AUDIT_CONFIG_FIELDS.VERDICT_FILE,
|
|
1463
|
-
AUDIT_CONFIG_FIELDS.VERDICT_FILE_SUFFIX,
|
|
1464
|
-
AUDIT_CONFIG_FIELDS.STATE_FILE
|
|
1465
|
-
]);
|
|
1466
|
-
var AUDIT_BRANCH_SLUG_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
1467
|
-
AUDIT_CONFIG_FIELDS.MAX_BYTES
|
|
1468
|
-
]);
|
|
1469
2013
|
var auditConfigDescriptor = {
|
|
1470
2014
|
section: AUDIT_SECTION,
|
|
1471
2015
|
defaults: DEFAULT_AUDIT_CONFIG,
|
|
@@ -1485,15 +2029,11 @@ function validateAuditConfig(raw) {
|
|
|
1485
2029
|
}
|
|
1486
2030
|
const unknown = rejectUnknownFields2(AUDIT_SECTION, raw, AUDIT_ALLOWED_FIELDS);
|
|
1487
2031
|
if (!unknown.ok) return unknown;
|
|
1488
|
-
const storage = raw[AUDIT_CONFIG_FIELDS.STORAGE] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.storage } : validateStorage(raw[AUDIT_CONFIG_FIELDS.STORAGE]);
|
|
1489
|
-
if (!storage.ok) return storage;
|
|
1490
2032
|
const baseRef = raw[AUDIT_CONFIG_FIELDS.BASE_REF] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.baseRef } : validateNonEmptyString2(
|
|
1491
2033
|
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BASE_REF}`,
|
|
1492
2034
|
raw[AUDIT_CONFIG_FIELDS.BASE_REF]
|
|
1493
2035
|
);
|
|
1494
2036
|
if (!baseRef.ok) return baseRef;
|
|
1495
|
-
const branchSlug = raw[AUDIT_CONFIG_FIELDS.BRANCH_SLUG] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.branchSlug } : validateBranchSlug(raw[AUDIT_CONFIG_FIELDS.BRANCH_SLUG]);
|
|
1496
|
-
if (!branchSlug.ok) return branchSlug;
|
|
1497
2037
|
const auditors = raw[AUDIT_CONFIG_FIELDS.AUDITORS] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.auditors } : validateArrayOfNonEmptyStrings(
|
|
1498
2038
|
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.AUDITORS}`,
|
|
1499
2039
|
raw[AUDIT_CONFIG_FIELDS.AUDITORS]
|
|
@@ -1507,61 +2047,12 @@ function validateAuditConfig(raw) {
|
|
|
1507
2047
|
return {
|
|
1508
2048
|
ok: true,
|
|
1509
2049
|
value: {
|
|
1510
|
-
storage: storage.value,
|
|
1511
2050
|
baseRef: baseRef.value,
|
|
1512
|
-
branchSlug: branchSlug.value,
|
|
1513
2051
|
auditors: auditors.value,
|
|
1514
2052
|
targets: targets.value
|
|
1515
2053
|
}
|
|
1516
2054
|
};
|
|
1517
2055
|
}
|
|
1518
|
-
function validateStorage(raw) {
|
|
1519
|
-
if (!isRecord2(raw)) {
|
|
1520
|
-
return { ok: false, error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE} must be an object` };
|
|
1521
|
-
}
|
|
1522
|
-
const unknown = rejectUnknownFields2(
|
|
1523
|
-
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE}`,
|
|
1524
|
-
raw,
|
|
1525
|
-
AUDIT_STORAGE_ALLOWED_FIELDS
|
|
1526
|
-
);
|
|
1527
|
-
if (!unknown.ok) return unknown;
|
|
1528
|
-
return validateStringRecord(
|
|
1529
|
-
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE}`,
|
|
1530
|
-
DEFAULT_AUDIT_CONFIG.storage,
|
|
1531
|
-
raw
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1534
|
-
function validateBranchSlug(raw) {
|
|
1535
|
-
if (!isRecord2(raw)) {
|
|
1536
|
-
return { ok: false, error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG} must be an object` };
|
|
1537
|
-
}
|
|
1538
|
-
const unknown = rejectUnknownFields2(
|
|
1539
|
-
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG}`,
|
|
1540
|
-
raw,
|
|
1541
|
-
AUDIT_BRANCH_SLUG_ALLOWED_FIELDS
|
|
1542
|
-
);
|
|
1543
|
-
if (!unknown.ok) return unknown;
|
|
1544
|
-
const maxBytesRaw = raw[AUDIT_CONFIG_FIELDS.MAX_BYTES];
|
|
1545
|
-
if (maxBytesRaw === void 0) return { ok: true, value: DEFAULT_AUDIT_CONFIG.branchSlug };
|
|
1546
|
-
if (typeof maxBytesRaw !== "number" || !Number.isInteger(maxBytesRaw) || maxBytesRaw < AUDIT_BRANCH_SLUG_MIN_MAX_BYTES) {
|
|
1547
|
-
return {
|
|
1548
|
-
ok: false,
|
|
1549
|
-
error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG}.${AUDIT_CONFIG_FIELDS.MAX_BYTES} must be an integer greater than or equal to ${AUDIT_BRANCH_SLUG_MIN_MAX_BYTES}`
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
return { ok: true, value: { maxBytes: maxBytesRaw } };
|
|
1553
|
-
}
|
|
1554
|
-
function validateStringRecord(path6, defaults4, raw) {
|
|
1555
|
-
const next = { ...defaults4 };
|
|
1556
|
-
for (const field of Object.keys(defaults4)) {
|
|
1557
|
-
const value = raw[field];
|
|
1558
|
-
if (value === void 0) continue;
|
|
1559
|
-
const validated = validateNonEmptyString2(`${path6}.${field}`, value);
|
|
1560
|
-
if (!validated.ok) return validated;
|
|
1561
|
-
next[field] = validated.value;
|
|
1562
|
-
}
|
|
1563
|
-
return { ok: true, value: next };
|
|
1564
|
-
}
|
|
1565
2056
|
function validateNonEmptyString2(path6, value) {
|
|
1566
2057
|
if (typeof value !== "string" || value.length === 0) {
|
|
1567
2058
|
return { ok: false, error: `${path6} must be a non-empty string` };
|
|
@@ -1645,6 +2136,112 @@ var DECISION_KINDS = Object.keys(KIND_REGISTRY).filter(
|
|
|
1645
2136
|
);
|
|
1646
2137
|
var NODE_SUFFIXES = NODE_KINDS.map((k) => KIND_REGISTRY[k].suffix);
|
|
1647
2138
|
var DECISION_SUFFIXES = DECISION_KINDS.map((k) => KIND_REGISTRY[k].suffix);
|
|
2139
|
+
var SPEC_TREE_GRAMMAR = {
|
|
2140
|
+
PRODUCT_SUFFIX: SPEC_TREE_CONFIG.PRODUCT.SUFFIX,
|
|
2141
|
+
EVIDENCE: {
|
|
2142
|
+
DIRECTORY_NAME: "tests",
|
|
2143
|
+
MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
|
|
2144
|
+
LEVELS: ["l1", "l2", "l3"],
|
|
2145
|
+
TAILS: {
|
|
2146
|
+
TYPESCRIPT: ["test", "ts"],
|
|
2147
|
+
PYTHON: ["py"],
|
|
2148
|
+
RUST: ["rs"]
|
|
2149
|
+
},
|
|
2150
|
+
SEGMENT_SEPARATOR: "."
|
|
2151
|
+
},
|
|
2152
|
+
RUNNERS: ["vitest", "playwright", "subprocess"],
|
|
2153
|
+
ORDER: {
|
|
2154
|
+
SEPARATOR: "-",
|
|
2155
|
+
PATTERN: /^\d+$/
|
|
2156
|
+
},
|
|
2157
|
+
PATH_SEPARATOR: "/",
|
|
2158
|
+
COORDINATION_NOTES: ["PLAN.md", "ISSUES.md"],
|
|
2159
|
+
EVAL_LANE: ["eval.toml", "cases.jsonl", "prompt.md", "history.jsonl", "runs"],
|
|
2160
|
+
PRIOR_NODE_SUFFIXES: [".capability", ".feature", ".story"]
|
|
2161
|
+
};
|
|
2162
|
+
var SPEC_TREE_EVIDENCE_FILE = SPEC_TREE_GRAMMAR.EVIDENCE;
|
|
2163
|
+
var NAMING_SCHEMA_VERSION_ID = {
|
|
2164
|
+
PRIOR: "1.0.0",
|
|
2165
|
+
CANONICAL: "2.0.0"
|
|
2166
|
+
};
|
|
2167
|
+
function namingSchemaVersionFromNodeSuffixes(version2, nodeSuffixes) {
|
|
2168
|
+
return {
|
|
2169
|
+
version: version2,
|
|
2170
|
+
nodeSuffixes,
|
|
2171
|
+
decisionSuffixes: DECISION_SUFFIXES,
|
|
2172
|
+
productSuffix: SPEC_TREE_GRAMMAR.PRODUCT_SUFFIX,
|
|
2173
|
+
evidence: SPEC_TREE_GRAMMAR.EVIDENCE,
|
|
2174
|
+
runners: SPEC_TREE_GRAMMAR.RUNNERS,
|
|
2175
|
+
order: SPEC_TREE_GRAMMAR.ORDER,
|
|
2176
|
+
pathSeparator: SPEC_TREE_GRAMMAR.PATH_SEPARATOR,
|
|
2177
|
+
coordinationNotes: SPEC_TREE_GRAMMAR.COORDINATION_NOTES,
|
|
2178
|
+
evalLane: SPEC_TREE_GRAMMAR.EVAL_LANE
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
var SPEC_TREE_NAMING_SCHEMA_VERSIONS = [
|
|
2182
|
+
namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.PRIOR, SPEC_TREE_GRAMMAR.PRIOR_NODE_SUFFIXES),
|
|
2183
|
+
namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.CANONICAL, NODE_SUFFIXES)
|
|
2184
|
+
];
|
|
2185
|
+
var VERSION_COMPONENT_SEPARATOR = ".";
|
|
2186
|
+
var VERSION_COMPONENT_RADIX = 10;
|
|
2187
|
+
var VERSION_MISSING_COMPONENT = 0;
|
|
2188
|
+
var VERSION_ORDER_EQUAL = 0;
|
|
2189
|
+
var VERSION_NUMERIC_COMPONENT = /^\d+$/;
|
|
2190
|
+
function parseVersionComponents(version2) {
|
|
2191
|
+
return version2.split(VERSION_COMPONENT_SEPARATOR).map((part) => {
|
|
2192
|
+
if (!VERSION_NUMERIC_COMPONENT.test(part)) {
|
|
2193
|
+
throw new Error(
|
|
2194
|
+
`Naming-schema version "${version2}" must use numeric dotted components; "${part}" is not numeric`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
return Number.parseInt(part, VERSION_COMPONENT_RADIX);
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
function compareNumericVersionIdentifiers(left, right) {
|
|
2201
|
+
const leftComponents = parseVersionComponents(left);
|
|
2202
|
+
const rightComponents = parseVersionComponents(right);
|
|
2203
|
+
const length = Math.max(leftComponents.length, rightComponents.length);
|
|
2204
|
+
for (let index = 0; index < length; index += 1) {
|
|
2205
|
+
const difference = (leftComponents[index] ?? VERSION_MISSING_COMPONENT) - (rightComponents[index] ?? VERSION_MISSING_COMPONENT);
|
|
2206
|
+
if (difference !== VERSION_ORDER_EQUAL) {
|
|
2207
|
+
return difference;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return VERSION_ORDER_EQUAL;
|
|
2211
|
+
}
|
|
2212
|
+
function compareNamingSchemaVersions(left, right) {
|
|
2213
|
+
return compareNumericVersionIdentifiers(left.version, right.version);
|
|
2214
|
+
}
|
|
2215
|
+
function canonicalNamingSchemaVersion(versions) {
|
|
2216
|
+
const [first, ...rest] = versions;
|
|
2217
|
+
if (first === void 0) {
|
|
2218
|
+
throw new Error("Naming-schema version tuple must declare at least one version");
|
|
2219
|
+
}
|
|
2220
|
+
return rest.reduce(
|
|
2221
|
+
(max, version2) => compareNamingSchemaVersions(version2, max) > VERSION_ORDER_EQUAL ? version2 : max,
|
|
2222
|
+
first
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
function supersededNodeSuffixes(versions) {
|
|
2226
|
+
const canonical = canonicalNamingSchemaVersion(versions);
|
|
2227
|
+
const canonicalSuffixes = new Set(canonical.nodeSuffixes);
|
|
2228
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
2229
|
+
for (const version2 of versions) {
|
|
2230
|
+
if (version2 === canonical) {
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
for (const suffix of version2.nodeSuffixes) {
|
|
2234
|
+
if (!canonicalSuffixes.has(suffix)) {
|
|
2235
|
+
superseded.add(suffix);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return [...superseded];
|
|
2240
|
+
}
|
|
2241
|
+
var SPEC_TREE_NAMING_VERSION = canonicalNamingSchemaVersion(SPEC_TREE_NAMING_SCHEMA_VERSIONS).version;
|
|
2242
|
+
var SPEC_TREE_SUPERSEDED_NODE_SUFFIXES = supersededNodeSuffixes(
|
|
2243
|
+
SPEC_TREE_NAMING_SCHEMA_VERSIONS
|
|
2244
|
+
);
|
|
1648
2245
|
var SPEC_TREE_NODE_STATE = {
|
|
1649
2246
|
DECLARED: "declared",
|
|
1650
2247
|
SPECIFIED: "specified",
|
|
@@ -1822,7 +2419,7 @@ var REGISTERED_TOOL_NAMES = Object.keys(ADAPTER_MAP);
|
|
|
1822
2419
|
|
|
1823
2420
|
// src/lib/file-inclusion/ignore-source.ts
|
|
1824
2421
|
import { readFileSync } from "fs";
|
|
1825
|
-
import { join } from "path";
|
|
2422
|
+
import { join as join4 } from "path";
|
|
1826
2423
|
var IGNORE_SOURCE_FILENAME_DEFAULT = "EXCLUDE";
|
|
1827
2424
|
var EMPTY_IGNORE_READER = {
|
|
1828
2425
|
isUnderIgnoreSource() {
|
|
@@ -1856,7 +2453,7 @@ function validateEntry(entry, lineNumber) {
|
|
|
1856
2453
|
}
|
|
1857
2454
|
function createIgnoreSourceReader(projectRoot, config) {
|
|
1858
2455
|
const { ignoreSourceFilename, specTreeRootSegment } = config;
|
|
1859
|
-
const filePath =
|
|
2456
|
+
const filePath = join4(projectRoot, specTreeRootSegment, ignoreSourceFilename);
|
|
1860
2457
|
let content;
|
|
1861
2458
|
try {
|
|
1862
2459
|
content = readFileSync(filePath, "utf8");
|
|
@@ -1919,8 +2516,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
|
|
|
1919
2516
|
var LAYER2 = HIDDEN_PREFIX_LAYER;
|
|
1920
2517
|
function hiddenPrefixPredicate(path6, config) {
|
|
1921
2518
|
const segments = path6.split("/");
|
|
1922
|
-
const
|
|
1923
|
-
const matched =
|
|
2519
|
+
const basename4 = segments[segments.length - 1] ?? "";
|
|
2520
|
+
const matched = basename4.startsWith(config.hiddenPrefix);
|
|
1924
2521
|
return { matched, layer: LAYER2 };
|
|
1925
2522
|
}
|
|
1926
2523
|
|
|
@@ -2319,7 +2916,7 @@ var defaults3 = {
|
|
|
2319
2916
|
enabled: false
|
|
2320
2917
|
}
|
|
2321
2918
|
};
|
|
2322
|
-
function
|
|
2919
|
+
function validatePaths(raw) {
|
|
2323
2920
|
const basePath = `${VALIDATION_SECTION}.${VALIDATION_PATHS_SUBSECTION}`;
|
|
2324
2921
|
const baseResult = validatePathFilterConfig(raw, basePath);
|
|
2325
2922
|
if (!baseResult.ok) return baseResult;
|
|
@@ -2386,7 +2983,7 @@ function validate7(value) {
|
|
|
2386
2983
|
}
|
|
2387
2984
|
const candidate = value;
|
|
2388
2985
|
const pathsRaw = candidate[VALIDATION_PATHS_SUBSECTION] ?? {};
|
|
2389
|
-
const pathsResult =
|
|
2986
|
+
const pathsResult = validatePaths(pathsRaw);
|
|
2390
2987
|
if (!pathsResult.ok) return pathsResult;
|
|
2391
2988
|
const literalRaw = candidate[VALIDATION_LITERAL_SUBSECTION] ?? {};
|
|
2392
2989
|
const literalResult = validateLiteral(literalRaw);
|
|
@@ -2414,11 +3011,11 @@ var productionRegistry = [
|
|
|
2414
3011
|
];
|
|
2415
3012
|
|
|
2416
3013
|
// src/config/descriptor-digest.ts
|
|
2417
|
-
import { createHash } from "crypto";
|
|
3014
|
+
import { createHash as createHash3 } from "crypto";
|
|
2418
3015
|
var DEFAULT_DESCRIPTOR_PATH = "descriptor section";
|
|
2419
3016
|
var SHA256_ALGORITHM = "sha256";
|
|
2420
|
-
var
|
|
2421
|
-
var
|
|
3017
|
+
var UTF8_ENCODING2 = "utf8";
|
|
3018
|
+
var HEX_ENCODING2 = "hex";
|
|
2422
3019
|
function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
|
|
2423
3020
|
const normalized = normalizeDescriptorJsonValue(value, path6, /* @__PURE__ */ new WeakSet());
|
|
2424
3021
|
if (!normalized.ok) return normalized;
|
|
@@ -2427,7 +3024,7 @@ function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
|
|
|
2427
3024
|
function digestDescriptorSection(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
|
|
2428
3025
|
const canonical = canonicalDescriptorJson(value, path6);
|
|
2429
3026
|
if (!canonical.ok) return canonical;
|
|
2430
|
-
const sha256 =
|
|
3027
|
+
const sha256 = createHash3(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING2)).digest(HEX_ENCODING2);
|
|
2431
3028
|
return {
|
|
2432
3029
|
ok: true,
|
|
2433
3030
|
value: {
|
|
@@ -2569,7 +3166,7 @@ async function readProductConfigFile(productDir) {
|
|
|
2569
3166
|
const detected = [];
|
|
2570
3167
|
for (const format2 of CONFIG_FILE_FORMAT_ORDER) {
|
|
2571
3168
|
const filename = CONFIG_FILE_DEFINITIONS[format2].filename;
|
|
2572
|
-
const path6 =
|
|
3169
|
+
const path6 = join5(productDir, filename);
|
|
2573
3170
|
let raw;
|
|
2574
3171
|
try {
|
|
2575
3172
|
raw = await readFile2(path6, "utf8");
|
|
@@ -2596,7 +3193,7 @@ function configFileForFormat(productDir, format2 = DEFAULT_CONFIG_FILE_FORMAT, r
|
|
|
2596
3193
|
return {
|
|
2597
3194
|
filename,
|
|
2598
3195
|
format: format2,
|
|
2599
|
-
path:
|
|
3196
|
+
path: join5(productDir, filename),
|
|
2600
3197
|
raw
|
|
2601
3198
|
};
|
|
2602
3199
|
}
|
|
@@ -2893,7 +3490,7 @@ var configDomain = {
|
|
|
2893
3490
|
|
|
2894
3491
|
// src/commands/session/archive.ts
|
|
2895
3492
|
import { mkdir, rename, stat } from "fs/promises";
|
|
2896
|
-
import { dirname as
|
|
3493
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
2897
3494
|
|
|
2898
3495
|
// src/domains/session/archive.ts
|
|
2899
3496
|
var SESSION_FILE_EXTENSION = ".md";
|
|
@@ -2974,24 +3571,24 @@ var HANDOFF_BASE_PREREQUISITE_LABEL = {
|
|
|
2974
3571
|
DETACHED_AT_DEFAULT_TIP: "HEAD is detached at the default-branch tip"
|
|
2975
3572
|
};
|
|
2976
3573
|
var HANDOFF_BASE_REMEDY = {
|
|
2977
|
-
/** Unclean working tree: commit, or hand off from the
|
|
2978
|
-
|
|
2979
|
-
/** Off the default-branch tip: detach to it, or hand off from the
|
|
2980
|
-
|
|
2981
|
-
/** Default branch unresolved: only the
|
|
2982
|
-
|
|
3574
|
+
/** Unclean working tree: commit, or hand off from the main checkout. */
|
|
3575
|
+
COMMIT_OR_MAIN_CHECKOUT: "commit the changes, or run handoff from the main checkout",
|
|
3576
|
+
/** Off the default-branch tip: detach to it, or hand off from the main checkout. */
|
|
3577
|
+
DETACH_TO_TIP_OR_MAIN_CHECKOUT: "detach HEAD to the default-branch tip, or run handoff from the main checkout",
|
|
3578
|
+
/** Default branch unresolved: only the main checkout can anchor the base. */
|
|
3579
|
+
MAIN_CHECKOUT_ONLY: "run handoff from the main checkout"
|
|
2983
3580
|
};
|
|
2984
3581
|
var HANDOFF_BASE_FACT_LABEL = {
|
|
2985
3582
|
DEFAULT_BRANCH: "default branch",
|
|
2986
3583
|
DEFAULT_TIP: "origin tip",
|
|
2987
3584
|
HEAD: "HEAD",
|
|
2988
3585
|
CURRENT_WORKTREE: "current worktree",
|
|
2989
|
-
|
|
3586
|
+
MAIN_CHECKOUT: "main checkout"
|
|
2990
3587
|
};
|
|
2991
3588
|
var HANDOFF_BASE_UNRESOLVED = "unresolved";
|
|
2992
3589
|
var CHECKLIST_INDENT = " ";
|
|
2993
3590
|
var REMEDY_SEPARATOR = " \u2014 ";
|
|
2994
|
-
var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this
|
|
3591
|
+
var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this worktree \u2014 it is not the main checkout.`;
|
|
2995
3592
|
function renderFactLine(label, value) {
|
|
2996
3593
|
return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
|
|
2997
3594
|
}
|
|
@@ -3007,7 +3604,7 @@ function renderHandoffBaseChecklist(checklist) {
|
|
|
3007
3604
|
renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
|
|
3008
3605
|
renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
|
|
3009
3606
|
renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
|
|
3010
|
-
renderFactLine(HANDOFF_BASE_FACT_LABEL.
|
|
3607
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.MAIN_CHECKOUT, checklist.mainCheckoutPath),
|
|
3011
3608
|
...checklist.prerequisites.map(renderPrerequisiteLine)
|
|
3012
3609
|
].join("\n");
|
|
3013
3610
|
}
|
|
@@ -3049,6 +3646,18 @@ var SessionInvalidGoalError = class extends SessionError {
|
|
|
3049
3646
|
this.name = "SessionInvalidGoalError";
|
|
3050
3647
|
}
|
|
3051
3648
|
};
|
|
3649
|
+
var SessionInvalidFieldError = class extends SessionError {
|
|
3650
|
+
/** The unknown field token the caller supplied. */
|
|
3651
|
+
field;
|
|
3652
|
+
/** The full set of valid field names. */
|
|
3653
|
+
validFields;
|
|
3654
|
+
constructor(field, validFields) {
|
|
3655
|
+
super(`Invalid field: "${field}". Valid fields: ${validFields.join(", ")}`);
|
|
3656
|
+
this.name = "SessionInvalidFieldError";
|
|
3657
|
+
this.field = field;
|
|
3658
|
+
this.validFields = validFields;
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3052
3661
|
var SessionInvalidNextStepError = class extends SessionError {
|
|
3053
3662
|
constructor() {
|
|
3054
3663
|
super("Invalid session next_step: next_step must be a non-empty string.");
|
|
@@ -3062,7 +3671,7 @@ var SessionHandoffBaseError = class extends SessionError {
|
|
|
3062
3671
|
silent;
|
|
3063
3672
|
constructor(options = {}) {
|
|
3064
3673
|
super(
|
|
3065
|
-
"Cannot create a handoff session from this git work context. Run handoff from the
|
|
3674
|
+
"Cannot create a handoff session from this git work context. Run handoff from the main checkout, or from a non-main checkout with a clean working tree detached at the tip of the default branch."
|
|
3066
3675
|
);
|
|
3067
3676
|
this.name = SESSION_HANDOFF_BASE_ERROR_NAME;
|
|
3068
3677
|
this.checklist = options.checklist ?? null;
|
|
@@ -3091,7 +3700,18 @@ var SessionLegacyFrontmatterInputError = class extends SessionError {
|
|
|
3091
3700
|
|
|
3092
3701
|
printf '%s\\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' | spx session handoff`
|
|
3093
3702
|
);
|
|
3094
|
-
this.name = "SessionLegacyFrontmatterInputError";
|
|
3703
|
+
this.name = "SessionLegacyFrontmatterInputError";
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
var SessionWorkBranchNotOnOriginError = class extends SessionError {
|
|
3707
|
+
/** The work-branch ref the caller supplied that does not resolve on `origin`. */
|
|
3708
|
+
workBranch;
|
|
3709
|
+
constructor(workBranch) {
|
|
3710
|
+
super(
|
|
3711
|
+
`Work-branch ref not found on origin: ${workBranch}. Push the branch to origin before recording it as the handoff base, or omit git_ref to record the git context.`
|
|
3712
|
+
);
|
|
3713
|
+
this.name = "SessionWorkBranchNotOnOriginError";
|
|
3714
|
+
this.workBranch = workBranch;
|
|
3095
3715
|
}
|
|
3096
3716
|
};
|
|
3097
3717
|
var SessionInvalidJsonHeaderError = class extends SessionError {
|
|
@@ -3101,14 +3721,12 @@ var SessionInvalidJsonHeaderError = class extends SessionError {
|
|
|
3101
3721
|
}
|
|
3102
3722
|
};
|
|
3103
3723
|
|
|
3104
|
-
// src/
|
|
3105
|
-
import {
|
|
3106
|
-
import { dirname, isAbsolute, join as join3, relative as relative2, resolve as resolve3 } from "path";
|
|
3724
|
+
// src/commands/session/resolve-config.ts
|
|
3725
|
+
import { join as join6 } from "path";
|
|
3107
3726
|
|
|
3108
3727
|
// src/config/defaults.ts
|
|
3109
3728
|
var DEFAULT_CONFIG = {
|
|
3110
3729
|
sessions: {
|
|
3111
|
-
dir: ".spx/sessions",
|
|
3112
3730
|
statusDirs: {
|
|
3113
3731
|
todo: "todo",
|
|
3114
3732
|
doing: "doing",
|
|
@@ -3117,228 +3735,29 @@ var DEFAULT_CONFIG = {
|
|
|
3117
3735
|
}
|
|
3118
3736
|
};
|
|
3119
3737
|
|
|
3120
|
-
// src/
|
|
3121
|
-
function withoutGitEnvironment(env) {
|
|
3122
|
-
const cleaned = { ...env };
|
|
3123
|
-
for (const key of Object.keys(cleaned)) {
|
|
3124
|
-
if (key.startsWith("GIT_")) {
|
|
3125
|
-
delete cleaned[key];
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
return cleaned;
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3131
|
-
// src/git/root.ts
|
|
3132
|
-
var defaultDeps = {
|
|
3133
|
-
execa: async (command, args, options) => {
|
|
3134
|
-
const result = await execa2(command, args, {
|
|
3135
|
-
...options,
|
|
3136
|
-
env: withoutGitEnvironment(process.env),
|
|
3137
|
-
extendEnv: false
|
|
3138
|
-
});
|
|
3139
|
-
return {
|
|
3140
|
-
exitCode: result.exitCode ?? 0,
|
|
3141
|
-
stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
|
|
3142
|
-
stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
|
|
3143
|
-
};
|
|
3144
|
-
}
|
|
3145
|
-
};
|
|
3146
|
-
var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
|
|
3147
|
-
var GIT_ROOT_COMMAND = {
|
|
3148
|
-
EXECUTABLE: "git",
|
|
3149
|
-
REV_PARSE: "rev-parse",
|
|
3150
|
-
ABBREV_REF: "--abbrev-ref",
|
|
3151
|
-
GIT_COMMON_DIR: "--git-common-dir",
|
|
3152
|
-
HEAD: "HEAD",
|
|
3153
|
-
SHOW_TOPLEVEL: "--show-toplevel",
|
|
3154
|
-
SYMBOLIC_REF: "symbolic-ref",
|
|
3155
|
-
SHORT: "--short",
|
|
3156
|
-
ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
|
|
3157
|
-
STATUS: "status",
|
|
3158
|
-
PORCELAIN: "--porcelain",
|
|
3159
|
-
PATH_FORMAT_ABSOLUTE: "--path-format=absolute"
|
|
3160
|
-
};
|
|
3161
|
-
var ORIGIN_REF_PREFIX = "origin/";
|
|
3162
|
-
var GIT_SHOW_TOPLEVEL_ARGS = [
|
|
3163
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3164
|
-
GIT_ROOT_COMMAND.SHOW_TOPLEVEL
|
|
3165
|
-
];
|
|
3166
|
-
var GIT_COMMON_DIR_ARGS = [
|
|
3167
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3168
|
-
GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
|
|
3169
|
-
GIT_ROOT_COMMAND.GIT_COMMON_DIR
|
|
3170
|
-
];
|
|
3171
|
-
var GIT_CURRENT_BRANCH_ARGS = [
|
|
3172
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3173
|
-
GIT_ROOT_COMMAND.ABBREV_REF,
|
|
3174
|
-
GIT_ROOT_COMMAND.HEAD
|
|
3175
|
-
];
|
|
3176
|
-
var GIT_HEAD_SHA_ARGS = [
|
|
3177
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3178
|
-
GIT_ROOT_COMMAND.HEAD
|
|
3179
|
-
];
|
|
3180
|
-
var GIT_ORIGIN_HEAD_REF_ARGS = [
|
|
3181
|
-
GIT_ROOT_COMMAND.SYMBOLIC_REF,
|
|
3182
|
-
GIT_ROOT_COMMAND.SHORT,
|
|
3183
|
-
GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
|
|
3184
|
-
];
|
|
3185
|
-
var GIT_STATUS_PORCELAIN_ARGS = [
|
|
3186
|
-
GIT_ROOT_COMMAND.STATUS,
|
|
3187
|
-
GIT_ROOT_COMMAND.PORCELAIN
|
|
3188
|
-
];
|
|
3189
|
-
async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultDeps) {
|
|
3190
|
-
try {
|
|
3191
|
-
const result = await deps.execa(
|
|
3192
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3193
|
-
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
3194
|
-
{ cwd, reject: false }
|
|
3195
|
-
);
|
|
3196
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
3197
|
-
return {
|
|
3198
|
-
productDir: extractStdout(result.stdout),
|
|
3199
|
-
isGitRepo: true
|
|
3200
|
-
};
|
|
3201
|
-
}
|
|
3202
|
-
return {
|
|
3203
|
-
productDir: cwd,
|
|
3204
|
-
isGitRepo: false,
|
|
3205
|
-
warning: NOT_GIT_REPO_WARNING
|
|
3206
|
-
};
|
|
3207
|
-
} catch {
|
|
3208
|
-
return {
|
|
3209
|
-
productDir: cwd,
|
|
3210
|
-
isGitRepo: false,
|
|
3211
|
-
warning: NOT_GIT_REPO_WARNING
|
|
3212
|
-
};
|
|
3213
|
-
}
|
|
3214
|
-
}
|
|
3215
|
-
function extractStdout(stdout) {
|
|
3216
|
-
if (!stdout) return "";
|
|
3217
|
-
const str = typeof stdout === "string" ? stdout : String(stdout);
|
|
3218
|
-
return str.trim().replace(/\/+$/, "");
|
|
3219
|
-
}
|
|
3220
|
-
async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultDeps) {
|
|
3221
|
-
try {
|
|
3222
|
-
const toplevelResult = await deps.execa(
|
|
3223
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3224
|
-
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
3225
|
-
{ cwd, reject: false }
|
|
3226
|
-
);
|
|
3227
|
-
if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
|
|
3228
|
-
return {
|
|
3229
|
-
productDir: cwd,
|
|
3230
|
-
isGitRepo: false,
|
|
3231
|
-
warning: NOT_GIT_REPO_WARNING,
|
|
3232
|
-
worktreeRoot: cwd
|
|
3233
|
-
};
|
|
3234
|
-
}
|
|
3235
|
-
const toplevel = extractStdout(toplevelResult.stdout);
|
|
3236
|
-
const commonDirResult = await deps.execa(
|
|
3237
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3238
|
-
[...GIT_COMMON_DIR_ARGS],
|
|
3239
|
-
{ cwd, reject: false }
|
|
3240
|
-
);
|
|
3241
|
-
if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
|
|
3242
|
-
return {
|
|
3243
|
-
productDir: toplevel,
|
|
3244
|
-
isGitRepo: true,
|
|
3245
|
-
worktreeRoot: toplevel
|
|
3246
|
-
};
|
|
3247
|
-
}
|
|
3248
|
-
const commonDir = extractStdout(commonDirResult.stdout);
|
|
3249
|
-
const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve3(toplevel, commonDir);
|
|
3250
|
-
const gitCommonDirProductRoot = dirname(absoluteCommonDir);
|
|
3251
|
-
return {
|
|
3252
|
-
productDir: gitCommonDirProductRoot,
|
|
3253
|
-
isGitRepo: true,
|
|
3254
|
-
worktreeRoot: toplevel
|
|
3255
|
-
};
|
|
3256
|
-
} catch {
|
|
3257
|
-
return {
|
|
3258
|
-
productDir: cwd,
|
|
3259
|
-
isGitRepo: false,
|
|
3260
|
-
warning: NOT_GIT_REPO_WARNING,
|
|
3261
|
-
worktreeRoot: cwd
|
|
3262
|
-
};
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultDeps) {
|
|
3266
|
-
const result = await deps.execa(
|
|
3267
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3268
|
-
[...GIT_ORIGIN_HEAD_REF_ARGS],
|
|
3269
|
-
{ cwd, reject: false }
|
|
3270
|
-
);
|
|
3271
|
-
if (result.exitCode !== 0) return null;
|
|
3272
|
-
const ref = extractStdout(result.stdout);
|
|
3273
|
-
if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
|
|
3274
|
-
const branch = ref.slice(ORIGIN_REF_PREFIX.length);
|
|
3275
|
-
return branch.length === 0 ? null : branch;
|
|
3276
|
-
}
|
|
3277
|
-
async function getCurrentBranch(cwd = process.cwd(), deps = defaultDeps) {
|
|
3278
|
-
const result = await deps.execa(
|
|
3279
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3280
|
-
[...GIT_CURRENT_BRANCH_ARGS],
|
|
3281
|
-
{ cwd, reject: false }
|
|
3282
|
-
);
|
|
3283
|
-
if (result.exitCode !== 0) return null;
|
|
3284
|
-
const branch = extractStdout(result.stdout);
|
|
3285
|
-
if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
|
|
3286
|
-
return branch;
|
|
3287
|
-
}
|
|
3288
|
-
async function getHeadSha(cwd = process.cwd(), deps = defaultDeps) {
|
|
3289
|
-
const result = await deps.execa(
|
|
3290
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3291
|
-
[...GIT_HEAD_SHA_ARGS],
|
|
3292
|
-
{ cwd, reject: false }
|
|
3293
|
-
);
|
|
3294
|
-
if (result.exitCode !== 0) return null;
|
|
3295
|
-
const sha = extractStdout(result.stdout);
|
|
3296
|
-
return sha.length === 0 ? null : sha;
|
|
3297
|
-
}
|
|
3298
|
-
async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultDeps) {
|
|
3299
|
-
const result = await deps.execa(
|
|
3300
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3301
|
-
[GIT_ROOT_COMMAND.REV_PARSE, ref],
|
|
3302
|
-
{ cwd, reject: false }
|
|
3303
|
-
);
|
|
3304
|
-
if (result.exitCode !== 0) return null;
|
|
3305
|
-
const sha = extractStdout(result.stdout);
|
|
3306
|
-
return sha.length === 0 ? null : sha;
|
|
3307
|
-
}
|
|
3308
|
-
async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultDeps) {
|
|
3309
|
-
const result = await deps.execa(
|
|
3310
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3311
|
-
[...GIT_STATUS_PORCELAIN_ARGS],
|
|
3312
|
-
{ cwd, reject: false }
|
|
3313
|
-
);
|
|
3314
|
-
if (result.exitCode !== 0) return false;
|
|
3315
|
-
return extractStdout(result.stdout).length === 0;
|
|
3316
|
-
}
|
|
3738
|
+
// src/commands/session/resolve-config.ts
|
|
3317
3739
|
async function resolveSessionConfig(options = {}) {
|
|
3318
3740
|
const { sessionsDir, cwd, deps } = options;
|
|
3319
3741
|
const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
|
|
3320
3742
|
if (sessionsDir) {
|
|
3321
3743
|
return {
|
|
3322
3744
|
config: {
|
|
3323
|
-
todoDir:
|
|
3324
|
-
doingDir:
|
|
3325
|
-
archiveDir:
|
|
3745
|
+
todoDir: join6(sessionsDir, statusDirs2.todo),
|
|
3746
|
+
doingDir: join6(sessionsDir, statusDirs2.doing),
|
|
3747
|
+
archiveDir: join6(sessionsDir, statusDirs2.archive)
|
|
3326
3748
|
}
|
|
3327
3749
|
};
|
|
3328
3750
|
}
|
|
3329
|
-
const
|
|
3330
|
-
const baseDir = join3(gitResult.productDir, DEFAULT_CONFIG.sessions.dir);
|
|
3751
|
+
const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
|
|
3331
3752
|
return {
|
|
3332
3753
|
config: {
|
|
3333
|
-
todoDir:
|
|
3334
|
-
doingDir:
|
|
3335
|
-
archiveDir:
|
|
3754
|
+
todoDir: join6(baseDir, statusDirs2.todo),
|
|
3755
|
+
doingDir: join6(baseDir, statusDirs2.doing),
|
|
3756
|
+
archiveDir: join6(baseDir, statusDirs2.archive)
|
|
3336
3757
|
},
|
|
3337
|
-
warning
|
|
3758
|
+
warning
|
|
3338
3759
|
};
|
|
3339
3760
|
}
|
|
3340
|
-
|
|
3341
|
-
// src/commands/session/resolve-config.ts
|
|
3342
3761
|
async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning) {
|
|
3343
3762
|
const { config, warning } = await resolveSessionConfig({ sessionsDir });
|
|
3344
3763
|
if (warning !== void 0) {
|
|
@@ -3371,9 +3790,9 @@ async function fileExists(path6) {
|
|
|
3371
3790
|
}
|
|
3372
3791
|
async function probeSessionPaths(sessionId, config) {
|
|
3373
3792
|
const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
|
|
3374
|
-
const todoPath =
|
|
3375
|
-
const doingPath =
|
|
3376
|
-
const archivePath =
|
|
3793
|
+
const todoPath = join7(config.todoDir, filename);
|
|
3794
|
+
const doingPath = join7(config.doingDir, filename);
|
|
3795
|
+
const archivePath = join7(config.archiveDir, filename);
|
|
3377
3796
|
return {
|
|
3378
3797
|
todo: await fileExists(todoPath) ? todoPath : null,
|
|
3379
3798
|
doing: await fileExists(doingPath) ? doingPath : null,
|
|
@@ -3394,7 +3813,7 @@ async function resolveArchivePaths(sessionId, config) {
|
|
|
3394
3813
|
}
|
|
3395
3814
|
async function archiveSingle(sessionId, config) {
|
|
3396
3815
|
const { source, target } = await resolveArchivePaths(sessionId, config);
|
|
3397
|
-
await mkdir(
|
|
3816
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
3398
3817
|
await rename(source, target);
|
|
3399
3818
|
return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
|
|
3400
3819
|
${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
|
|
@@ -3417,7 +3836,7 @@ function resolveDeletePath(sessionId, existingPaths) {
|
|
|
3417
3836
|
}
|
|
3418
3837
|
|
|
3419
3838
|
// src/domains/session/show.ts
|
|
3420
|
-
import { join as
|
|
3839
|
+
import { join as join8 } from "path";
|
|
3421
3840
|
|
|
3422
3841
|
// src/domains/session/list.ts
|
|
3423
3842
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -3428,12 +3847,12 @@ var SESSION_ID_SEPARATOR = "_";
|
|
|
3428
3847
|
function generateSessionId(options = {}) {
|
|
3429
3848
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3430
3849
|
const date = now();
|
|
3431
|
-
const year = date.
|
|
3432
|
-
const month = String(date.
|
|
3433
|
-
const day = String(date.
|
|
3434
|
-
const hours = String(date.
|
|
3435
|
-
const minutes = String(date.
|
|
3436
|
-
const seconds = String(date.
|
|
3850
|
+
const year = date.getUTCFullYear();
|
|
3851
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
3852
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
3853
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
3854
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
3855
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
3437
3856
|
return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
|
|
3438
3857
|
}
|
|
3439
3858
|
function parseSessionId(id) {
|
|
@@ -3453,7 +3872,11 @@ function parseSessionId(id) {
|
|
|
3453
3872
|
if (hours < 0 || hours > 23) return null;
|
|
3454
3873
|
if (minutes < 0 || minutes > 59) return null;
|
|
3455
3874
|
if (seconds < 0 || seconds > 59) return null;
|
|
3456
|
-
|
|
3875
|
+
const date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
|
|
3876
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day || date.getUTCHours() !== hours || date.getUTCMinutes() !== minutes || date.getUTCSeconds() !== seconds) {
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
return date;
|
|
3457
3880
|
}
|
|
3458
3881
|
|
|
3459
3882
|
// src/domains/session/types.ts
|
|
@@ -3556,6 +3979,67 @@ function sortSessions(sessions) {
|
|
|
3556
3979
|
return dateB.getTime() - dateA.getTime();
|
|
3557
3980
|
});
|
|
3558
3981
|
}
|
|
3982
|
+
var FIELD_SELECTION_SEPARATOR = ",";
|
|
3983
|
+
var SESSION_RECORD_FIELD = {
|
|
3984
|
+
ID: "id",
|
|
3985
|
+
STATUS: "status",
|
|
3986
|
+
PRIORITY: SESSION_FRONT_MATTER.PRIORITY,
|
|
3987
|
+
GIT_REF: SESSION_FRONT_MATTER.GIT_REF,
|
|
3988
|
+
GOAL: SESSION_FRONT_MATTER.GOAL,
|
|
3989
|
+
NEXT_STEP: SESSION_FRONT_MATTER.NEXT_STEP,
|
|
3990
|
+
SPECS: SESSION_FRONT_MATTER.SPECS,
|
|
3991
|
+
FILES: SESSION_FRONT_MATTER.FILES,
|
|
3992
|
+
CREATED_AT: SESSION_FRONT_MATTER.CREATED_AT,
|
|
3993
|
+
AGENT_SESSION_ID: SESSION_FRONT_MATTER.AGENT_SESSION_ID
|
|
3994
|
+
};
|
|
3995
|
+
var SESSION_RECORD_FIELDS = Object.values(SESSION_RECORD_FIELD);
|
|
3996
|
+
function toSessionRecord(session) {
|
|
3997
|
+
const { id, status, metadata } = session;
|
|
3998
|
+
const record = {
|
|
3999
|
+
id,
|
|
4000
|
+
status,
|
|
4001
|
+
priority: metadata.priority,
|
|
4002
|
+
git_ref: metadata.git_ref,
|
|
4003
|
+
goal: metadata.goal,
|
|
4004
|
+
next_step: metadata.next_step,
|
|
4005
|
+
specs: metadata.specs,
|
|
4006
|
+
files: metadata.files
|
|
4007
|
+
};
|
|
4008
|
+
if (metadata.created_at !== void 0) {
|
|
4009
|
+
record.created_at = metadata.created_at;
|
|
4010
|
+
}
|
|
4011
|
+
if (metadata.agent_session_id !== void 0) {
|
|
4012
|
+
record.agent_session_id = metadata.agent_session_id;
|
|
4013
|
+
}
|
|
4014
|
+
return record;
|
|
4015
|
+
}
|
|
4016
|
+
function projectSessionRecord(record, fields) {
|
|
4017
|
+
const projected = {};
|
|
4018
|
+
for (const field of fields) {
|
|
4019
|
+
const value = record[field];
|
|
4020
|
+
if (value !== void 0) {
|
|
4021
|
+
projected[field] = value;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return projected;
|
|
4025
|
+
}
|
|
4026
|
+
function isSessionRecordField(value) {
|
|
4027
|
+
return SESSION_RECORD_FIELDS.includes(value);
|
|
4028
|
+
}
|
|
4029
|
+
function parseFieldSelection(input) {
|
|
4030
|
+
const tokens = input.split(FIELD_SELECTION_SEPARATOR).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
4031
|
+
if (tokens.length === 0) {
|
|
4032
|
+
throw new SessionInvalidFieldError(input, SESSION_RECORD_FIELDS);
|
|
4033
|
+
}
|
|
4034
|
+
const selection = [];
|
|
4035
|
+
for (const token of tokens) {
|
|
4036
|
+
if (!isSessionRecordField(token)) {
|
|
4037
|
+
throw new SessionInvalidFieldError(token, SESSION_RECORD_FIELDS);
|
|
4038
|
+
}
|
|
4039
|
+
selection.push(token);
|
|
4040
|
+
}
|
|
4041
|
+
return selection;
|
|
4042
|
+
}
|
|
3559
4043
|
|
|
3560
4044
|
// src/domains/session/show.ts
|
|
3561
4045
|
var SESSION_SHOW_LABEL = {
|
|
@@ -3569,11 +4053,12 @@ var SESSION_SHOW_LABEL = {
|
|
|
3569
4053
|
};
|
|
3570
4054
|
var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
|
|
3571
4055
|
var SESSION_SHOW_SEPARATOR_WIDTH = 40;
|
|
3572
|
-
var
|
|
4056
|
+
var sessionsBaseDir = join8(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
4057
|
+
var { statusDirs } = DEFAULT_CONFIG.sessions;
|
|
3573
4058
|
var DEFAULT_SESSION_CONFIG = {
|
|
3574
|
-
todoDir:
|
|
3575
|
-
doingDir:
|
|
3576
|
-
archiveDir:
|
|
4059
|
+
todoDir: join8(sessionsBaseDir, statusDirs.todo),
|
|
4060
|
+
doingDir: join8(sessionsBaseDir, statusDirs.doing),
|
|
4061
|
+
archiveDir: join8(sessionsBaseDir, statusDirs.archive)
|
|
3577
4062
|
};
|
|
3578
4063
|
var SEARCH_ORDER = [...SESSION_STATUSES];
|
|
3579
4064
|
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
@@ -3631,7 +4116,7 @@ async function deleteCommand(options) {
|
|
|
3631
4116
|
|
|
3632
4117
|
// src/commands/session/handoff.ts
|
|
3633
4118
|
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
3634
|
-
import { join as
|
|
4119
|
+
import { join as join9, resolve as resolve3 } from "path";
|
|
3635
4120
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
3636
4121
|
|
|
3637
4122
|
// src/domains/session/create.ts
|
|
@@ -3652,11 +4137,11 @@ function cleanPrerequisite(facts) {
|
|
|
3652
4137
|
return {
|
|
3653
4138
|
label: HANDOFF_BASE_PREREQUISITE_LABEL.CLEAN_WORKING_TREE,
|
|
3654
4139
|
met: facts.isClean,
|
|
3655
|
-
remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.
|
|
4140
|
+
remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_MAIN_CHECKOUT
|
|
3656
4141
|
};
|
|
3657
4142
|
}
|
|
3658
4143
|
function detachedAtTipPrerequisite(facts, met) {
|
|
3659
|
-
const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.
|
|
4144
|
+
const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.MAIN_CHECKOUT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_MAIN_CHECKOUT;
|
|
3660
4145
|
return {
|
|
3661
4146
|
label: HANDOFF_BASE_PREREQUISITE_LABEL.DETACHED_AT_DEFAULT_TIP,
|
|
3662
4147
|
met,
|
|
@@ -3669,7 +4154,7 @@ function buildChecklist(facts, prerequisites) {
|
|
|
3669
4154
|
defaultTipSha: facts.defaultTipSha,
|
|
3670
4155
|
headSha: facts.headSha,
|
|
3671
4156
|
currentWorktreePath: facts.currentWorktreePath,
|
|
3672
|
-
|
|
4157
|
+
mainCheckoutPath: facts.mainCheckoutPath,
|
|
3673
4158
|
prerequisites
|
|
3674
4159
|
};
|
|
3675
4160
|
}
|
|
@@ -3677,7 +4162,7 @@ function resolveHandoffGitRef(facts) {
|
|
|
3677
4162
|
if (!facts.isGitRepo) {
|
|
3678
4163
|
throw new SessionHandoffBaseError({ silent: true });
|
|
3679
4164
|
}
|
|
3680
|
-
if (facts.
|
|
4165
|
+
if (facts.isMainCheckout) {
|
|
3681
4166
|
if (facts.branch !== null) return facts.branch;
|
|
3682
4167
|
if (facts.headSha !== null) return facts.headSha;
|
|
3683
4168
|
throw new SessionHandoffBaseError();
|
|
@@ -3692,6 +4177,12 @@ function resolveHandoffGitRef(facts) {
|
|
|
3692
4177
|
}
|
|
3693
4178
|
throw new SessionHandoffBaseError({ checklist: buildChecklist(facts, prerequisites) });
|
|
3694
4179
|
}
|
|
4180
|
+
function resolveWorkBranchGitRef(workBranch, existsOnOrigin) {
|
|
4181
|
+
if (!existsOnOrigin) {
|
|
4182
|
+
throw new SessionWorkBranchNotOnOriginError(workBranch);
|
|
4183
|
+
}
|
|
4184
|
+
return workBranch;
|
|
4185
|
+
}
|
|
3695
4186
|
|
|
3696
4187
|
// src/domains/session/parse-handoff-input.ts
|
|
3697
4188
|
var LEGACY_FRONTMATTER_PREFIX = /^---\r?\n/;
|
|
@@ -3782,12 +4273,14 @@ function validateHandoffHeader(parsed) {
|
|
|
3782
4273
|
const nextStep = ensureStringOrEmpty(obj[SESSION_FRONT_MATTER.NEXT_STEP], SESSION_FRONT_MATTER.NEXT_STEP);
|
|
3783
4274
|
const specs = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.SPECS], SESSION_FRONT_MATTER.SPECS);
|
|
3784
4275
|
const files = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.FILES], SESSION_FRONT_MATTER.FILES);
|
|
4276
|
+
const gitRef = ensureOptionalString(obj[SESSION_FRONT_MATTER.GIT_REF], SESSION_FRONT_MATTER.GIT_REF);
|
|
3785
4277
|
return {
|
|
3786
4278
|
priority,
|
|
3787
4279
|
goal,
|
|
3788
4280
|
next_step: nextStep,
|
|
3789
4281
|
specs,
|
|
3790
|
-
files
|
|
4282
|
+
files,
|
|
4283
|
+
...gitRef === void 0 ? {} : { git_ref: gitRef }
|
|
3791
4284
|
};
|
|
3792
4285
|
}
|
|
3793
4286
|
function ensurePriorityOrDefault(value) {
|
|
@@ -3806,6 +4299,13 @@ function ensureStringOrEmpty(value, fieldName) {
|
|
|
3806
4299
|
}
|
|
3807
4300
|
return value;
|
|
3808
4301
|
}
|
|
4302
|
+
function ensureOptionalString(value, fieldName) {
|
|
4303
|
+
if (value === void 0) return void 0;
|
|
4304
|
+
if (typeof value !== "string") {
|
|
4305
|
+
throw new SessionInvalidJsonHeaderError(`${fieldName} must be a string`);
|
|
4306
|
+
}
|
|
4307
|
+
return value;
|
|
4308
|
+
}
|
|
3809
4309
|
function ensureStringArrayOrDefault(value, fieldName) {
|
|
3810
4310
|
if (value === void 0) return [];
|
|
3811
4311
|
if (!Array.isArray(value)) {
|
|
@@ -3821,18 +4321,19 @@ function ensureStringArrayOrDefault(value, fieldName) {
|
|
|
3821
4321
|
|
|
3822
4322
|
// src/commands/session/handoff.ts
|
|
3823
4323
|
async function resolveSessionGitRef(cwd, deps) {
|
|
3824
|
-
const [branch, headSha,
|
|
4324
|
+
const [branch, headSha, facts] = await Promise.all([
|
|
3825
4325
|
getCurrentBranch(cwd, deps),
|
|
3826
4326
|
getHeadSha(cwd, deps),
|
|
3827
|
-
|
|
4327
|
+
gatherGitFacts(cwd, deps)
|
|
3828
4328
|
]);
|
|
3829
|
-
const
|
|
3830
|
-
const
|
|
3831
|
-
const
|
|
4329
|
+
const isGitRepo = facts !== null;
|
|
4330
|
+
const currentWorktreePath = facts?.worktreeRoot ?? cwd ?? process.cwd();
|
|
4331
|
+
const isMain = facts !== null && isMainCheckout(facts);
|
|
4332
|
+
const designatedMainCheckout = facts !== null ? mainCheckoutPath(facts) : null;
|
|
3832
4333
|
let isClean = false;
|
|
3833
4334
|
let defaultBranch = null;
|
|
3834
4335
|
let defaultTipSha = null;
|
|
3835
|
-
if (
|
|
4336
|
+
if (isGitRepo && !isMain) {
|
|
3836
4337
|
[isClean, defaultBranch] = await Promise.all([
|
|
3837
4338
|
isWorkingTreeClean(cwd, deps),
|
|
3838
4339
|
resolveDefaultBranch(cwd, deps)
|
|
@@ -3840,17 +4341,22 @@ async function resolveSessionGitRef(cwd, deps) {
|
|
|
3840
4341
|
defaultTipSha = defaultBranch === null ? null : await resolveRefSha(`${ORIGIN_REF_PREFIX}${defaultBranch}`, cwd, deps);
|
|
3841
4342
|
}
|
|
3842
4343
|
return resolveHandoffGitRef({
|
|
3843
|
-
isGitRepo
|
|
3844
|
-
|
|
4344
|
+
isGitRepo,
|
|
4345
|
+
isMainCheckout: isMain,
|
|
3845
4346
|
branch,
|
|
3846
4347
|
headSha,
|
|
3847
4348
|
isClean,
|
|
3848
4349
|
defaultBranch,
|
|
3849
4350
|
defaultTipSha,
|
|
3850
4351
|
currentWorktreePath,
|
|
3851
|
-
|
|
4352
|
+
mainCheckoutPath: designatedMainCheckout
|
|
3852
4353
|
});
|
|
3853
4354
|
}
|
|
4355
|
+
async function resolveRecordedGitRef(suppliedRef, gateRef, cwd, deps) {
|
|
4356
|
+
if (suppliedRef === void 0 || suppliedRef.length === 0) return gateRef;
|
|
4357
|
+
const existsOnOrigin = await originBranchExists(suppliedRef, cwd, deps);
|
|
4358
|
+
return resolveWorkBranchGitRef(suppliedRef, existsOnOrigin);
|
|
4359
|
+
}
|
|
3854
4360
|
async function handoffCommand(options) {
|
|
3855
4361
|
const { config } = await resolveSessionConfig({
|
|
3856
4362
|
sessionsDir: options.sessionsDir,
|
|
@@ -3868,9 +4374,10 @@ async function handoffCommand(options) {
|
|
|
3868
4374
|
if (header.next_step.length === 0) {
|
|
3869
4375
|
throw new SessionInvalidNextStepError();
|
|
3870
4376
|
}
|
|
3871
|
-
const
|
|
4377
|
+
const gateRef = await resolveSessionGitRef(options.cwd, options.deps);
|
|
4378
|
+
const gitRef = await resolveRecordedGitRef(header.git_ref, gateRef, options.cwd, options.deps);
|
|
3872
4379
|
const sessionId = generateSessionId();
|
|
3873
|
-
const agentSessionId =
|
|
4380
|
+
const agentSessionId = resolveAgentSessionId(options.env ?? process.env);
|
|
3874
4381
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3875
4382
|
const frontMatterObject = {
|
|
3876
4383
|
[SESSION_FRONT_MATTER.PRIORITY]: header.priority,
|
|
@@ -3887,8 +4394,8 @@ async function handoffCommand(options) {
|
|
|
3887
4394
|
const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
|
|
3888
4395
|
const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
|
|
3889
4396
|
const filename = `${sessionId}.md`;
|
|
3890
|
-
const sessionPath =
|
|
3891
|
-
const absolutePath =
|
|
4397
|
+
const sessionPath = join9(config.todoDir, filename);
|
|
4398
|
+
const absolutePath = resolve3(sessionPath);
|
|
3892
4399
|
await mkdir2(config.todoDir, { recursive: true });
|
|
3893
4400
|
await writeFile(sessionPath, fullContent, "utf-8");
|
|
3894
4401
|
const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
@@ -3898,7 +4405,7 @@ async function handoffCommand(options) {
|
|
|
3898
4405
|
|
|
3899
4406
|
// src/commands/session/list.ts
|
|
3900
4407
|
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
3901
|
-
import { join as
|
|
4408
|
+
import { join as join10 } from "path";
|
|
3902
4409
|
var SESSION_LIST_FORMAT = {
|
|
3903
4410
|
TEXT: "text",
|
|
3904
4411
|
JSON: "json"
|
|
@@ -3912,7 +4419,7 @@ async function loadSessionsFromDir(dir, status) {
|
|
|
3912
4419
|
for (const file of files) {
|
|
3913
4420
|
if (!file.endsWith(".md")) continue;
|
|
3914
4421
|
const id = file.replace(".md", "");
|
|
3915
|
-
const filePath =
|
|
4422
|
+
const filePath = join10(dir, file);
|
|
3916
4423
|
const content = await readFile3(filePath, "utf-8");
|
|
3917
4424
|
const metadata = parseSessionMetadata(content);
|
|
3918
4425
|
sessions.push({
|
|
@@ -3954,6 +4461,7 @@ function validateStatus(input) {
|
|
|
3954
4461
|
);
|
|
3955
4462
|
}
|
|
3956
4463
|
async function listCommand(options) {
|
|
4464
|
+
const fieldSelection = options.fields !== void 0 ? parseFieldSelection(options.fields) : void 0;
|
|
3957
4465
|
const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
|
|
3958
4466
|
const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
|
|
3959
4467
|
const sessionsByStatus = {};
|
|
@@ -3962,8 +4470,16 @@ async function listCommand(options) {
|
|
|
3962
4470
|
const sessions = await loadSessionsFromDir(config[dirKey], status);
|
|
3963
4471
|
sessionsByStatus[status] = sortSessions(sessions);
|
|
3964
4472
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
4473
|
+
const emitJson = fieldSelection !== void 0 || options.format === SESSION_LIST_FORMAT.JSON;
|
|
4474
|
+
if (emitJson) {
|
|
4475
|
+
const recordsByStatus = {};
|
|
4476
|
+
for (const status of statuses) {
|
|
4477
|
+
recordsByStatus[status] = (sessionsByStatus[status] ?? []).map((session) => {
|
|
4478
|
+
const record = toSessionRecord(session);
|
|
4479
|
+
return fieldSelection !== void 0 ? projectSessionRecord(record, fieldSelection) : record;
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
return JSON.stringify(recordsByStatus, null, 2);
|
|
3967
4483
|
}
|
|
3968
4484
|
const lines = [];
|
|
3969
4485
|
for (const status of statuses) {
|
|
@@ -3976,7 +4492,7 @@ async function listCommand(options) {
|
|
|
3976
4492
|
|
|
3977
4493
|
// src/commands/session/pickup.ts
|
|
3978
4494
|
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
|
|
3979
|
-
import { join as
|
|
4495
|
+
import { join as join11 } from "path";
|
|
3980
4496
|
|
|
3981
4497
|
// src/domains/session/pickup.ts
|
|
3982
4498
|
function buildClaimPaths(sessionId, config) {
|
|
@@ -4021,7 +4537,7 @@ async function loadTodoSessions(config) {
|
|
|
4021
4537
|
for (const file of files) {
|
|
4022
4538
|
if (!file.endsWith(".md")) continue;
|
|
4023
4539
|
const id = file.replace(".md", "");
|
|
4024
|
-
const filePath =
|
|
4540
|
+
const filePath = join11(config.todoDir, file);
|
|
4025
4541
|
const content = await readFile4(filePath, "utf-8");
|
|
4026
4542
|
const metadata = parseSessionMetadata(content);
|
|
4027
4543
|
sessions.push({
|
|
@@ -4077,7 +4593,7 @@ async function pickupCommand(options) {
|
|
|
4077
4593
|
|
|
4078
4594
|
// src/commands/session/prune.ts
|
|
4079
4595
|
import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
|
|
4080
|
-
import { join as
|
|
4596
|
+
import { join as join12 } from "path";
|
|
4081
4597
|
|
|
4082
4598
|
// src/domains/session/prune.ts
|
|
4083
4599
|
var DEFAULT_KEEP_COUNT = 5;
|
|
@@ -4126,7 +4642,7 @@ async function loadArchiveSessions(config) {
|
|
|
4126
4642
|
for (const file of files) {
|
|
4127
4643
|
if (!file.endsWith(".md")) continue;
|
|
4128
4644
|
const id = file.replace(".md", "");
|
|
4129
|
-
const filePath =
|
|
4645
|
+
const filePath = join12(config.archiveDir, file);
|
|
4130
4646
|
const content = await readFile5(filePath, "utf-8");
|
|
4131
4647
|
const metadata = parseSessionMetadata(content);
|
|
4132
4648
|
sessions.push({
|
|
@@ -4316,9 +4832,9 @@ Prefilled by the CLI:
|
|
|
4316
4832
|
created_at, git_ref, and agent_session_id when an agent session ID is
|
|
4317
4833
|
available.
|
|
4318
4834
|
|
|
4319
|
-
git_ref records the branch name (
|
|
4320
|
-
(detached), or the origin/<default> tip SHA (clean detached
|
|
4321
|
-
Handoff is refused from any other
|
|
4835
|
+
git_ref records the branch name (main checkout on a branch), the HEAD SHA
|
|
4836
|
+
(detached), or the origin/<default> tip SHA (clean detached non-main checkout).
|
|
4837
|
+
Handoff is refused from any other non-main checkout state.
|
|
4322
4838
|
|
|
4323
4839
|
Output Tags (for automation):
|
|
4324
4840
|
<HANDOFF_ID>session-id</HANDOFF_ID> Session identifier
|
|
@@ -4349,17 +4865,17 @@ async function readStdin() {
|
|
|
4349
4865
|
if (process.stdin.isTTY) {
|
|
4350
4866
|
return void 0;
|
|
4351
4867
|
}
|
|
4352
|
-
return new Promise((
|
|
4868
|
+
return new Promise((resolve5) => {
|
|
4353
4869
|
let data = "";
|
|
4354
4870
|
process.stdin.setEncoding("utf-8");
|
|
4355
4871
|
process.stdin.on("data", (chunk) => {
|
|
4356
4872
|
data += chunk;
|
|
4357
4873
|
});
|
|
4358
4874
|
process.stdin.on("end", () => {
|
|
4359
|
-
|
|
4875
|
+
resolve5(data.length === 0 ? void 0 : data);
|
|
4360
4876
|
});
|
|
4361
4877
|
process.stdin.on("error", () => {
|
|
4362
|
-
|
|
4878
|
+
resolve5(void 0);
|
|
4363
4879
|
});
|
|
4364
4880
|
});
|
|
4365
4881
|
}
|
|
@@ -4368,11 +4884,12 @@ function handleError(error) {
|
|
|
4368
4884
|
process.exit(1);
|
|
4369
4885
|
}
|
|
4370
4886
|
function registerSessionCommands(sessionCmd) {
|
|
4371
|
-
sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4887
|
+
sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4372
4888
|
try {
|
|
4373
4889
|
const output = await listCommand({
|
|
4374
4890
|
status: options.status,
|
|
4375
|
-
format: options.json ?
|
|
4891
|
+
format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
|
|
4892
|
+
fields: options.fields,
|
|
4376
4893
|
sessionsDir: options.sessionsDir,
|
|
4377
4894
|
onWarning: writeWarning
|
|
4378
4895
|
});
|
|
@@ -4381,11 +4898,12 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4381
4898
|
handleError(error);
|
|
4382
4899
|
}
|
|
4383
4900
|
});
|
|
4384
|
-
sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4901
|
+
sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4385
4902
|
try {
|
|
4386
4903
|
const output = await listCommand({
|
|
4387
4904
|
status: SESSION_STATUSES[0],
|
|
4388
|
-
format: options.json ?
|
|
4905
|
+
format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
|
|
4906
|
+
fields: options.fields,
|
|
4389
4907
|
sessionsDir: options.sessionsDir,
|
|
4390
4908
|
onWarning: writeWarning
|
|
4391
4909
|
});
|
|
@@ -4440,7 +4958,8 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4440
4958
|
const content = await readStdin();
|
|
4441
4959
|
const result = await handoffCommand({
|
|
4442
4960
|
content,
|
|
4443
|
-
sessionsDir: options.sessionsDir
|
|
4961
|
+
sessionsDir: options.sessionsDir,
|
|
4962
|
+
env: process.env
|
|
4444
4963
|
});
|
|
4445
4964
|
console.log(result.output);
|
|
4446
4965
|
} catch (error) {
|
|
@@ -4513,7 +5032,7 @@ var sessionDomain = {
|
|
|
4513
5032
|
|
|
4514
5033
|
// src/lib/spec-tree/index.ts
|
|
4515
5034
|
import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
4516
|
-
import { join as
|
|
5035
|
+
import { join as join13 } from "path";
|
|
4517
5036
|
var SPEC_TREE_FIELD_KEY = {
|
|
4518
5037
|
VERSION: "version",
|
|
4519
5038
|
PRODUCT: "product",
|
|
@@ -4531,7 +5050,9 @@ var SPEC_TREE_ENTRY_TYPE = {
|
|
|
4531
5050
|
PRODUCT: SPEC_TREE_FIELD_KEY.PRODUCT,
|
|
4532
5051
|
NODE: SPEC_TREE_KIND_CATEGORY.NODE,
|
|
4533
5052
|
DECISION: SPEC_TREE_KIND_CATEGORY.DECISION,
|
|
4534
|
-
EVIDENCE: "evidence"
|
|
5053
|
+
EVIDENCE: "evidence",
|
|
5054
|
+
SUPERSEDED: "superseded",
|
|
5055
|
+
INVALID: "invalid"
|
|
4535
5056
|
};
|
|
4536
5057
|
var SPEC_TREE_FILESYSTEM_RECORD_TYPE = {
|
|
4537
5058
|
DIRECTORY: "directory",
|
|
@@ -4542,17 +5063,6 @@ var SPEC_TREE_EVIDENCE_STATUS = {
|
|
|
4542
5063
|
FAILING: SPEC_TREE_NODE_STATE.FAILING,
|
|
4543
5064
|
PASSING: SPEC_TREE_NODE_STATE.PASSING
|
|
4544
5065
|
};
|
|
4545
|
-
var SPEC_TREE_EVIDENCE_FILE = {
|
|
4546
|
-
DIRECTORY_NAME: "tests",
|
|
4547
|
-
MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
|
|
4548
|
-
LEVELS: ["l1", "l2", "l3"],
|
|
4549
|
-
TAILS: {
|
|
4550
|
-
TYPESCRIPT: ["test", "ts"],
|
|
4551
|
-
PYTHON: ["py"],
|
|
4552
|
-
RUST: ["rs"]
|
|
4553
|
-
},
|
|
4554
|
-
SEGMENT_SEPARATOR: "."
|
|
4555
|
-
};
|
|
4556
5066
|
var SPEC_TREE_EVIDENCE_FILE_TAILS = Object.values(SPEC_TREE_EVIDENCE_FILE.TAILS);
|
|
4557
5067
|
var SPEC_TREE_PROJECTION = {
|
|
4558
5068
|
VERSION: 1,
|
|
@@ -4586,30 +5096,33 @@ var SPEC_TREE_NODE_RELATION_KEYS = {
|
|
|
4586
5096
|
DECISIONS: SPEC_TREE_FIELD_KEY.DECISIONS
|
|
4587
5097
|
};
|
|
4588
5098
|
var ORDER_COMPARISON_EQUAL = 0;
|
|
4589
|
-
var SPEC_TREE_PATH_SEPARATOR =
|
|
4590
|
-
var SPEC_TREE_ORDER_SEPARATOR =
|
|
5099
|
+
var SPEC_TREE_PATH_SEPARATOR = SPEC_TREE_GRAMMAR.PATH_SEPARATOR;
|
|
5100
|
+
var SPEC_TREE_ORDER_SEPARATOR = SPEC_TREE_GRAMMAR.ORDER.SEPARATOR;
|
|
4591
5101
|
var SPEC_TREE_ORDER_RADIX = 10;
|
|
4592
5102
|
var SPEC_TREE_TEXT_ENCODING = "utf8";
|
|
4593
5103
|
var SPEC_TREE_EMPTY_RELATIVE_PATH = "";
|
|
4594
|
-
var SPEC_TREE_ORDER_PATTERN =
|
|
5104
|
+
var SPEC_TREE_ORDER_PATTERN = SPEC_TREE_GRAMMAR.ORDER.PATTERN;
|
|
4595
5105
|
var SPEC_TREE_MIN_EVIDENCE_PATH_SEGMENTS = 2;
|
|
4596
5106
|
var SPEC_TREE_PARENT_SEGMENT_OFFSET = 2;
|
|
4597
5107
|
var SPEC_TREE_FIRST_EVIDENCE_MARKER_INDEX = 1;
|
|
4598
5108
|
var SPEC_TREE_EXACTLY_ONE_EVIDENCE_MARKER = 1;
|
|
4599
5109
|
function createFilesystemSpecTreeSource(options) {
|
|
4600
5110
|
const registry = options.registry ?? KIND_REGISTRY;
|
|
5111
|
+
const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
|
|
4601
5112
|
const includePath = options.includePath ?? includeEverySpecTreePath;
|
|
4602
5113
|
return {
|
|
4603
|
-
entries: () => readFilesystemSourceEntries(options.productDir, registry, includePath),
|
|
5114
|
+
entries: () => readFilesystemSourceEntries(options.productDir, registry, schemaVersions, includePath),
|
|
4604
5115
|
async readText(ref) {
|
|
4605
5116
|
if (ref.path === void 0) {
|
|
4606
5117
|
throw new Error("Filesystem source refs require a path");
|
|
4607
5118
|
}
|
|
4608
|
-
return readFile7(
|
|
5119
|
+
return readFile7(join13(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
|
|
4609
5120
|
}
|
|
4610
5121
|
};
|
|
4611
5122
|
}
|
|
4612
|
-
function recognizeSpecTreeFilesystemEntry(record,
|
|
5123
|
+
function recognizeSpecTreeFilesystemEntry(record, options = {}) {
|
|
5124
|
+
const registry = options.registry ?? KIND_REGISTRY;
|
|
5125
|
+
const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
|
|
4613
5126
|
const name = readLastPathSegment(record.relativePath);
|
|
4614
5127
|
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && isProductFile(record.relativePath)) {
|
|
4615
5128
|
return {
|
|
@@ -4620,49 +5133,107 @@ function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
|
|
|
4620
5133
|
};
|
|
4621
5134
|
}
|
|
4622
5135
|
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.DIRECTORY) {
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
5136
|
+
return recognizeDirectoryRecord(record, name, registry, schemaVersions);
|
|
5137
|
+
}
|
|
5138
|
+
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && record.parentId !== void 0 && isEvidenceFile(record.relativePath)) {
|
|
5139
|
+
return {
|
|
5140
|
+
type: SPEC_TREE_ENTRY_TYPE.EVIDENCE,
|
|
5141
|
+
id: record.relativePath,
|
|
5142
|
+
parentId: record.parentId,
|
|
5143
|
+
status: SPEC_TREE_EVIDENCE_STATUS.LINKED,
|
|
5144
|
+
ref: sourceRefForRelativePath(record.relativePath)
|
|
5145
|
+
};
|
|
5146
|
+
}
|
|
5147
|
+
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE) {
|
|
5148
|
+
const decisionMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION);
|
|
5149
|
+
if (decisionMatch === null) return null;
|
|
5150
|
+
const parsed = parseOrderedSlug(stripSuffix(name, decisionMatch.definition.suffix));
|
|
4626
5151
|
if (parsed === null) return null;
|
|
4627
5152
|
return {
|
|
4628
|
-
type: SPEC_TREE_ENTRY_TYPE.
|
|
4629
|
-
kind:
|
|
5153
|
+
type: SPEC_TREE_ENTRY_TYPE.DECISION,
|
|
5154
|
+
kind: decisionMatch.kind,
|
|
4630
5155
|
id: record.relativePath,
|
|
4631
5156
|
order: parsed.order,
|
|
4632
5157
|
slug: parsed.slug,
|
|
4633
5158
|
parentId: record.parentId,
|
|
4634
|
-
ref:
|
|
5159
|
+
ref: sourceRefForRelativePath(record.relativePath)
|
|
4635
5160
|
};
|
|
4636
5161
|
}
|
|
4637
|
-
|
|
5162
|
+
return null;
|
|
5163
|
+
}
|
|
5164
|
+
function recognizeDirectoryRecord(record, name, registry, schemaVersions) {
|
|
5165
|
+
const canonical = canonicalNamingSchemaVersion(schemaVersions);
|
|
5166
|
+
const canonicalMatch = matchNodeSuffixFromVersion(name, canonical);
|
|
5167
|
+
if (canonicalMatch !== null) {
|
|
5168
|
+
const kind = nodeKindForSuffix(canonicalMatch.suffix, registry);
|
|
5169
|
+
if (kind !== null) {
|
|
5170
|
+
return {
|
|
5171
|
+
type: SPEC_TREE_ENTRY_TYPE.NODE,
|
|
5172
|
+
kind,
|
|
5173
|
+
id: record.relativePath,
|
|
5174
|
+
order: canonicalMatch.parsed.order,
|
|
5175
|
+
slug: canonicalMatch.parsed.slug,
|
|
5176
|
+
parentId: record.parentId,
|
|
5177
|
+
ref: sourceRefForNode(record.relativePath, canonicalMatch.parsed.slug)
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
const supersededVersion = matchSupersededNodeVersion(name, schemaVersions, canonical);
|
|
5182
|
+
if (supersededVersion !== null) {
|
|
4638
5183
|
return {
|
|
4639
|
-
type: SPEC_TREE_ENTRY_TYPE.
|
|
5184
|
+
type: SPEC_TREE_ENTRY_TYPE.SUPERSEDED,
|
|
4640
5185
|
id: record.relativePath,
|
|
5186
|
+
version: supersededVersion,
|
|
4641
5187
|
parentId: record.parentId,
|
|
4642
|
-
status: SPEC_TREE_EVIDENCE_STATUS.LINKED,
|
|
4643
5188
|
ref: sourceRefForRelativePath(record.relativePath)
|
|
4644
5189
|
};
|
|
4645
5190
|
}
|
|
4646
|
-
if (
|
|
4647
|
-
const decisionMatch = matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.DECISION);
|
|
4648
|
-
if (decisionMatch === null) return null;
|
|
4649
|
-
const parsed = parseOrderedSlug(stripSuffix(name, decisionMatch.definition.suffix));
|
|
4650
|
-
if (parsed === null) return null;
|
|
5191
|
+
if (parseOrderedSlug(name) !== null) {
|
|
4651
5192
|
return {
|
|
4652
|
-
type: SPEC_TREE_ENTRY_TYPE.
|
|
4653
|
-
kind: decisionMatch.kind,
|
|
5193
|
+
type: SPEC_TREE_ENTRY_TYPE.INVALID,
|
|
4654
5194
|
id: record.relativePath,
|
|
4655
|
-
order: parsed.order,
|
|
4656
|
-
slug: parsed.slug,
|
|
4657
5195
|
parentId: record.parentId,
|
|
4658
5196
|
ref: sourceRefForRelativePath(record.relativePath)
|
|
4659
5197
|
};
|
|
4660
5198
|
}
|
|
4661
5199
|
return null;
|
|
4662
5200
|
}
|
|
5201
|
+
function matchNodeSuffixFromVersion(name, version2) {
|
|
5202
|
+
for (const suffix of version2.nodeSuffixes) {
|
|
5203
|
+
if (!name.endsWith(suffix)) continue;
|
|
5204
|
+
const parsed = parseOrderedSlug(stripSuffix(name, suffix));
|
|
5205
|
+
if (parsed !== null) {
|
|
5206
|
+
return { suffix, parsed };
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
return null;
|
|
5210
|
+
}
|
|
5211
|
+
function nodeKindForSuffix(suffix, registry) {
|
|
5212
|
+
for (const [kind, definition] of Object.entries(registry)) {
|
|
5213
|
+
if (definition.category === SPEC_TREE_KIND_CATEGORY.NODE && definition.suffix === suffix) {
|
|
5214
|
+
return kind;
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
return null;
|
|
5218
|
+
}
|
|
5219
|
+
function matchSupersededNodeVersion(name, schemaVersions, canonical) {
|
|
5220
|
+
const canonicalSuffixes = new Set(canonical.nodeSuffixes);
|
|
5221
|
+
const priorVersions = schemaVersions.filter((version2) => version2 !== canonical).sort((left, right) => compareNamingSchemaVersions(right, left));
|
|
5222
|
+
for (const version2 of priorVersions) {
|
|
5223
|
+
for (const suffix of version2.nodeSuffixes) {
|
|
5224
|
+
if (canonicalSuffixes.has(suffix)) continue;
|
|
5225
|
+
if (name.endsWith(suffix) && parseOrderedSlug(stripSuffix(name, suffix)) !== null) {
|
|
5226
|
+
return version2.version;
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
return null;
|
|
5231
|
+
}
|
|
4663
5232
|
async function readSpecTree(options) {
|
|
4664
5233
|
const entries = await collectSourceEntries(options.source);
|
|
4665
5234
|
const product = entries.find(isProductEntry) ?? null;
|
|
5235
|
+
const superseded = entries.filter(isSupersededEntry);
|
|
5236
|
+
const residual = entries.filter(isInvalidEntry);
|
|
4666
5237
|
const evidenceByParent = groupEvidence(entries.filter(isEvidenceEntry));
|
|
4667
5238
|
const decisions = entries.filter(isDecisionEntry).map(toDecision).sort(compareOrderedEntries);
|
|
4668
5239
|
const decisionsByParent = groupDecisions(decisions);
|
|
@@ -4702,6 +5273,8 @@ async function readSpecTree(options) {
|
|
|
4702
5273
|
nodes: roots,
|
|
4703
5274
|
allNodes,
|
|
4704
5275
|
decisions,
|
|
5276
|
+
superseded,
|
|
5277
|
+
residual,
|
|
4705
5278
|
entries
|
|
4706
5279
|
};
|
|
4707
5280
|
}
|
|
@@ -4743,6 +5316,12 @@ function isDecisionEntry(entry) {
|
|
|
4743
5316
|
function isEvidenceEntry(entry) {
|
|
4744
5317
|
return entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE;
|
|
4745
5318
|
}
|
|
5319
|
+
function isSupersededEntry(entry) {
|
|
5320
|
+
return entry.type === SPEC_TREE_ENTRY_TYPE.SUPERSEDED;
|
|
5321
|
+
}
|
|
5322
|
+
function isInvalidEntry(entry) {
|
|
5323
|
+
return entry.type === SPEC_TREE_ENTRY_TYPE.INVALID;
|
|
5324
|
+
}
|
|
4746
5325
|
function toDecision(entry) {
|
|
4747
5326
|
return {
|
|
4748
5327
|
id: entry.id,
|
|
@@ -4817,11 +5396,12 @@ function compareOrderedEntries(left, right) {
|
|
|
4817
5396
|
if (orderComparison !== ORDER_COMPARISON_EQUAL) return orderComparison;
|
|
4818
5397
|
return left.id.localeCompare(right.id);
|
|
4819
5398
|
}
|
|
4820
|
-
async function* readFilesystemSourceEntries(productDir, registry, includePath) {
|
|
5399
|
+
async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
|
|
4821
5400
|
yield* walkFilesystemDirectory({
|
|
4822
|
-
absolutePath:
|
|
5401
|
+
absolutePath: join13(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
|
|
4823
5402
|
relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
|
|
4824
5403
|
registry,
|
|
5404
|
+
schemaVersions,
|
|
4825
5405
|
includePath
|
|
4826
5406
|
});
|
|
4827
5407
|
}
|
|
@@ -4842,14 +5422,15 @@ async function* walkFilesystemDirectory(context) {
|
|
|
4842
5422
|
if (recordType === void 0) continue;
|
|
4843
5423
|
const sourceEntry = recognizeSpecTreeFilesystemEntry(
|
|
4844
5424
|
{ type: recordType, relativePath, parentId: context.parentId },
|
|
4845
|
-
context.registry
|
|
5425
|
+
{ registry: context.registry, schemaVersions: context.schemaVersions }
|
|
4846
5426
|
);
|
|
4847
5427
|
if (sourceEntry !== null) yield sourceEntry;
|
|
4848
|
-
if (entry.isDirectory() && shouldDescendIntoDirectory(
|
|
5428
|
+
if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
|
|
4849
5429
|
yield* walkFilesystemDirectory({
|
|
4850
|
-
absolutePath:
|
|
5430
|
+
absolutePath: join13(context.absolutePath, entry.name),
|
|
4851
5431
|
relativePath,
|
|
4852
5432
|
registry: context.registry,
|
|
5433
|
+
schemaVersions: context.schemaVersions,
|
|
4853
5434
|
includePath: context.includePath,
|
|
4854
5435
|
parentId: sourceEntry?.type === SPEC_TREE_ENTRY_TYPE.NODE ? sourceEntry.id : context.parentId
|
|
4855
5436
|
});
|
|
@@ -4907,14 +5488,8 @@ function segmentsEndWith(segments, suffix) {
|
|
|
4907
5488
|
const start = segments.length - suffix.length;
|
|
4908
5489
|
return suffix.every((value, index) => segments[start + index] === value);
|
|
4909
5490
|
}
|
|
4910
|
-
function shouldDescendIntoDirectory(
|
|
4911
|
-
|
|
4912
|
-
if (name === SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME) return true;
|
|
4913
|
-
return !isUnregisteredOrderedDirectory(name, registry);
|
|
4914
|
-
}
|
|
4915
|
-
function isUnregisteredOrderedDirectory(name, registry) {
|
|
4916
|
-
if (matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.NODE) !== null) return false;
|
|
4917
|
-
return parseOrderedSlug(name) !== null;
|
|
5491
|
+
function shouldDescendIntoDirectory(sourceEntry) {
|
|
5492
|
+
return sourceEntry === null || sourceEntry.type === SPEC_TREE_ENTRY_TYPE.NODE;
|
|
4918
5493
|
}
|
|
4919
5494
|
function sourceRefForRelativePath(relativePath) {
|
|
4920
5495
|
const path6 = joinSpecTreePath(SPEC_TREE_CONFIG.ROOT_DIRECTORY, relativePath);
|
|
@@ -4998,14 +5573,14 @@ function formatNextNode(node) {
|
|
|
4998
5573
|
|
|
4999
5574
|
// src/commands/testing/discovery.ts
|
|
5000
5575
|
import { readdir as readdir6 } from "fs/promises";
|
|
5001
|
-
import { join as
|
|
5576
|
+
import { join as join14, relative, sep } from "path";
|
|
5002
5577
|
var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
|
|
5003
5578
|
var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
|
|
5004
5579
|
var POSIX_SEPARATOR = "/";
|
|
5005
|
-
var
|
|
5580
|
+
var ERROR_CODE_NOT_FOUND2 = "ENOENT";
|
|
5006
5581
|
async function discoverTestFiles(productDir) {
|
|
5007
5582
|
const found = [];
|
|
5008
|
-
await collectTestFiles(
|
|
5583
|
+
await collectTestFiles(join14(productDir, SPEC_ROOT_DIRECTORY), false, found);
|
|
5009
5584
|
return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
|
|
5010
5585
|
}
|
|
5011
5586
|
async function collectTestFiles(directory, insideTestsDir, found) {
|
|
@@ -5013,11 +5588,11 @@ async function collectTestFiles(directory, insideTestsDir, found) {
|
|
|
5013
5588
|
try {
|
|
5014
5589
|
entries = await readdir6(directory, { withFileTypes: true });
|
|
5015
5590
|
} catch (error) {
|
|
5016
|
-
if (
|
|
5591
|
+
if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) return;
|
|
5017
5592
|
throw error;
|
|
5018
5593
|
}
|
|
5019
5594
|
for (const entry of entries) {
|
|
5020
|
-
const childPath =
|
|
5595
|
+
const childPath = join14(directory, entry.name);
|
|
5021
5596
|
if (entry.isDirectory()) {
|
|
5022
5597
|
await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
|
|
5023
5598
|
} else if (insideTestsDir && entry.isFile()) {
|
|
@@ -5026,14 +5601,14 @@ async function collectTestFiles(directory, insideTestsDir, found) {
|
|
|
5026
5601
|
}
|
|
5027
5602
|
}
|
|
5028
5603
|
function toPosixRelative(productDir, absolute) {
|
|
5029
|
-
return
|
|
5604
|
+
return relative(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
|
|
5030
5605
|
}
|
|
5031
5606
|
function compareAscii(left, right) {
|
|
5032
5607
|
if (left < right) return -1;
|
|
5033
5608
|
if (left > right) return 1;
|
|
5034
5609
|
return 0;
|
|
5035
5610
|
}
|
|
5036
|
-
function
|
|
5611
|
+
function hasErrorCode2(error, code) {
|
|
5037
5612
|
return typeof error === "object" && error !== null && error.code === code;
|
|
5038
5613
|
}
|
|
5039
5614
|
|
|
@@ -5041,7 +5616,7 @@ function hasErrorCode(error, code) {
|
|
|
5041
5616
|
var SUCCESS_EXIT_CODE = 0;
|
|
5042
5617
|
function aggregateTestExitCode(invocations) {
|
|
5043
5618
|
for (const invocation of invocations) {
|
|
5044
|
-
if (invocation.invoked && invocation.exitCode !==
|
|
5619
|
+
if (invocation.invoked && invocation.exitCode !== SUCCESS_EXIT_CODE) {
|
|
5045
5620
|
return invocation.exitCode;
|
|
5046
5621
|
}
|
|
5047
5622
|
}
|
|
@@ -5091,7 +5666,7 @@ async function runTests(options, deps) {
|
|
|
5091
5666
|
outcomes.push({
|
|
5092
5667
|
runnerId: group.language.name,
|
|
5093
5668
|
testPaths: group.testPaths,
|
|
5094
|
-
exitCode: invocation.exitCode
|
|
5669
|
+
exitCode: invocation.exitCode
|
|
5095
5670
|
});
|
|
5096
5671
|
}
|
|
5097
5672
|
}
|
|
@@ -5099,55 +5674,11 @@ async function runTests(options, deps) {
|
|
|
5099
5674
|
}
|
|
5100
5675
|
|
|
5101
5676
|
// src/commands/testing/run-command.ts
|
|
5102
|
-
import { join as
|
|
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
|
-
}
|
|
5677
|
+
import { join as join16 } from "path";
|
|
5149
5678
|
|
|
5150
5679
|
// src/testing/run-state.ts
|
|
5680
|
+
import { createHash as createHash4 } from "crypto";
|
|
5681
|
+
import { join as join15 } from "path";
|
|
5151
5682
|
var TEST_RUN_STATE_STATUS = {
|
|
5152
5683
|
PASSED: "passed",
|
|
5153
5684
|
FAILED: "failed",
|
|
@@ -5160,11 +5691,14 @@ var TESTING_RUN_STATE_INCOMPLETE_REASON = {
|
|
|
5160
5691
|
SHAPE_INVALID_STATE: "shape-invalid-state"
|
|
5161
5692
|
};
|
|
5162
5693
|
var TESTING_RUN_STATE_ERROR = {
|
|
5163
|
-
|
|
5164
|
-
|
|
5694
|
+
RUN_FILE_COLLISION_LIMIT: "testing run file collision limit exhausted",
|
|
5695
|
+
RUN_FILE_CREATE_FAILED: "testing run file create failed",
|
|
5165
5696
|
STATE_ALREADY_EXISTS: "testing run state already exists",
|
|
5166
5697
|
STATE_WRITE_FAILED: "testing run state write failed"
|
|
5167
5698
|
};
|
|
5699
|
+
var TESTING_RUN_STATE_ERROR_CODE = {
|
|
5700
|
+
NOT_FOUND: "ENOENT"
|
|
5701
|
+
};
|
|
5168
5702
|
var TEST_RUN_STATE_FIELDS = {
|
|
5169
5703
|
BRANCH_NAME: "branchName",
|
|
5170
5704
|
HEAD_SHA: "headSha",
|
|
@@ -5186,119 +5720,64 @@ var PRODUCT_INPUT_DIGEST_FIELDS = {
|
|
|
5186
5720
|
DESCRIPTOR_ID: "descriptorId",
|
|
5187
5721
|
DIGEST: "digest"
|
|
5188
5722
|
};
|
|
5189
|
-
var DEFAULT_TESTING_STORAGE = {
|
|
5190
|
-
spxDir: ".spx",
|
|
5191
|
-
localDir: "local",
|
|
5192
|
-
testingDir: "testing",
|
|
5193
|
-
runsDir: "runs",
|
|
5194
|
-
stateFile: "state.json"
|
|
5195
|
-
};
|
|
5196
5723
|
var SHA256_ALGORITHM2 = "sha256";
|
|
5197
|
-
var
|
|
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;
|
|
5724
|
+
var HEX_ENCODING3 = "hex";
|
|
5205
5725
|
var SEGMENT_SEPARATOR = "-";
|
|
5206
|
-
var
|
|
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
|
-
};
|
|
5726
|
+
var defaultFileSystem2 = defaultFileSystem;
|
|
5219
5727
|
function formatTestRunTimestamp(date) {
|
|
5220
|
-
return
|
|
5728
|
+
return formatRunTimestamp(date);
|
|
5221
5729
|
}
|
|
5222
|
-
function testingRunsDir(productDir
|
|
5223
|
-
|
|
5730
|
+
function testingRunsDir(productDir) {
|
|
5731
|
+
const worktreeScope = worktreeScopeDir(productDir);
|
|
5732
|
+
if (!worktreeScope.ok) throw new Error(worktreeScope.error);
|
|
5733
|
+
const result = runsDir(worktreeScope.value, STATE_STORE_DOMAIN.TEST);
|
|
5734
|
+
if (!result.ok) throw new Error(result.error);
|
|
5735
|
+
return result.value;
|
|
5224
5736
|
}
|
|
5225
|
-
async function
|
|
5226
|
-
const
|
|
5227
|
-
|
|
5228
|
-
const
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
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 };
|
|
5737
|
+
async function createTestRunFile(productDir, options = {}) {
|
|
5738
|
+
const worktreeScope = worktreeScopeDir(productDir);
|
|
5739
|
+
if (!worktreeScope.ok) return worktreeScope;
|
|
5740
|
+
const created = await createJsonlRunFile(worktreeScope.value, STATE_STORE_DOMAIN.TEST, options);
|
|
5741
|
+
if (!created.ok) return {
|
|
5742
|
+
ok: false,
|
|
5743
|
+
error: testingRunFileError(created.error)
|
|
5744
|
+
};
|
|
5745
|
+
return { ok: true, value: created.value };
|
|
5251
5746
|
}
|
|
5252
|
-
async function writeTerminalTestRunState(
|
|
5253
|
-
const
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
try {
|
|
5257
|
-
await fs7.readFile(statePath, UTF8_ENCODING2);
|
|
5747
|
+
async function writeTerminalTestRunState(runFilePath, state, options = {}) {
|
|
5748
|
+
const written = await writeJsonlRunRecord(runFilePath, testRunStateRecord(state), options);
|
|
5749
|
+
if (written.ok) return written;
|
|
5750
|
+
if (written.error === STATE_STORE_ERROR.RECORD_ALREADY_EXISTS) {
|
|
5258
5751
|
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
5752
|
}
|
|
5753
|
+
return {
|
|
5754
|
+
ok: false,
|
|
5755
|
+
error: testingWriteError(written.error)
|
|
5756
|
+
};
|
|
5275
5757
|
}
|
|
5276
5758
|
async function readTestingRuns(productDir, options = {}) {
|
|
5277
|
-
const fs7 = options.fs ??
|
|
5278
|
-
const
|
|
5279
|
-
const runsDir = testingRunsDir(productDir, storage);
|
|
5759
|
+
const fs7 = options.fs ?? defaultFileSystem2;
|
|
5760
|
+
const runsDir2 = testingRunsDir(productDir);
|
|
5280
5761
|
let entries;
|
|
5281
5762
|
try {
|
|
5282
|
-
entries = await fs7.readdir(
|
|
5763
|
+
entries = await fs7.readdir(runsDir2, { withFileTypes: true });
|
|
5283
5764
|
} catch (error) {
|
|
5284
|
-
if (
|
|
5765
|
+
if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
|
|
5285
5766
|
return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
|
|
5286
5767
|
}
|
|
5287
|
-
return { ok: false, error:
|
|
5768
|
+
return { ok: false, error: toErrorMessage2(error) };
|
|
5288
5769
|
}
|
|
5289
5770
|
const terminalRuns = [];
|
|
5290
5771
|
const incompleteRuns = [];
|
|
5291
|
-
for (const entry of entries.filter(
|
|
5292
|
-
const
|
|
5293
|
-
const
|
|
5294
|
-
const stateResult = await readTestRunStatePath(statePath, fs7);
|
|
5772
|
+
for (const entry of entries.filter(isTestRunFileEntry)) {
|
|
5773
|
+
const runFilePath = join15(runsDir2, entry.name);
|
|
5774
|
+
const stateResult = await readTestRunStatePath(runFilePath, fs7);
|
|
5295
5775
|
if (stateResult.ok) {
|
|
5296
|
-
terminalRuns.push({
|
|
5776
|
+
terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
|
|
5297
5777
|
} else {
|
|
5298
5778
|
incompleteRuns.push({
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
statePath,
|
|
5779
|
+
runFileName: entry.name,
|
|
5780
|
+
runFilePath,
|
|
5302
5781
|
reason: stateResult.reason,
|
|
5303
5782
|
...stateResult.error === void 0 ? {} : { error: stateResult.error }
|
|
5304
5783
|
});
|
|
@@ -5339,21 +5818,47 @@ function extractStalenessInputs(state) {
|
|
|
5339
5818
|
productInputDigests: state.productInputDigests
|
|
5340
5819
|
};
|
|
5341
5820
|
}
|
|
5342
|
-
|
|
5343
|
-
|
|
5821
|
+
function testRunStateRecord(state) {
|
|
5822
|
+
return {
|
|
5823
|
+
branchName: state.branchName,
|
|
5824
|
+
headSha: state.headSha,
|
|
5825
|
+
testingConfigDigest: state.testingConfigDigest,
|
|
5826
|
+
runnerOutcomes: state.runnerOutcomes.map((outcome) => ({
|
|
5827
|
+
runnerId: outcome.runnerId,
|
|
5828
|
+
testPaths: outcome.testPaths,
|
|
5829
|
+
exitCode: outcome.exitCode
|
|
5830
|
+
})),
|
|
5831
|
+
discoveredTestPathsDigest: state.discoveredTestPathsDigest,
|
|
5832
|
+
discoveredTestContentDigest: state.discoveredTestContentDigest,
|
|
5833
|
+
productInputDigests: state.productInputDigests.map((digest) => ({
|
|
5834
|
+
descriptorId: digest.descriptorId,
|
|
5835
|
+
digest: digest.digest
|
|
5836
|
+
})),
|
|
5837
|
+
startedAt: state.startedAt,
|
|
5838
|
+
completedAt: state.completedAt,
|
|
5839
|
+
status: state.status
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
5842
|
+
async function readTestRunStatePath(runFilePath, fs7) {
|
|
5843
|
+
let content;
|
|
5344
5844
|
try {
|
|
5345
|
-
|
|
5845
|
+
content = await fs7.readFile(runFilePath, "utf8");
|
|
5346
5846
|
} catch (error) {
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5847
|
+
return {
|
|
5848
|
+
ok: false,
|
|
5849
|
+
reason: hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND) ? TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE : TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR,
|
|
5850
|
+
error: toErrorMessage2(error)
|
|
5851
|
+
};
|
|
5852
|
+
}
|
|
5853
|
+
const latest = latestNonEmptyJsonlLine(content);
|
|
5854
|
+
if (latest === void 0) {
|
|
5855
|
+
return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
|
|
5351
5856
|
}
|
|
5352
5857
|
let parsed;
|
|
5353
5858
|
try {
|
|
5354
|
-
parsed = JSON.parse(
|
|
5355
|
-
} catch
|
|
5356
|
-
return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE
|
|
5859
|
+
parsed = JSON.parse(latest);
|
|
5860
|
+
} catch {
|
|
5861
|
+
return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
|
|
5357
5862
|
}
|
|
5358
5863
|
const validated = validateTestRunState(parsed);
|
|
5359
5864
|
if (!validated.ok) {
|
|
@@ -5375,8 +5880,8 @@ function validateTestRunState(value) {
|
|
|
5375
5880
|
if (!discoveredTestPathsDigest.ok) return discoveredTestPathsDigest;
|
|
5376
5881
|
const discoveredTestContentDigest = readString(value, TEST_RUN_STATE_FIELDS.DISCOVERED_TEST_CONTENT_DIGEST);
|
|
5377
5882
|
if (!discoveredTestContentDigest.ok) return discoveredTestContentDigest;
|
|
5378
|
-
const
|
|
5379
|
-
if (!
|
|
5883
|
+
const productInputDigests2 = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
|
|
5884
|
+
if (!productInputDigests2.ok) return productInputDigests2;
|
|
5380
5885
|
const startedAt = readString(value, TEST_RUN_STATE_FIELDS.STARTED_AT);
|
|
5381
5886
|
if (!startedAt.ok) return startedAt;
|
|
5382
5887
|
const completedAt = readString(value, TEST_RUN_STATE_FIELDS.COMPLETED_AT);
|
|
@@ -5392,7 +5897,7 @@ function validateTestRunState(value) {
|
|
|
5392
5897
|
runnerOutcomes: runnerOutcomes.value,
|
|
5393
5898
|
discoveredTestPathsDigest: discoveredTestPathsDigest.value,
|
|
5394
5899
|
discoveredTestContentDigest: discoveredTestContentDigest.value,
|
|
5395
|
-
productInputDigests:
|
|
5900
|
+
productInputDigests: productInputDigests2.value,
|
|
5396
5901
|
startedAt: startedAt.value,
|
|
5397
5902
|
completedAt: completedAt.value,
|
|
5398
5903
|
status: status.value
|
|
@@ -5456,7 +5961,7 @@ function compareTerminalRuns(left, right) {
|
|
|
5456
5961
|
if (completed !== 0) return completed;
|
|
5457
5962
|
const started = compareAsciiStrings(left.state.startedAt, right.state.startedAt);
|
|
5458
5963
|
if (started !== 0) return started;
|
|
5459
|
-
return compareAsciiStrings(left.
|
|
5964
|
+
return compareAsciiStrings(left.runFileName, right.runFileName);
|
|
5460
5965
|
}
|
|
5461
5966
|
function productInputDigestsEqual(left, right) {
|
|
5462
5967
|
return canonicalProductInputDigests(left) === canonicalProductInputDigests(right);
|
|
@@ -5465,37 +5970,50 @@ function canonicalProductInputDigests(digests) {
|
|
|
5465
5970
|
const normalized = [...digests].map((digest) => [digest.descriptorId, digest.digest]).sort((left, right) => compareAsciiStrings(left.join(SEGMENT_SEPARATOR), right.join(SEGMENT_SEPARATOR)));
|
|
5466
5971
|
return JSON.stringify(normalized);
|
|
5467
5972
|
}
|
|
5468
|
-
function
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5973
|
+
function isTestRunFileEntry(entry) {
|
|
5974
|
+
return entry.isFile() && isRunFileName(entry.name);
|
|
5975
|
+
}
|
|
5976
|
+
function testingRunFileError(error) {
|
|
5977
|
+
const stateStoreError = parseStateStoreError(error);
|
|
5978
|
+
if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT) {
|
|
5979
|
+
return TESTING_RUN_STATE_ERROR.RUN_FILE_COLLISION_LIMIT;
|
|
5980
|
+
}
|
|
5981
|
+
if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED) {
|
|
5982
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, stateStoreError.detail);
|
|
5983
|
+
}
|
|
5984
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, error);
|
|
5985
|
+
}
|
|
5986
|
+
function testingWriteError(error) {
|
|
5987
|
+
const stateStoreError = parseStateStoreError(error);
|
|
5988
|
+
if (stateStoreError?.code === STATE_STORE_ERROR.RECORD_WRITE_FAILED) {
|
|
5989
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, stateStoreError.detail);
|
|
5990
|
+
}
|
|
5991
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, error);
|
|
5472
5992
|
}
|
|
5473
|
-
function
|
|
5474
|
-
return
|
|
5993
|
+
function withDomainErrorDetail(domainError, detail) {
|
|
5994
|
+
return detail === void 0 ? domainError : `${domainError}: ${detail}`;
|
|
5475
5995
|
}
|
|
5476
5996
|
function isRecord4(value) {
|
|
5477
5997
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5478
5998
|
}
|
|
5479
5999
|
function sha256Hex(value) {
|
|
5480
|
-
return
|
|
6000
|
+
return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
|
|
5481
6001
|
}
|
|
5482
|
-
function
|
|
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) {
|
|
6002
|
+
function toErrorMessage2(error) {
|
|
5489
6003
|
return error instanceof Error ? error.message : String(error);
|
|
5490
6004
|
}
|
|
5491
6005
|
|
|
5492
6006
|
// src/commands/testing/run-command.ts
|
|
5493
6007
|
var NO_GIT_IDENTITY = "(unknown)";
|
|
5494
6008
|
var TEXT_ENCODING = "utf8";
|
|
5495
|
-
var
|
|
6009
|
+
var PRODUCT_INPUT_FIELDS = {
|
|
6010
|
+
PATH: "path",
|
|
6011
|
+
PRESENT: "present",
|
|
6012
|
+
CONTENT: "content"
|
|
6013
|
+
};
|
|
5496
6014
|
function resolveRecordingDependencies(deps) {
|
|
5497
6015
|
return {
|
|
5498
|
-
fs: deps.fs ??
|
|
6016
|
+
fs: deps.fs ?? defaultFileSystem2,
|
|
5499
6017
|
now: deps.now ?? (() => /* @__PURE__ */ new Date()),
|
|
5500
6018
|
git: deps.git
|
|
5501
6019
|
};
|
|
@@ -5518,28 +6036,74 @@ function coveredTestPaths(dispatch) {
|
|
|
5518
6036
|
async function readCoveredContents(productDir, paths, fs7) {
|
|
5519
6037
|
const entries = [];
|
|
5520
6038
|
for (const path6 of paths) {
|
|
5521
|
-
entries.push({ path: path6, content: await fs7.readFile(
|
|
6039
|
+
entries.push({ path: path6, content: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING) });
|
|
6040
|
+
}
|
|
6041
|
+
return entries;
|
|
6042
|
+
}
|
|
6043
|
+
async function readProductInputEntries(productDir, paths, fs7) {
|
|
6044
|
+
const entries = [];
|
|
6045
|
+
for (const path6 of [...paths].sort(compareAsciiStrings)) {
|
|
6046
|
+
try {
|
|
6047
|
+
entries.push({
|
|
6048
|
+
[PRODUCT_INPUT_FIELDS.PATH]: path6,
|
|
6049
|
+
[PRODUCT_INPUT_FIELDS.PRESENT]: true,
|
|
6050
|
+
[PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING)
|
|
6051
|
+
});
|
|
6052
|
+
} catch (error) {
|
|
6053
|
+
if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
|
|
6054
|
+
entries.push({
|
|
6055
|
+
[PRODUCT_INPUT_FIELDS.PATH]: path6,
|
|
6056
|
+
[PRODUCT_INPUT_FIELDS.PRESENT]: false
|
|
6057
|
+
});
|
|
6058
|
+
}
|
|
5522
6059
|
}
|
|
5523
6060
|
return entries;
|
|
5524
6061
|
}
|
|
6062
|
+
async function digestLanguageProductInputs(productDir, language, coveredPaths, fs7) {
|
|
6063
|
+
const entries = await readProductInputEntries(productDir, languageProductInputPaths(language, coveredPaths), fs7);
|
|
6064
|
+
const digest = digestDescriptorSection(entries, `${language.name} product inputs`);
|
|
6065
|
+
if (!digest.ok) {
|
|
6066
|
+
throw new Error(`failed to digest ${language.name} product inputs: ${digest.error}`);
|
|
6067
|
+
}
|
|
6068
|
+
return digest.value.sha256;
|
|
6069
|
+
}
|
|
6070
|
+
async function productInputDigests(productDir, registry, coveredPaths, fs7) {
|
|
6071
|
+
const digests = [];
|
|
6072
|
+
for (const language of registry.languages) {
|
|
6073
|
+
digests.push({
|
|
6074
|
+
descriptorId: language.name,
|
|
6075
|
+
digest: await digestLanguageProductInputs(productDir, language, coveredPaths, fs7)
|
|
6076
|
+
});
|
|
6077
|
+
}
|
|
6078
|
+
return digests;
|
|
6079
|
+
}
|
|
5525
6080
|
function deriveStatus(outcomes) {
|
|
5526
6081
|
const allPassed = outcomes.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE);
|
|
5527
6082
|
return allPassed ? TEST_RUN_STATE_STATUS.PASSED : TEST_RUN_STATE_STATUS.FAILED;
|
|
5528
6083
|
}
|
|
5529
|
-
async function currentStalenessInputs(productDir, coveredPaths, deps
|
|
5530
|
-
const fs7 = deps.fs ??
|
|
6084
|
+
async function currentStalenessInputs(productDir, coveredPaths, deps) {
|
|
6085
|
+
const fs7 = deps.fs ?? defaultFileSystem2;
|
|
5531
6086
|
const testingConfigDigest = deps.testingConfigDigest ?? (await resolveTestingConfig(productDir)).digest;
|
|
5532
6087
|
const contents = await readCoveredContents(productDir, coveredPaths, fs7);
|
|
5533
6088
|
return {
|
|
5534
6089
|
testingConfigDigest,
|
|
5535
6090
|
discoveredTestPathsDigest: digestTestPaths(coveredPaths),
|
|
5536
6091
|
discoveredTestContentDigest: digestTestContents(contents),
|
|
5537
|
-
productInputDigests:
|
|
6092
|
+
productInputDigests: await productInputDigests(productDir, deps.registry, coveredPaths, fs7)
|
|
5538
6093
|
};
|
|
5539
6094
|
}
|
|
5540
|
-
|
|
6095
|
+
function languageProductInputPaths(language, coveredPaths) {
|
|
6096
|
+
return [
|
|
6097
|
+
.../* @__PURE__ */ new Set([
|
|
6098
|
+
...language.productInputPaths,
|
|
6099
|
+
...language.coveredProductInputPaths?.(coveredPaths) ?? []
|
|
6100
|
+
])
|
|
6101
|
+
].sort(compareAsciiStrings);
|
|
6102
|
+
}
|
|
6103
|
+
async function recordRun(runFile, productDir, dispatch, recording, registry, testingConfigDigest) {
|
|
5541
6104
|
const staleness = await currentStalenessInputs(productDir, coveredTestPaths(dispatch), {
|
|
5542
6105
|
fs: recording.fs,
|
|
6106
|
+
registry,
|
|
5543
6107
|
testingConfigDigest
|
|
5544
6108
|
});
|
|
5545
6109
|
const branchName = await getCurrentBranch(productDir, recording.git) ?? NO_GIT_IDENTITY;
|
|
@@ -5552,43 +6116,43 @@ async function recordRun(directory, productDir, dispatch, recording, testingConf
|
|
|
5552
6116
|
discoveredTestPathsDigest: staleness.discoveredTestPathsDigest,
|
|
5553
6117
|
discoveredTestContentDigest: staleness.discoveredTestContentDigest,
|
|
5554
6118
|
productInputDigests: staleness.productInputDigests,
|
|
5555
|
-
startedAt:
|
|
6119
|
+
startedAt: runFile.startedAt,
|
|
5556
6120
|
completedAt: formatTestRunTimestamp(recording.now()),
|
|
5557
6121
|
status: deriveStatus(dispatch.outcomes)
|
|
5558
6122
|
};
|
|
5559
|
-
const written = await writeTerminalTestRunState(
|
|
6123
|
+
const written = await writeTerminalTestRunState(runFile.runFilePath, state, { fs: recording.fs });
|
|
5560
6124
|
if (!written.ok) {
|
|
5561
6125
|
throw new Error(`failed to record test run: ${written.error}`);
|
|
5562
6126
|
}
|
|
5563
6127
|
return state;
|
|
5564
6128
|
}
|
|
5565
|
-
async function
|
|
5566
|
-
const
|
|
5567
|
-
if (!
|
|
5568
|
-
throw new Error(`failed to create test run
|
|
6129
|
+
async function reserveRunFile(productDir, recording) {
|
|
6130
|
+
const runFile = await createTestRunFile(productDir, { fs: recording.fs, now: recording.now });
|
|
6131
|
+
if (!runFile.ok) {
|
|
6132
|
+
throw new Error(`failed to create test run file: ${runFile.error}`);
|
|
5569
6133
|
}
|
|
5570
|
-
return
|
|
6134
|
+
return runFile.value;
|
|
5571
6135
|
}
|
|
5572
6136
|
async function runTestsCommand(options, deps) {
|
|
5573
6137
|
const { config, digest } = await resolveTestingConfig(options.productDir);
|
|
5574
6138
|
const passingScope = options.passing ? config.passingScope : void 0;
|
|
5575
6139
|
const recording = resolveRecordingDependencies(deps);
|
|
5576
|
-
const
|
|
6140
|
+
const runFile = await reserveRunFile(options.productDir, recording);
|
|
5577
6141
|
const dispatch = await runTests(
|
|
5578
6142
|
{ productDir: options.productDir, registry: deps.registry, passingScope },
|
|
5579
6143
|
{ runnerDepsFor: deps.runnerDepsFor }
|
|
5580
6144
|
);
|
|
5581
|
-
const recorded = await recordRun(
|
|
6145
|
+
const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry, digest);
|
|
5582
6146
|
return { dispatch, recorded };
|
|
5583
6147
|
}
|
|
5584
6148
|
async function runNodeCommand(options, deps) {
|
|
5585
6149
|
const recording = resolveRecordingDependencies(deps);
|
|
5586
|
-
const
|
|
6150
|
+
const runFile = await reserveRunFile(options.productDir, recording);
|
|
5587
6151
|
const dispatch = await runTests(
|
|
5588
6152
|
{ productDir: options.productDir, registry: deps.registry, passingScope: { include: [options.nodePath] } },
|
|
5589
6153
|
{ runnerDepsFor: deps.runnerDepsFor }
|
|
5590
6154
|
);
|
|
5591
|
-
const recorded = await recordRun(
|
|
6155
|
+
const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry);
|
|
5592
6156
|
return { dispatch, recorded };
|
|
5593
6157
|
}
|
|
5594
6158
|
|
|
@@ -5596,19 +6160,37 @@ async function runNodeCommand(options, deps) {
|
|
|
5596
6160
|
var PATH_SEPARATOR = "/";
|
|
5597
6161
|
function createNodeOutcomeResolver(deps) {
|
|
5598
6162
|
let evidence;
|
|
6163
|
+
const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
|
|
5599
6164
|
const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
|
|
6165
|
+
const refreshEvidence = () => {
|
|
6166
|
+
evidence = loadResolverEvidence(deps);
|
|
6167
|
+
};
|
|
6168
|
+
const currentInputsFor = (coveredPaths) => {
|
|
6169
|
+
const cacheKey = coveredPathCollectionKey(coveredPaths);
|
|
6170
|
+
const cached = currentInputsByCoveredPaths.get(cacheKey);
|
|
6171
|
+
if (cached !== void 0) {
|
|
6172
|
+
return cached;
|
|
6173
|
+
}
|
|
6174
|
+
const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
|
|
6175
|
+
currentInputsByCoveredPaths.set(cacheKey, current);
|
|
6176
|
+
return current;
|
|
6177
|
+
};
|
|
5600
6178
|
return async (nodeId) => {
|
|
5601
6179
|
const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
|
|
5602
6180
|
const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
|
|
5603
6181
|
const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
|
|
5604
|
-
const usable = await usableRecordedOutcome(
|
|
6182
|
+
const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
|
|
5605
6183
|
if (usable !== void 0) {
|
|
5606
6184
|
return usable;
|
|
5607
6185
|
}
|
|
5608
6186
|
const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
|
|
6187
|
+
refreshEvidence();
|
|
5609
6188
|
return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
|
|
5610
6189
|
};
|
|
5611
6190
|
}
|
|
6191
|
+
function coveredPathCollectionKey(coveredPaths) {
|
|
6192
|
+
return JSON.stringify([...coveredPaths].sort());
|
|
6193
|
+
}
|
|
5612
6194
|
async function loadResolverEvidence(deps) {
|
|
5613
6195
|
const discoveredTestPaths = await discoverTestFiles(deps.productDir);
|
|
5614
6196
|
const runs = await readTestingRuns(deps.productDir, deps);
|
|
@@ -5618,7 +6200,7 @@ function filterNodeTestPaths(discoveredTestPaths, nodePath) {
|
|
|
5618
6200
|
const prefix = `${nodePath}${PATH_SEPARATOR}`;
|
|
5619
6201
|
return discoveredTestPaths.filter((path6) => path6.startsWith(prefix));
|
|
5620
6202
|
}
|
|
5621
|
-
async function usableRecordedOutcome(
|
|
6203
|
+
async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
|
|
5622
6204
|
const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
|
|
5623
6205
|
if (latest === void 0) {
|
|
5624
6206
|
return void 0;
|
|
@@ -5628,7 +6210,7 @@ async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, no
|
|
|
5628
6210
|
if (!runCoveredPaths.every((path6) => presentTestPaths.has(path6))) {
|
|
5629
6211
|
return void 0;
|
|
5630
6212
|
}
|
|
5631
|
-
const current = await
|
|
6213
|
+
const current = await currentInputsFor(runCoveredPaths);
|
|
5632
6214
|
const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
|
|
5633
6215
|
return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
|
|
5634
6216
|
}
|
|
@@ -5648,11 +6230,11 @@ function serializeNodeStatus(state) {
|
|
|
5648
6230
|
}
|
|
5649
6231
|
|
|
5650
6232
|
// src/lib/node-status/provider.ts
|
|
5651
|
-
import { join as
|
|
6233
|
+
import { join as join18 } from "path";
|
|
5652
6234
|
|
|
5653
6235
|
// src/lib/node-status/read.ts
|
|
5654
6236
|
import { readFileSync as readFileSync2 } from "fs";
|
|
5655
|
-
import { join as
|
|
6237
|
+
import { join as join17 } from "path";
|
|
5656
6238
|
var NODE_STATUS_FILENAME = "spx.status.json";
|
|
5657
6239
|
var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
|
|
5658
6240
|
function isNodeError2(error) {
|
|
@@ -5662,7 +6244,7 @@ function isSpecTreeNodeState(value) {
|
|
|
5662
6244
|
return typeof value === "string" && NODE_STATUS_VALUES.has(value);
|
|
5663
6245
|
}
|
|
5664
6246
|
function readNodeStatus(nodeDir) {
|
|
5665
|
-
const filePath =
|
|
6247
|
+
const filePath = join17(nodeDir, NODE_STATUS_FILENAME);
|
|
5666
6248
|
let content;
|
|
5667
6249
|
try {
|
|
5668
6250
|
content = readFileSync2(filePath, "utf8");
|
|
@@ -5684,7 +6266,7 @@ function readNodeStatus(nodeDir) {
|
|
|
5684
6266
|
|
|
5685
6267
|
// src/lib/node-status/provider.ts
|
|
5686
6268
|
function nodeDirectory(productDir, node) {
|
|
5687
|
-
return
|
|
6269
|
+
return join18(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
|
|
5688
6270
|
}
|
|
5689
6271
|
function createNodeStatusProvider(productDir) {
|
|
5690
6272
|
return {
|
|
@@ -5696,7 +6278,7 @@ function createNodeStatusProvider(productDir) {
|
|
|
5696
6278
|
|
|
5697
6279
|
// src/lib/node-status/update.ts
|
|
5698
6280
|
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
5699
|
-
import { dirname as
|
|
6281
|
+
import { dirname as dirname4, join as join19 } from "path";
|
|
5700
6282
|
var NODE_STATUS_TEXT_ENCODING = "utf8";
|
|
5701
6283
|
async function updateNodeStatus(options) {
|
|
5702
6284
|
const { productDir, resolveOutcome } = options;
|
|
@@ -5734,8 +6316,8 @@ function isNodeExcluded(ignoreReader, node) {
|
|
|
5734
6316
|
return ignoreReader.isUnderIgnoreSource(reference);
|
|
5735
6317
|
}
|
|
5736
6318
|
async function writeNodeStatus(productDir, nodeId, state) {
|
|
5737
|
-
const filePath =
|
|
5738
|
-
await mkdir4(
|
|
6319
|
+
const filePath = join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
|
|
6320
|
+
await mkdir4(dirname4(filePath), { recursive: true });
|
|
5739
6321
|
await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
|
|
5740
6322
|
}
|
|
5741
6323
|
|
|
@@ -5849,26 +6431,95 @@ function formatNodeLabel(node) {
|
|
|
5849
6431
|
}
|
|
5850
6432
|
|
|
5851
6433
|
// src/testing/languages/python.ts
|
|
5852
|
-
import { basename } from "path";
|
|
6434
|
+
import { basename as basename2, dirname as dirname5, join as join20 } from "path/posix";
|
|
6435
|
+
|
|
6436
|
+
// src/validation/discovery/language-finder.ts
|
|
6437
|
+
import fs5 from "fs";
|
|
6438
|
+
import path4 from "path";
|
|
6439
|
+
var TYPESCRIPT_MARKER = "tsconfig.json";
|
|
6440
|
+
var PYTHON_MARKER = "pyproject.toml";
|
|
6441
|
+
var ESLINT_CONFIG_FILES = [
|
|
6442
|
+
"eslint.config.ts",
|
|
6443
|
+
"eslint.config.js",
|
|
6444
|
+
"eslint.config.mjs",
|
|
6445
|
+
"eslint.config.cjs"
|
|
6446
|
+
];
|
|
6447
|
+
var ESLINT_PRODUCTION_CONFIG_FILES = [
|
|
6448
|
+
"eslint.config.production.ts",
|
|
6449
|
+
"eslint.config.production.js",
|
|
6450
|
+
"eslint.config.production.mjs",
|
|
6451
|
+
"eslint.config.production.cjs"
|
|
6452
|
+
];
|
|
6453
|
+
var defaultLanguageDetectionDeps = {
|
|
6454
|
+
existsSync: fs5.existsSync
|
|
6455
|
+
};
|
|
6456
|
+
function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
6457
|
+
const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
|
|
6458
|
+
if (!present) {
|
|
6459
|
+
return { present: false };
|
|
6460
|
+
}
|
|
6461
|
+
const eslintConfigFile = ESLINT_CONFIG_FILES.find(
|
|
6462
|
+
(configFile) => deps.existsSync(path4.join(projectRoot, configFile))
|
|
6463
|
+
);
|
|
6464
|
+
const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
|
|
6465
|
+
(configFile) => deps.existsSync(path4.join(projectRoot, configFile))
|
|
6466
|
+
);
|
|
6467
|
+
return { present: true, eslintConfigFile, productionEslintConfigFile };
|
|
6468
|
+
}
|
|
6469
|
+
function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
6470
|
+
const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
|
|
6471
|
+
return { present };
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6474
|
+
// src/testing/languages/python-pytest-contract.ts
|
|
6475
|
+
var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
|
|
6476
|
+
var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
|
|
6477
|
+
var UV_COMMAND = "uv";
|
|
6478
|
+
var PYTEST_INVOKE_ARGS = ["run", "--active", "pytest"];
|
|
6479
|
+
|
|
6480
|
+
// src/testing/languages/python.ts
|
|
5853
6481
|
var PYTHON_TESTING_LANGUAGE_NAME = "python";
|
|
6482
|
+
var PYTHON_PRODUCT_INPUT_PATH = {
|
|
6483
|
+
CONFTEST: "conftest.py",
|
|
6484
|
+
HIDDEN_PYTEST_INI: ".pytest.ini",
|
|
6485
|
+
HIDDEN_PYTEST_TOML: ".pytest.toml",
|
|
6486
|
+
PYPROJECT: "pyproject.toml",
|
|
6487
|
+
PYTEST_TOML: "pytest.toml",
|
|
6488
|
+
PYTEST_INI: "pytest.ini",
|
|
6489
|
+
SETUP_CFG: "setup.cfg",
|
|
6490
|
+
SETUP_PY: "setup.py",
|
|
6491
|
+
TOX_INI: "tox.ini",
|
|
6492
|
+
UV_LOCK: "uv.lock"
|
|
6493
|
+
};
|
|
6494
|
+
var PYTHON_PRODUCT_INPUT_PATHS = Object.values(PYTHON_PRODUCT_INPUT_PATH);
|
|
5854
6495
|
var PYTHON_TEST_FILE_PREFIX = "test_";
|
|
5855
6496
|
var PYTHON_TEST_FILE_EXTENSION = ".py";
|
|
5856
6497
|
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
6498
|
function matchesTestFile(filePath) {
|
|
5862
|
-
return
|
|
6499
|
+
return basename2(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
|
|
6500
|
+
}
|
|
6501
|
+
function coveredProductInputPaths(coveredTestPaths2) {
|
|
6502
|
+
const paths = /* @__PURE__ */ new Set();
|
|
6503
|
+
for (const testPath of coveredTestPaths2) {
|
|
6504
|
+
if (!matchesTestFile(testPath)) continue;
|
|
6505
|
+
let directory = dirname5(testPath);
|
|
6506
|
+
while (directory !== "." && directory.length > 0) {
|
|
6507
|
+
paths.add(join20(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
|
|
6508
|
+
const parent = dirname5(directory);
|
|
6509
|
+
if (parent === directory) break;
|
|
6510
|
+
directory = parent;
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
return [...paths].sort(compareAsciiStrings);
|
|
5863
6514
|
}
|
|
5864
6515
|
function excludeFlag(nodePath) {
|
|
5865
6516
|
return `${PYTHON_PYTEST_IGNORE_FLAG_PREFIX}${nodePath}${PYTHON_PYTEST_IGNORE_FLAG_SUFFIX}`;
|
|
5866
6517
|
}
|
|
5867
6518
|
function detect(projectRoot, deps) {
|
|
5868
|
-
return deps
|
|
6519
|
+
return deps?.isLanguagePresent?.(projectRoot) ?? detectPython(projectRoot).present;
|
|
5869
6520
|
}
|
|
5870
6521
|
async function runTests2(request, deps) {
|
|
5871
|
-
if (!
|
|
6522
|
+
if (!detect(request.projectRoot, deps)) {
|
|
5872
6523
|
return { invoked: false };
|
|
5873
6524
|
}
|
|
5874
6525
|
const args = [
|
|
@@ -5882,6 +6533,8 @@ async function runTests2(request, deps) {
|
|
|
5882
6533
|
var pythonTestingLanguage = {
|
|
5883
6534
|
name: PYTHON_TESTING_LANGUAGE_NAME,
|
|
5884
6535
|
testFilePatterns: PYTHON_TEST_FILE_PATTERNS,
|
|
6536
|
+
productInputPaths: PYTHON_PRODUCT_INPUT_PATHS,
|
|
6537
|
+
coveredProductInputPaths,
|
|
5885
6538
|
matchesTestFile,
|
|
5886
6539
|
excludeFlag,
|
|
5887
6540
|
detect,
|
|
@@ -5892,6 +6545,15 @@ var pythonTestingLanguage = {
|
|
|
5892
6545
|
var TYPESCRIPT_TESTING_LANGUAGE_NAME = "typescript";
|
|
5893
6546
|
var TYPESCRIPT_TEST_FILE_PATTERNS = ["*.test.ts", "*.test.tsx"];
|
|
5894
6547
|
var TYPESCRIPT_TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx"];
|
|
6548
|
+
var TYPESCRIPT_PRODUCT_INPUT_PATHS = [
|
|
6549
|
+
"package.json",
|
|
6550
|
+
"pnpm-lock.yaml",
|
|
6551
|
+
"tsconfig.json",
|
|
6552
|
+
"vitest.config.js",
|
|
6553
|
+
"vitest.config.mjs",
|
|
6554
|
+
"vitest.config.ts",
|
|
6555
|
+
"vitest.config.mts"
|
|
6556
|
+
];
|
|
5895
6557
|
var TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX = "--exclude=spx/";
|
|
5896
6558
|
var TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX = "/**";
|
|
5897
6559
|
var PACKAGE_MANAGER_COMMAND = "pnpm";
|
|
@@ -5904,10 +6566,10 @@ function excludeFlag2(nodePath) {
|
|
|
5904
6566
|
return `${TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX}${nodePath}${TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX}`;
|
|
5905
6567
|
}
|
|
5906
6568
|
function detect2(projectRoot, deps) {
|
|
5907
|
-
return deps
|
|
6569
|
+
return deps?.isLanguagePresent?.(projectRoot) ?? detectTypeScript(projectRoot).present;
|
|
5908
6570
|
}
|
|
5909
6571
|
async function runTests3(request, deps) {
|
|
5910
|
-
if (!
|
|
6572
|
+
if (!detect2(request.projectRoot, deps)) {
|
|
5911
6573
|
return { invoked: false };
|
|
5912
6574
|
}
|
|
5913
6575
|
const args = [
|
|
@@ -5923,6 +6585,7 @@ async function runTests3(request, deps) {
|
|
|
5923
6585
|
var typescriptTestingLanguage = {
|
|
5924
6586
|
name: TYPESCRIPT_TESTING_LANGUAGE_NAME,
|
|
5925
6587
|
testFilePatterns: TYPESCRIPT_TEST_FILE_PATTERNS,
|
|
6588
|
+
productInputPaths: TYPESCRIPT_PRODUCT_INPUT_PATHS,
|
|
5926
6589
|
matchesTestFile: matchesTestFile2,
|
|
5927
6590
|
excludeFlag: excludeFlag2,
|
|
5928
6591
|
detect: detect2,
|
|
@@ -6078,51 +6741,8 @@ function spawnManagedSubprocess(runner, command, args, options) {
|
|
|
6078
6741
|
});
|
|
6079
6742
|
}
|
|
6080
6743
|
|
|
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
6744
|
// src/interfaces/cli/testing-runner-deps.ts
|
|
6120
6745
|
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
6746
|
function createCommandRunner(productDir, outStream) {
|
|
6127
6747
|
return (command, args) => new Promise((resolveResult) => {
|
|
6128
6748
|
const child = spawnManagedSubprocess(lifecycleProcessRunner, command, args, {
|
|
@@ -6136,13 +6756,7 @@ function createCommandRunner(productDir, outStream) {
|
|
|
6136
6756
|
}
|
|
6137
6757
|
function createRunnerDepsFor(productDir, outStream = process.stdout) {
|
|
6138
6758
|
const runCommand = createCommandRunner(productDir, outStream);
|
|
6139
|
-
return (
|
|
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
|
-
};
|
|
6759
|
+
return () => ({ runCommand });
|
|
6146
6760
|
}
|
|
6147
6761
|
|
|
6148
6762
|
// src/interfaces/cli/spec.ts
|
|
@@ -6273,10 +6887,10 @@ var testingDomain = {
|
|
|
6273
6887
|
};
|
|
6274
6888
|
|
|
6275
6889
|
// src/commands/validation/markdown.ts
|
|
6276
|
-
import { relative as
|
|
6890
|
+
import { relative as relative3 } from "path";
|
|
6277
6891
|
|
|
6278
6892
|
// src/validation/config/path-filter.ts
|
|
6279
|
-
import { isAbsolute as isAbsolute2, relative as
|
|
6893
|
+
import { isAbsolute as isAbsolute2, relative as relative2 } from "path";
|
|
6280
6894
|
var PATH_PREFIX_SEPARATOR = "/";
|
|
6281
6895
|
function hasEffectiveValidationPathMetadata(filter) {
|
|
6282
6896
|
return "hasIncludeFilter" in filter && typeof filter.hasIncludeFilter === "boolean" && "noMatchingIncludes" in filter && typeof filter.noMatchingIncludes === "boolean";
|
|
@@ -6296,7 +6910,7 @@ function nonEmpty(values) {
|
|
|
6296
6910
|
return values?.filter((value) => value.length > 0) ?? [];
|
|
6297
6911
|
}
|
|
6298
6912
|
function toProjectRelativeValidationPath(projectRoot, path6) {
|
|
6299
|
-
return isAbsolute2(path6) ?
|
|
6913
|
+
return isAbsolute2(path6) ? relative2(projectRoot, path6) : path6;
|
|
6300
6914
|
}
|
|
6301
6915
|
function intersectIncludes(baseInclude, toolInclude) {
|
|
6302
6916
|
const base = nonEmpty(baseInclude);
|
|
@@ -6371,8 +6985,8 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
|
|
|
6371
6985
|
}
|
|
6372
6986
|
|
|
6373
6987
|
// src/validation/steps/markdown.ts
|
|
6374
|
-
import { existsSync
|
|
6375
|
-
import { basename as
|
|
6988
|
+
import { existsSync, statSync } from "fs";
|
|
6989
|
+
import { basename as basename3, dirname as dirname6, join as join21, relative as pathRelative } from "path";
|
|
6376
6990
|
import { main as markdownlintMain } from "markdownlint-cli2";
|
|
6377
6991
|
import relativeLinksRule from "markdownlint-rule-relative-links";
|
|
6378
6992
|
var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
|
|
@@ -6411,7 +7025,7 @@ function buildMarkdownlintConfig(directoryName) {
|
|
|
6411
7025
|
};
|
|
6412
7026
|
}
|
|
6413
7027
|
function getDefaultDirectories(projectRoot) {
|
|
6414
|
-
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) =>
|
|
7028
|
+
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join21(projectRoot, name)).filter((dir) => existsSync(dir));
|
|
6415
7029
|
}
|
|
6416
7030
|
function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
|
|
6417
7031
|
if (isExistingDirectory(path6, deps)) {
|
|
@@ -6470,7 +7084,7 @@ async function validateMarkdown(options) {
|
|
|
6470
7084
|
async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
6471
7085
|
const errors = [];
|
|
6472
7086
|
const directory = targetDirectory(target);
|
|
6473
|
-
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [
|
|
7087
|
+
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename3(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
|
|
6474
7088
|
const { customRules, ...markdownlintConfig } = config;
|
|
6475
7089
|
const optionsOverride = {
|
|
6476
7090
|
config: {
|
|
@@ -6494,7 +7108,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
6494
7108
|
if (parsed) {
|
|
6495
7109
|
errors.push({
|
|
6496
7110
|
...parsed,
|
|
6497
|
-
file:
|
|
7111
|
+
file: join21(directory, parsed.file)
|
|
6498
7112
|
});
|
|
6499
7113
|
}
|
|
6500
7114
|
}
|
|
@@ -6502,7 +7116,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
6502
7116
|
return errors;
|
|
6503
7117
|
}
|
|
6504
7118
|
function targetDirectory(target) {
|
|
6505
|
-
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ?
|
|
7119
|
+
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname6(target.path) : target.path;
|
|
6506
7120
|
}
|
|
6507
7121
|
function hasMarkdownExtension(path6) {
|
|
6508
7122
|
const lastDot = path6.lastIndexOf(".");
|
|
@@ -6530,7 +7144,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
|
|
|
6530
7144
|
return rootSegment;
|
|
6531
7145
|
}
|
|
6532
7146
|
}
|
|
6533
|
-
return
|
|
7147
|
+
return basename3(directory);
|
|
6534
7148
|
}
|
|
6535
7149
|
|
|
6536
7150
|
// src/commands/validation/messages.ts
|
|
@@ -6606,7 +7220,7 @@ async function markdownCommand(options) {
|
|
|
6606
7220
|
path: path6
|
|
6607
7221
|
}));
|
|
6608
7222
|
const targets = unfilteredTargets.filter(
|
|
6609
|
-
(target) => pathPassesValidationFilter(
|
|
7223
|
+
(target) => pathPassesValidationFilter(relative3(cwd, target.path), pathFilter)
|
|
6610
7224
|
);
|
|
6611
7225
|
const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
|
|
6612
7226
|
const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
|
|
@@ -7515,8 +8129,8 @@ var ParseErrorCode;
|
|
|
7515
8129
|
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
7516
8130
|
|
|
7517
8131
|
// src/validation/config/scope.ts
|
|
7518
|
-
import { existsSync as
|
|
7519
|
-
import { isAbsolute as isAbsolute3, join as
|
|
8132
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
8133
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
7520
8134
|
var TSCONFIG_FILES = {
|
|
7521
8135
|
full: "tsconfig.json",
|
|
7522
8136
|
production: "tsconfig.production.json"
|
|
@@ -7526,11 +8140,11 @@ var GLOB_MARKER = "*";
|
|
|
7526
8140
|
var HIDDEN_PATH_PREFIX = ".";
|
|
7527
8141
|
var defaultScopeDeps = {
|
|
7528
8142
|
readFileSync: readFileSync3,
|
|
7529
|
-
existsSync:
|
|
8143
|
+
existsSync: existsSync2,
|
|
7530
8144
|
readdirSync
|
|
7531
8145
|
};
|
|
7532
8146
|
function resolveProjectPath(projectRoot, path6) {
|
|
7533
|
-
return isAbsolute3(path6) ? path6 :
|
|
8147
|
+
return isAbsolute3(path6) ? path6 : join22(projectRoot, path6);
|
|
7534
8148
|
}
|
|
7535
8149
|
function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
|
|
7536
8150
|
try {
|
|
@@ -7574,7 +8188,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
|
|
|
7574
8188
|
if (hasDirectTsFiles) return true;
|
|
7575
8189
|
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
7576
8190
|
for (const subdir of subdirs.slice(0, 5)) {
|
|
7577
|
-
if (hasTypeScriptFilesRecursive(
|
|
8191
|
+
if (hasTypeScriptFilesRecursive(join22(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
7578
8192
|
return true;
|
|
7579
8193
|
}
|
|
7580
8194
|
}
|
|
@@ -7597,7 +8211,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
|
|
|
7597
8211
|
});
|
|
7598
8212
|
if (!isExcluded) {
|
|
7599
8213
|
try {
|
|
7600
|
-
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(
|
|
8214
|
+
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join22(projectRoot, dir), 2, deps);
|
|
7601
8215
|
if (hasTypeScriptFiles) {
|
|
7602
8216
|
directories.add(dir);
|
|
7603
8217
|
}
|
|
@@ -7628,13 +8242,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
|
|
|
7628
8242
|
function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
|
|
7629
8243
|
return patterns.filter((pattern) => {
|
|
7630
8244
|
const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
|
|
7631
|
-
return topLevelDir === null || deps.existsSync(
|
|
8245
|
+
return topLevelDir === null || deps.existsSync(join22(projectRoot, topLevelDir));
|
|
7632
8246
|
}).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
|
|
7633
8247
|
}
|
|
7634
8248
|
function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
|
|
7635
8249
|
const config = resolveTypeScriptConfig(scope, projectRoot, deps);
|
|
7636
8250
|
const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
|
|
7637
|
-
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(
|
|
8251
|
+
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join22(projectRoot, dir)));
|
|
7638
8252
|
return existingDirectories;
|
|
7639
8253
|
}
|
|
7640
8254
|
function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
@@ -7758,7 +8372,7 @@ function formatSkipMessage(stepName, result) {
|
|
|
7758
8372
|
|
|
7759
8373
|
// src/validation/steps/circular.ts
|
|
7760
8374
|
import madge from "madge";
|
|
7761
|
-
import { join as
|
|
8375
|
+
import { join as join23 } from "path";
|
|
7762
8376
|
var CIRCULAR_DEPS_KEYS = {
|
|
7763
8377
|
MADGE: "madge"
|
|
7764
8378
|
};
|
|
@@ -7767,11 +8381,11 @@ var defaultCircularDeps = {
|
|
|
7767
8381
|
};
|
|
7768
8382
|
async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
|
|
7769
8383
|
try {
|
|
7770
|
-
const analyzeDirectories = typescriptScope.directories.map((directory) =>
|
|
8384
|
+
const analyzeDirectories = typescriptScope.directories.map((directory) => join23(projectRoot, directory));
|
|
7771
8385
|
if (analyzeDirectories.length === 0) {
|
|
7772
8386
|
return { success: true };
|
|
7773
8387
|
}
|
|
7774
|
-
const tsConfigFile =
|
|
8388
|
+
const tsConfigFile = join23(projectRoot, TSCONFIG_FILES[scope]);
|
|
7775
8389
|
const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
|
|
7776
8390
|
const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
|
|
7777
8391
|
const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -7851,10 +8465,10 @@ ${cycles}`;
|
|
|
7851
8465
|
}
|
|
7852
8466
|
|
|
7853
8467
|
// src/validation/steps/knip.ts
|
|
7854
|
-
import { existsSync as
|
|
8468
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7855
8469
|
import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
|
|
7856
8470
|
import { tmpdir } from "os";
|
|
7857
|
-
import { isAbsolute as isAbsolute4, join as
|
|
8471
|
+
import { isAbsolute as isAbsolute4, join as join24 } from "path";
|
|
7858
8472
|
var defaultKnipProcessRunner = lifecycleProcessRunner;
|
|
7859
8473
|
var KNIP_COMMAND_TOKENS = {
|
|
7860
8474
|
COMMAND: "knip",
|
|
@@ -7862,7 +8476,7 @@ var KNIP_COMMAND_TOKENS = {
|
|
|
7862
8476
|
USE_TSCONFIG_FILES_FLAG: "--use-tsconfig-files"
|
|
7863
8477
|
};
|
|
7864
8478
|
var defaultKnipDeps = {
|
|
7865
|
-
existsSync:
|
|
8479
|
+
existsSync: existsSync3,
|
|
7866
8480
|
mkdtemp,
|
|
7867
8481
|
rm,
|
|
7868
8482
|
writeFile: writeFile3
|
|
@@ -7882,7 +8496,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
|
|
|
7882
8496
|
}
|
|
7883
8497
|
async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
7884
8498
|
const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
|
|
7885
|
-
const localBin =
|
|
8499
|
+
const localBin = join24(projectRoot, "node_modules", ".bin", "knip");
|
|
7886
8500
|
const binary = deps.existsSync(localBin) ? localBin : "npx";
|
|
7887
8501
|
const baseArgs = scopedTsconfig === void 0 ? [] : [
|
|
7888
8502
|
KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
|
|
@@ -7912,13 +8526,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
7912
8526
|
knipProcess.stderr?.on("data", (data) => {
|
|
7913
8527
|
knipError += data.toString();
|
|
7914
8528
|
});
|
|
7915
|
-
return new Promise((
|
|
8529
|
+
return new Promise((resolve5) => {
|
|
7916
8530
|
const resolveAfterCleanup = (result) => {
|
|
7917
8531
|
if (resultResolved) {
|
|
7918
8532
|
return;
|
|
7919
8533
|
}
|
|
7920
8534
|
resultResolved = true;
|
|
7921
|
-
void cleanupOnce().finally(() =>
|
|
8535
|
+
void cleanupOnce().finally(() => resolve5(result));
|
|
7922
8536
|
};
|
|
7923
8537
|
knipProcess.on("close", (code) => {
|
|
7924
8538
|
if (code === 0) {
|
|
@@ -7937,12 +8551,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
7937
8551
|
});
|
|
7938
8552
|
}
|
|
7939
8553
|
async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
|
|
7940
|
-
const tempDir = await deps.mkdtemp(
|
|
7941
|
-
const configPath =
|
|
7942
|
-
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern :
|
|
8554
|
+
const tempDir = await deps.mkdtemp(join24(tmpdir(), "validate-knip-"));
|
|
8555
|
+
const configPath = join24(tempDir, TSCONFIG_FILES.full);
|
|
8556
|
+
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join24(projectRoot, pattern);
|
|
7943
8557
|
const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
|
|
7944
8558
|
const config = {
|
|
7945
|
-
extends:
|
|
8559
|
+
extends: join24(projectRoot, TSCONFIG_FILES.full),
|
|
7946
8560
|
include: project.map(toProjectPathPattern),
|
|
7947
8561
|
exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
|
|
7948
8562
|
};
|
|
@@ -7991,13 +8605,13 @@ async function knipCommand(options) {
|
|
|
7991
8605
|
}
|
|
7992
8606
|
|
|
7993
8607
|
// src/validation/steps/eslint.ts
|
|
7994
|
-
import { existsSync as
|
|
7995
|
-
import { join as
|
|
8608
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8609
|
+
import { join as join26 } from "path";
|
|
7996
8610
|
|
|
7997
8611
|
// src/validation/lint-policy.ts
|
|
7998
8612
|
import { execFileSync } from "child_process";
|
|
7999
|
-
import { existsSync as
|
|
8000
|
-
import { join as
|
|
8613
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
8614
|
+
import { join as join25 } from "path";
|
|
8001
8615
|
|
|
8002
8616
|
// src/validation/lint-policy-constants.ts
|
|
8003
8617
|
var LINT_POLICY_MANIFESTS = {
|
|
@@ -8038,29 +8652,30 @@ var TEST_LINT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NOD
|
|
|
8038
8652
|
var TEST_LINT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NODES.key;
|
|
8039
8653
|
var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.file;
|
|
8040
8654
|
var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.key;
|
|
8041
|
-
var SPEC_TREE_ROOT =
|
|
8042
|
-
var SPEC_TREE_NODE_SUFFIX_PATTERN = /\.(enabler|outcome)$/;
|
|
8043
|
-
var DEPRECATED_SPEC_NODE_SUFFIX_PATTERN = /\.(capability|feature|story)$/;
|
|
8655
|
+
var SPEC_TREE_ROOT = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
|
|
8044
8656
|
var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS.LOCAL_MAIN];
|
|
8657
|
+
function isSpecTreeNodePath(entry) {
|
|
8658
|
+
return NODE_SUFFIXES.some((suffix) => entry.endsWith(suffix));
|
|
8659
|
+
}
|
|
8045
8660
|
function readManifest(productDir, file, key) {
|
|
8046
8661
|
return parseLintPolicyManifest(
|
|
8047
|
-
readFileSync4(
|
|
8662
|
+
readFileSync4(join25(productDir, file), "utf-8"),
|
|
8048
8663
|
file,
|
|
8049
8664
|
key
|
|
8050
8665
|
);
|
|
8051
8666
|
}
|
|
8052
8667
|
function manifestExists(productDir, file) {
|
|
8053
|
-
return
|
|
8668
|
+
return existsSync4(join25(productDir, file));
|
|
8054
8669
|
}
|
|
8055
8670
|
function findDeprecatedSpecNodePath(productDir) {
|
|
8056
8671
|
function visit2(relativeDirectory) {
|
|
8057
|
-
const absoluteDirectory =
|
|
8672
|
+
const absoluteDirectory = join25(productDir, relativeDirectory);
|
|
8058
8673
|
for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
|
|
8059
8674
|
if (!entry.isDirectory()) {
|
|
8060
8675
|
continue;
|
|
8061
8676
|
}
|
|
8062
8677
|
const childPath = `${relativeDirectory}/${entry.name}`;
|
|
8063
|
-
if (
|
|
8678
|
+
if (SPEC_TREE_SUPERSEDED_NODE_SUFFIXES.some((suffix) => entry.name.endsWith(suffix))) {
|
|
8064
8679
|
return childPath;
|
|
8065
8680
|
}
|
|
8066
8681
|
const nestedDeprecatedPath = visit2(childPath);
|
|
@@ -8070,13 +8685,13 @@ function findDeprecatedSpecNodePath(productDir) {
|
|
|
8070
8685
|
}
|
|
8071
8686
|
return void 0;
|
|
8072
8687
|
}
|
|
8073
|
-
const specTreeRootPath =
|
|
8074
|
-
if (!
|
|
8688
|
+
const specTreeRootPath = join25(productDir, SPEC_TREE_ROOT);
|
|
8689
|
+
if (!existsSync4(specTreeRootPath)) {
|
|
8075
8690
|
return void 0;
|
|
8076
8691
|
}
|
|
8077
8692
|
return visit2(SPEC_TREE_ROOT);
|
|
8078
8693
|
}
|
|
8079
|
-
function assertManifestEntries(productDir, file, entries,
|
|
8694
|
+
function assertManifestEntries(productDir, file, entries, suffixPredicate, suffixDescription) {
|
|
8080
8695
|
const duplicates = entries.filter((entry, index) => entries.indexOf(entry) !== index);
|
|
8081
8696
|
if (duplicates.length > 0) {
|
|
8082
8697
|
throw new Error(
|
|
@@ -8093,11 +8708,11 @@ function assertManifestEntries(productDir, file, entries, suffixPattern, suffixD
|
|
|
8093
8708
|
if (entry.includes("..")) {
|
|
8094
8709
|
throw new Error(`${file} entry must not contain '..': ${entry}`);
|
|
8095
8710
|
}
|
|
8096
|
-
if (!
|
|
8711
|
+
if (!suffixPredicate(entry)) {
|
|
8097
8712
|
throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
|
|
8098
8713
|
}
|
|
8099
|
-
const absoluteEntry =
|
|
8100
|
-
if (!
|
|
8714
|
+
const absoluteEntry = join25(productDir, entry);
|
|
8715
|
+
if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
|
|
8101
8716
|
throw new Error(`${file} entry does not exist as a directory: ${entry}`);
|
|
8102
8717
|
}
|
|
8103
8718
|
}
|
|
@@ -8186,7 +8801,7 @@ function validateTestLintDebtNodeManifest(productDir, entries) {
|
|
|
8186
8801
|
productDir,
|
|
8187
8802
|
TEST_LINT_DEBT_NODE_MANIFEST_FILE,
|
|
8188
8803
|
entries,
|
|
8189
|
-
|
|
8804
|
+
isSpecTreeNodePath,
|
|
8190
8805
|
"be a Spec Tree node path"
|
|
8191
8806
|
);
|
|
8192
8807
|
assertManifestDoesNotGrow(
|
|
@@ -8201,7 +8816,7 @@ function validateTestOwnedConstantDebtNodeManifest(productDir, entries) {
|
|
|
8201
8816
|
productDir,
|
|
8202
8817
|
TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE,
|
|
8203
8818
|
entries,
|
|
8204
|
-
|
|
8819
|
+
isSpecTreeNodePath,
|
|
8205
8820
|
"be a Spec Tree node path"
|
|
8206
8821
|
);
|
|
8207
8822
|
assertManifestDoesNotGrow(
|
|
@@ -8355,9 +8970,9 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8355
8970
|
scope,
|
|
8356
8971
|
scopeConfig: context.scopeConfig
|
|
8357
8972
|
});
|
|
8358
|
-
return new Promise((
|
|
8359
|
-
const localBin =
|
|
8360
|
-
const binary =
|
|
8973
|
+
return new Promise((resolve5) => {
|
|
8974
|
+
const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
|
|
8975
|
+
const binary = existsSync5(localBin) ? localBin : "npx";
|
|
8361
8976
|
const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
|
|
8362
8977
|
const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
|
|
8363
8978
|
cwd: projectRoot
|
|
@@ -8365,13 +8980,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8365
8980
|
forwardValidationSubprocessOutput(eslintProcess, outputStreams);
|
|
8366
8981
|
eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
8367
8982
|
if (code === 0) {
|
|
8368
|
-
|
|
8983
|
+
resolve5({ success: true });
|
|
8369
8984
|
} else {
|
|
8370
|
-
|
|
8985
|
+
resolve5({ success: false, error: `ESLint exited with code ${code}` });
|
|
8371
8986
|
}
|
|
8372
8987
|
});
|
|
8373
8988
|
eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
8374
|
-
|
|
8989
|
+
resolve5({ success: false, error: error.message });
|
|
8375
8990
|
});
|
|
8376
8991
|
});
|
|
8377
8992
|
}
|
|
@@ -8458,7 +9073,7 @@ async function lintCommand(options) {
|
|
|
8458
9073
|
|
|
8459
9074
|
// src/validation/literal/index.ts
|
|
8460
9075
|
import { readFile as readFile8 } from "fs/promises";
|
|
8461
|
-
import { isAbsolute as isAbsolute5, relative as
|
|
9076
|
+
import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve4 } from "path";
|
|
8462
9077
|
|
|
8463
9078
|
// src/lib/file-inclusion/predicates/ignore-source.ts
|
|
8464
9079
|
var IGNORE_SOURCE_LAYER = "ignore-source";
|
|
@@ -8493,7 +9108,7 @@ var ignoreSourceLayer = makeLayer(
|
|
|
8493
9108
|
|
|
8494
9109
|
// src/lib/file-inclusion/pipeline.ts
|
|
8495
9110
|
import { readdir as readdir7 } from "fs/promises";
|
|
8496
|
-
import { join as
|
|
9111
|
+
import { join as join27, relative as relative4, sep as sep2 } from "path";
|
|
8497
9112
|
var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
|
|
8498
9113
|
function isNodeError3(err) {
|
|
8499
9114
|
return err instanceof Error && "code" in err;
|
|
@@ -8511,11 +9126,11 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
|
|
|
8511
9126
|
for (const entry of dirEntries) {
|
|
8512
9127
|
if (entry.isDirectory()) {
|
|
8513
9128
|
if (artifactDirs.has(entry.name)) continue;
|
|
8514
|
-
const absolutePath =
|
|
9129
|
+
const absolutePath = join27(absoluteDir, entry.name);
|
|
8515
9130
|
await collectPaths(absolutePath, projectRoot, result, artifactDirs);
|
|
8516
9131
|
} else if (entry.isFile()) {
|
|
8517
|
-
const absolutePath =
|
|
8518
|
-
const rel =
|
|
9132
|
+
const absolutePath = join27(absoluteDir, entry.name);
|
|
9133
|
+
const rel = relative4(projectRoot, absolutePath);
|
|
8519
9134
|
result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
|
|
8520
9135
|
}
|
|
8521
9136
|
}
|
|
@@ -8921,8 +9536,8 @@ async function validateLiteralReuse(input) {
|
|
|
8921
9536
|
const config = input.config ?? literalConfigDescriptor.defaults;
|
|
8922
9537
|
const request = input.files ? {
|
|
8923
9538
|
explicit: input.files.map((f) => {
|
|
8924
|
-
const abs = isAbsolute5(f) ? f :
|
|
8925
|
-
return
|
|
9539
|
+
const abs = isAbsolute5(f) ? f : resolve4(input.productDir, f);
|
|
9540
|
+
return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
8926
9541
|
})
|
|
8927
9542
|
} : { walkRoot: input.productDir };
|
|
8928
9543
|
const scope = await runPipeline(
|
|
@@ -8933,7 +9548,7 @@ async function validateLiteralReuse(input) {
|
|
|
8933
9548
|
EMPTY_IGNORE_READER
|
|
8934
9549
|
);
|
|
8935
9550
|
const filtered = applyPathFilter2(scope.included, input.pathConfig);
|
|
8936
|
-
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) =>
|
|
9551
|
+
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve4(input.productDir, entry.path));
|
|
8937
9552
|
const collectOptions = {
|
|
8938
9553
|
visitorKeys: defaultVisitorKeys,
|
|
8939
9554
|
minStringLength: config.minStringLength,
|
|
@@ -8943,7 +9558,7 @@ async function validateLiteralReuse(input) {
|
|
|
8943
9558
|
const testOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
8944
9559
|
const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
8945
9560
|
for (const abs of candidateFiles) {
|
|
8946
|
-
const rel =
|
|
9561
|
+
const rel = relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
8947
9562
|
const content = await readSafe(abs);
|
|
8948
9563
|
if (content === null) continue;
|
|
8949
9564
|
const occurrences = collectLiterals(content, rel, collectOptions);
|
|
@@ -9172,24 +9787,24 @@ function formatLoc(loc) {
|
|
|
9172
9787
|
}
|
|
9173
9788
|
|
|
9174
9789
|
// src/validation/steps/typescript.ts
|
|
9175
|
-
import { existsSync as
|
|
9790
|
+
import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
9176
9791
|
import { mkdtemp as mkdtemp2 } from "fs/promises";
|
|
9177
|
-
import { isAbsolute as isAbsolute6, join as
|
|
9792
|
+
import { isAbsolute as isAbsolute6, join as join28 } from "path";
|
|
9178
9793
|
var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
|
|
9179
9794
|
var defaultTypeScriptDeps = {
|
|
9180
9795
|
mkdtemp: mkdtemp2,
|
|
9181
9796
|
mkdirSync,
|
|
9182
9797
|
writeFileSync,
|
|
9183
9798
|
rmSync,
|
|
9184
|
-
existsSync:
|
|
9799
|
+
existsSync: existsSync6
|
|
9185
9800
|
};
|
|
9186
9801
|
var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
|
|
9187
9802
|
var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
|
|
9188
9803
|
var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
|
|
9189
9804
|
async function createTemporaryTsconfigDir(projectRoot, deps) {
|
|
9190
|
-
const parent =
|
|
9805
|
+
const parent = join28(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
|
|
9191
9806
|
deps.mkdirSync(parent, { recursive: true });
|
|
9192
|
-
return deps.mkdtemp(
|
|
9807
|
+
return deps.mkdtemp(join28(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
|
|
9193
9808
|
}
|
|
9194
9809
|
function buildTypeScriptArgs(context) {
|
|
9195
9810
|
const { scope, configFile } = context;
|
|
@@ -9197,11 +9812,11 @@ function buildTypeScriptArgs(context) {
|
|
|
9197
9812
|
}
|
|
9198
9813
|
async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
|
|
9199
9814
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9200
|
-
const configPath =
|
|
9815
|
+
const configPath = join28(tempDir, "tsconfig.json");
|
|
9201
9816
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9202
|
-
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file :
|
|
9817
|
+
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join28(projectRoot, file));
|
|
9203
9818
|
const tempConfig = {
|
|
9204
|
-
extends:
|
|
9819
|
+
extends: join28(projectRoot, baseConfigFile),
|
|
9205
9820
|
files: absoluteFiles,
|
|
9206
9821
|
include: [],
|
|
9207
9822
|
exclude: [],
|
|
@@ -9218,11 +9833,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
|
|
|
9218
9833
|
}
|
|
9219
9834
|
async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
|
|
9220
9835
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9221
|
-
const configPath =
|
|
9836
|
+
const configPath = join28(tempDir, "tsconfig.json");
|
|
9222
9837
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9223
|
-
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern :
|
|
9838
|
+
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join28(projectRoot, pattern);
|
|
9224
9839
|
const tempConfig = {
|
|
9225
|
-
extends:
|
|
9840
|
+
extends: join28(projectRoot, baseConfigFile),
|
|
9226
9841
|
include: scopeConfig.filePatterns.map(toProjectPathPattern),
|
|
9227
9842
|
exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
|
|
9228
9843
|
compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
|
|
@@ -9249,8 +9864,8 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9249
9864
|
if (files && files.length > 0) {
|
|
9250
9865
|
const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
|
|
9251
9866
|
try {
|
|
9252
|
-
return await new Promise((
|
|
9253
|
-
const tscBin =
|
|
9867
|
+
return await new Promise((resolve5) => {
|
|
9868
|
+
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9254
9869
|
const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9255
9870
|
const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9256
9871
|
const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
|
|
@@ -9260,14 +9875,14 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9260
9875
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9261
9876
|
cleanup();
|
|
9262
9877
|
if (code === 0) {
|
|
9263
|
-
|
|
9878
|
+
resolve5({ success: true, skipped: false });
|
|
9264
9879
|
} else {
|
|
9265
|
-
|
|
9880
|
+
resolve5({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9266
9881
|
}
|
|
9267
9882
|
});
|
|
9268
9883
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9269
9884
|
cleanup();
|
|
9270
|
-
|
|
9885
|
+
resolve5({ success: false, error: error.message });
|
|
9271
9886
|
});
|
|
9272
9887
|
});
|
|
9273
9888
|
} catch (error) {
|
|
@@ -9283,10 +9898,10 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9283
9898
|
return { success: true, skipped: true };
|
|
9284
9899
|
}
|
|
9285
9900
|
const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
|
|
9286
|
-
const tscBin =
|
|
9901
|
+
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9287
9902
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9288
9903
|
tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9289
|
-
return new Promise((
|
|
9904
|
+
return new Promise((resolve5) => {
|
|
9290
9905
|
const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
|
|
9291
9906
|
cwd: projectRoot
|
|
9292
9907
|
});
|
|
@@ -9294,36 +9909,36 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9294
9909
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9295
9910
|
cleanup();
|
|
9296
9911
|
if (code === 0) {
|
|
9297
|
-
|
|
9912
|
+
resolve5({ success: true, skipped: false });
|
|
9298
9913
|
} else {
|
|
9299
|
-
|
|
9914
|
+
resolve5({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9300
9915
|
}
|
|
9301
9916
|
});
|
|
9302
9917
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9303
9918
|
cleanup();
|
|
9304
|
-
|
|
9919
|
+
resolve5({ success: false, error: error.message });
|
|
9305
9920
|
});
|
|
9306
9921
|
});
|
|
9307
9922
|
} else {
|
|
9308
|
-
const tscBin =
|
|
9923
|
+
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9309
9924
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9310
9925
|
const rawArgs = buildTypeScriptArgs({ scope, configFile });
|
|
9311
9926
|
tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
|
|
9312
9927
|
}
|
|
9313
|
-
return new Promise((
|
|
9928
|
+
return new Promise((resolve5) => {
|
|
9314
9929
|
const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
|
|
9315
9930
|
cwd: projectRoot
|
|
9316
9931
|
});
|
|
9317
9932
|
forwardValidationSubprocessOutput(tscProcess, outputStreams);
|
|
9318
9933
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9319
9934
|
if (code === 0) {
|
|
9320
|
-
|
|
9935
|
+
resolve5({ success: true, skipped: false });
|
|
9321
9936
|
} else {
|
|
9322
|
-
|
|
9937
|
+
resolve5({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9323
9938
|
}
|
|
9324
9939
|
});
|
|
9325
9940
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9326
|
-
|
|
9941
|
+
resolve5({ success: false, error: error.message });
|
|
9327
9942
|
});
|
|
9328
9943
|
});
|
|
9329
9944
|
}
|
|
@@ -9565,7 +10180,7 @@ function truncate(value) {
|
|
|
9565
10180
|
|
|
9566
10181
|
// src/validation/literal/allowlist-existing.ts
|
|
9567
10182
|
import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
9568
|
-
import { dirname as
|
|
10183
|
+
import { dirname as dirname7, join as join29 } from "path";
|
|
9569
10184
|
var EXIT_OK = 0;
|
|
9570
10185
|
var EXIT_ERROR = 1;
|
|
9571
10186
|
var INCLUDE_FIELD = "include";
|
|
@@ -9585,9 +10200,9 @@ var productionReader = {
|
|
|
9585
10200
|
};
|
|
9586
10201
|
var productionWriter = {
|
|
9587
10202
|
async write(filePath, content) {
|
|
9588
|
-
const dir =
|
|
10203
|
+
const dir = dirname7(filePath);
|
|
9589
10204
|
const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
|
|
9590
|
-
const tmpPath =
|
|
10205
|
+
const tmpPath = join29(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
|
|
9591
10206
|
await writeFile4(tmpPath, content, "utf8");
|
|
9592
10207
|
await rename4(tmpPath, filePath);
|
|
9593
10208
|
}
|
|
@@ -9910,8 +10525,8 @@ var validationDomain = {
|
|
|
9910
10525
|
|
|
9911
10526
|
// src/interfaces/cli/registry.ts
|
|
9912
10527
|
var CLI_DOMAINS = [
|
|
9913
|
-
auditDomain,
|
|
9914
10528
|
claudeDomain,
|
|
10529
|
+
compactDomain,
|
|
9915
10530
|
configDomain,
|
|
9916
10531
|
sessionDomain,
|
|
9917
10532
|
specDomain,
|