@gobing-ai/spur 0.3.98 → 0.3.99
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/.claude-plugin/marketplace.json +1 -1
- package/config/plugin-scripts.json +5 -0
- package/config/templates/task/brainstorm.md +1 -1
- package/config/templates/task/feature-impl.md +1 -1
- package/config/templates/task/issue.md +1 -1
- package/config/templates/task/meta.md +1 -1
- package/config/templates/task/review.md +1 -1
- package/config/templates/task/standard.md +1 -1
- package/package.json +9 -9
- package/plugins/sp/lib/idea-handoff.generated.mjs +37 -4
- package/plugins/sp/lib/inline-run.generated.mjs +38 -5
- package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/persist-out-check.mjs +164 -0
- package/plugins/sp/scripts/persist-out-check.ts +185 -0
- package/plugins/sp/scripts/residual-scan.mjs +50 -8
- package/plugins/sp/scripts/residual-scan.ts +4 -3
- package/plugins/sp/scripts/task-diffstat.mjs +17 -5
- package/plugins/sp/scripts/task-diffstat.ts +25 -4
- package/plugins/sp/skills/code-verification/SKILL.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +216 -37
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +3 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +107 -3
- package/spur.js +1662 -386
- package/web/_astro/{BoardApp.CYbe-OTb.js → BoardApp.BVp-LYi6.js} +101 -101
- package/web/_astro/{BoardApp.Dflq5a8x.js → BoardApp.i4eh_fHA.js} +1 -1
- package/web/_astro/{TaskDetail.CK7oXddP.js → TaskDetail.C5Cu8LdB.js} +1 -1
- package/web/_astro/{arc.BIqkDATc.js → arc.Bu5XpBEz.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.D0B3SvWv.js → architectureDiagram-3BPJPVTR.9vHQTvhF.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.C2YyKQmL.js → blockDiagram-GPEHLZMM.Cn2Xl7D6.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DmtTYrt_.js → c4Diagram-AAUBKEIU.7o0P_QDo.js} +1 -1
- package/web/_astro/channel.6IFIKBhu.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.HsRXbh7l.js → chunk-2J33WTMH.C3zZFsNt.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.obE_P-R3.js → chunk-4BX2VUAB.CNy_M7fO.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CjWTF5mz.js → chunk-55IACEB6.2TiPnLBL.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DMo799Ax.js → chunk-727SXJPM.5CGSyKAp.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.-cE6JOKZ.js → chunk-AQP2D5EJ.w-ZIgfsK.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D6Rw6HIk.js → chunk-FMBD7UC4.BkcEyhKa.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.d5rBD73Z.js → chunk-ND2GUHAM.mxKKbBG4.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.r3ox40Q3.js → chunk-QZHKN3VN.cyA0z9aT.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CGGbvjjJ.js → classDiagram-4FO5ZUOK.CqtUzwhX.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js → classDiagram-v2-Q7XG4LA2.CqtUzwhX.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CmKtPd4l.js → cose-bilkent-S5V4N54A.BExPTopI.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.QNg5GXs8.js → cynefin-OW5HDTMX.Bq2i10Cn.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.B1SabsjC.js → dagre-BM42HDAG.By_HsOuv.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.RtDbNj89.js → diagram-2AECGRRQ.BtU0b7hz.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.IzZpuM-Y.js → diagram-5GNKFQAL.D00ztMdT.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DPTF_fgv.js → diagram-KO2AKTUF.DMVJWaUN.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CCyza39P.js → diagram-LMA3HP47.Cxqrjg5-.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BXAi8zcZ.js → diagram-OG6HWLK6.2lz_1B5h.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DUDX7_MR.js → erDiagram-TEJ5UH35.BVQEuJ3T.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.C63QTh3N.js → flowDiagram-I6XJVG4X.Qlyo90ky.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.CnlZJSwG.js → ganttDiagram-6RSMTGT7.DRwb8HHZ.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CTqIrOPC.js → gitGraphDiagram-PVQCEYII.YfoPthfS.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.BYMiDFh9.js → infoDiagram-5YYISTIA.DvFREck1.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CRiQmI-z.js → ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.YQgWBsEU.js → journeyDiagram-JHISSGLW.BDMT_AFb.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.1ECnrrK1.js → kanban-definition-UN3LZRKU.D6Fh3gj9.js} +1 -1
- package/web/_astro/{linear.DzVA71yT.js → linear.Dxw00fGQ.js} +1 -1
- package/web/_astro/{mermaid.core.BK7P2Sr_.js → mermaid.core.CsoxUWa0.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BvXyWZK7.js → mindmap-definition-RKZ34NQL.CN8oeHLu.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.BiXk1TW3.js → pieDiagram-4H26LBE5.C7ADqQ_7.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.krvBH8lh.js → quadrantDiagram-W4KKPZXB.Cw9lKyHP.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ih1gL25c.js → requirementDiagram-4Y6WPE33.B1ni-z4q.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.CpUxkv4u.js → sankeyDiagram-5OEKKPKP.BhexdjVr.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.clAfeN7y.js → sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DpXWb8-m.js → stateDiagram-AJRCARHV.CkuMt27u.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.ClZb9GIH.js → stateDiagram-v2-BHNVJYJU.Dc_aazUA.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BLcX5dh7.js → timeline-definition-PNZ67QCA.ja7sUFhH.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.oGgpKMpx.js → vennDiagram-CIIHVFJN.BxIshX_K.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BPHtMtt-.js → wardleyDiagram-YWT4CUSO.DfHlC0sh.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BNZOPYSk.js → xychartDiagram-2RQKCTM6.DSnhHwCh.js} +1 -1
- package/web/index.html +1 -1
- package/web/_astro/channel.JuKVyuhZ.js +0 -1
|
@@ -61,3 +61,8 @@ export declare function foldVerdict(
|
|
|
61
61
|
maxFindings?: number,
|
|
62
62
|
): FoldResult;
|
|
63
63
|
export declare function renderReport(wbs: string, items: ResidualItem[], attemptCount: number): string;
|
|
64
|
+
export declare function verdictDisagreementNote(
|
|
65
|
+
runDir: string,
|
|
66
|
+
wbs: string,
|
|
67
|
+
fs: Pick<typeof import('node:fs'), 'existsSync' | 'readFileSync' | 'statSync'>,
|
|
68
|
+
): string | null;
|
|
@@ -3,7 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
5
5
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
6
|
-
var
|
|
6
|
+
var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
|
|
7
|
+
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|\u2014)\s*(\(.*\))?\.?$/i;
|
|
7
8
|
var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
8
9
|
var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
9
10
|
var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -60,7 +61,7 @@ function parseReviewFindings(taskContent) {
|
|
|
60
61
|
const priority = (cells[priorityCol] ?? "").trim();
|
|
61
62
|
const finding = (cells[findingCol] ?? "").trim();
|
|
62
63
|
const disposition = dispositionCol === -1 ? "" : (cells[dispositionCol] ?? "").trim();
|
|
63
|
-
if (PRIORITY_PATTERN.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
|
|
64
|
+
if (PRIORITY_PATTERN.test(priority) && !RANGE_PRIORITY.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
|
|
64
65
|
const location = locationOf(cells[locationCol] ?? "", finding);
|
|
65
66
|
out.push(DEFERRED_DISPOSITION.test(disposition) ? { priority, location, text: finding, deferral: disposition } : { priority, location, text: finding });
|
|
66
67
|
}
|
|
@@ -70,7 +71,7 @@ function parseReviewFindings(taskContent) {
|
|
|
70
71
|
return out;
|
|
71
72
|
}
|
|
72
73
|
function splitRow(line) {
|
|
73
|
-
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(
|
|
74
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(/(?<!\\)\|/).map((c) => c.trim());
|
|
74
75
|
}
|
|
75
76
|
function parseDiffMarkers(addedLines) {
|
|
76
77
|
return addedLines.filter((l) => !EXCLUDED_PATHS.some((p) => l.file.startsWith(p))).filter((l) => !l.text.includes(ALLOW_PRAGMA)).filter((l) => MARKER_PATTERN.test(l.text)).map((l) => ({ location: `${l.file}:${l.line}`, text: l.text.trim() }));
|
|
@@ -198,7 +199,8 @@ function renderReport(wbs, items, attemptCount) {
|
|
|
198
199
|
function recordedVerdictPath(runDir, wbs, fs) {
|
|
199
200
|
const evidence = join(runDir, "..", "memory", "evidence");
|
|
200
201
|
const durable = join(evidence, `${wbs}-verdict.json`);
|
|
201
|
-
|
|
202
|
+
const run = join(runDir, `${wbs}-verdict.json`);
|
|
203
|
+
for (const path of [join(runDir, ".."), join(evidence, ".."), evidence, durable, run]) {
|
|
202
204
|
try {
|
|
203
205
|
if (fs.lstatSync(path).isSymbolicLink())
|
|
204
206
|
throw new Error(`residual-scan: symlink evidence path: ${path}`);
|
|
@@ -207,9 +209,47 @@ function recordedVerdictPath(runDir, wbs, fs) {
|
|
|
207
209
|
throw error;
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
|
-
|
|
212
|
+
if (!fs.existsSync(durable))
|
|
213
|
+
return run;
|
|
214
|
+
if (!fs.existsSync(run))
|
|
215
|
+
return durable;
|
|
216
|
+
const newer = (a, b) => mtimeOf(fs, b) > mtimeOf(fs, a) ? b : a;
|
|
217
|
+
return newer(run, durable);
|
|
218
|
+
}
|
|
219
|
+
function mtimeOf(fs, path) {
|
|
220
|
+
try {
|
|
221
|
+
return fs.statSync(path).mtimeMs;
|
|
222
|
+
} catch {
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function verdictDisagreementNote(runDir, wbs, fs) {
|
|
227
|
+
const run = join(runDir, `${wbs}-verdict.json`);
|
|
228
|
+
const durable = join(runDir, "..", "memory", "evidence", `${wbs}-verdict.json`);
|
|
229
|
+
if (!fs.existsSync(run) || !fs.existsSync(durable))
|
|
230
|
+
return null;
|
|
231
|
+
const bytes = (path) => {
|
|
232
|
+
try {
|
|
233
|
+
return fs.statSync(path).isFile() ? fs.readFileSync(path, "utf8") : null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const runBytes = bytes(run);
|
|
239
|
+
if (runBytes === null || runBytes === bytes(durable))
|
|
240
|
+
return null;
|
|
241
|
+
const value = (path) => {
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(bytes(path) ?? "{}").verdict ?? "?";
|
|
244
|
+
} catch {
|
|
245
|
+
return "?";
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const winner = mtimeOf(fs, durable) > mtimeOf(fs, run) ? "durable" : "run";
|
|
249
|
+
return `residual-fold: ${wbs} verdict copies disagree — run=${run} (${value(run)}) durable=${durable} (${value(durable)})` + ` → chose ${winner} (newer mtime)`;
|
|
211
250
|
}
|
|
212
251
|
export {
|
|
252
|
+
verdictDisagreementNote,
|
|
213
253
|
scanResiduals,
|
|
214
254
|
renderReport,
|
|
215
255
|
recordedVerdictPath,
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.99",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/persist-out-check.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
8
|
+
var EVIDENCE_DIR = join(".spur", "memory", "evidence");
|
|
9
|
+
var RUN_DIR = join(".spur", "run");
|
|
10
|
+
var NAMED_CAP = 32;
|
|
11
|
+
var PREFIX_CAP = 64;
|
|
12
|
+
var EVIDENCE_CAP = 256;
|
|
13
|
+
function wbsOfTaskFile(path) {
|
|
14
|
+
const m = /^(\d+)_/.exec(basename(path));
|
|
15
|
+
return m?.[1] ?? null;
|
|
16
|
+
}
|
|
17
|
+
function listFilesUnder(root, rel, cap) {
|
|
18
|
+
const dir = join(root, rel);
|
|
19
|
+
if (!existsSync(dir))
|
|
20
|
+
return [];
|
|
21
|
+
const out = [];
|
|
22
|
+
const walk = (cur) => {
|
|
23
|
+
for (const entry of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
24
|
+
const p = join(cur, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
if (!walk(p))
|
|
27
|
+
return false;
|
|
28
|
+
} else {
|
|
29
|
+
out.push(relative(root, p));
|
|
30
|
+
if (out.length > cap)
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
return walk(dir) ? out : null;
|
|
37
|
+
}
|
|
38
|
+
function listObligations(wtRoot, wbsList, runIds) {
|
|
39
|
+
const evidence = listFilesUnder(wtRoot, EVIDENCE_DIR, EVIDENCE_CAP);
|
|
40
|
+
if (evidence === null)
|
|
41
|
+
return null;
|
|
42
|
+
const out = [...evidence];
|
|
43
|
+
const prefixes = [...wbsList, ...runIds];
|
|
44
|
+
for (const prefix of prefixes) {
|
|
45
|
+
const dir = join(wtRoot, RUN_DIR);
|
|
46
|
+
if (!existsSync(dir))
|
|
47
|
+
continue;
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir).sort();
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const owned = entries.filter((n) => n.startsWith(`${prefix}-`));
|
|
55
|
+
if (owned.length > PREFIX_CAP)
|
|
56
|
+
return null;
|
|
57
|
+
out.push(...owned.map((n) => join(RUN_DIR, n)));
|
|
58
|
+
}
|
|
59
|
+
return [...new Set(out)].sort();
|
|
60
|
+
}
|
|
61
|
+
function compareTrees(wtRoot, invokeRoot, files) {
|
|
62
|
+
const missing = [];
|
|
63
|
+
const divergent = [];
|
|
64
|
+
let ok = 0;
|
|
65
|
+
for (const rel of files) {
|
|
66
|
+
const wtPath = join(wtRoot, rel);
|
|
67
|
+
const invPath = join(invokeRoot, rel);
|
|
68
|
+
if (!existsSync(invPath)) {
|
|
69
|
+
missing.push(rel);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const same = readFileSync(wtPath).equals(readFileSync(invPath));
|
|
74
|
+
if (same)
|
|
75
|
+
ok++;
|
|
76
|
+
else
|
|
77
|
+
divergent.push(rel);
|
|
78
|
+
} catch {
|
|
79
|
+
divergent.push(rel);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { missing, divergent, ok };
|
|
83
|
+
}
|
|
84
|
+
function persistOutCheckUsage() {
|
|
85
|
+
return "usage: persist-out-check --from <worktree> [--task-file <path>]\u2026 [--run-id <id>]\u2026 [--root <invoke-tree>]";
|
|
86
|
+
}
|
|
87
|
+
function defaultInvokeRoot(cwd) {
|
|
88
|
+
try {
|
|
89
|
+
const run = spawnSync("git", ["-C", cwd, "rev-parse", "--git-common-dir"], { encoding: "utf8" });
|
|
90
|
+
const common = run.stdout.trim();
|
|
91
|
+
if (run.status === 0 && common)
|
|
92
|
+
return dirname(isAbsolute(common) ? common : join(cwd, common));
|
|
93
|
+
} catch {}
|
|
94
|
+
return cwd;
|
|
95
|
+
}
|
|
96
|
+
function main(argv, invokeRoot = defaultInvokeRoot(process.cwd())) {
|
|
97
|
+
let wtRoot = "";
|
|
98
|
+
const wbsList = [];
|
|
99
|
+
const runIds = [];
|
|
100
|
+
for (let i = 0;i < argv.length; i++) {
|
|
101
|
+
const arg = argv[i];
|
|
102
|
+
if (arg === "--spur-bin") {
|
|
103
|
+
i++;
|
|
104
|
+
} else if (arg === "--from") {
|
|
105
|
+
wtRoot = argv[++i] ?? "";
|
|
106
|
+
} else if (arg === "--root") {
|
|
107
|
+
invokeRoot = argv[++i] ?? invokeRoot;
|
|
108
|
+
} else if (arg === "--task-file") {
|
|
109
|
+
const wbs = wbsOfTaskFile(argv[++i] ?? "");
|
|
110
|
+
if (wbs)
|
|
111
|
+
wbsList.push(wbs);
|
|
112
|
+
} else if (arg === "--run-id") {
|
|
113
|
+
const id = argv[++i] ?? "";
|
|
114
|
+
if (id)
|
|
115
|
+
runIds.push(id);
|
|
116
|
+
} else {
|
|
117
|
+
process.stderr.write(`${persistOutCheckUsage()}
|
|
118
|
+
`);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
|
|
123
|
+
process.stderr.write(`${persistOutCheckUsage()}
|
|
124
|
+
persist-out-check: --from must be an existing worktree directory
|
|
125
|
+
`);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
const files = listObligations(wtRoot, wbsList, runIds);
|
|
129
|
+
if (files === null) {
|
|
130
|
+
process.stderr.write(`persist-out-check: BLOCKED \u2014 worktree evidence listing failed or exceeded caps (64/prefix, ${EVIDENCE_CAP} evidence files) \u2014 inspect by hand
|
|
131
|
+
`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files);
|
|
135
|
+
const findings = [
|
|
136
|
+
...missing.map((f) => `MISSING ${f}`),
|
|
137
|
+
...divergent.map((f) => `DIVERGENT ${f} \u2014 reconcile by hand (persist-out never overwrites)`)
|
|
138
|
+
];
|
|
139
|
+
for (const line of findings.slice(0, NAMED_CAP))
|
|
140
|
+
process.stdout.write(`${line}
|
|
141
|
+
`);
|
|
142
|
+
if (findings.length > NAMED_CAP)
|
|
143
|
+
process.stdout.write(`\u2026 +${findings.length - NAMED_CAP} more
|
|
144
|
+
`);
|
|
145
|
+
if (findings.length === 0) {
|
|
146
|
+
process.stdout.write(`persist-out-check: ok \u2014 ${ok} evidence file(s) persisted, nothing abandoned
|
|
147
|
+
`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
process.stderr.write(`persist-out-check: BLOCKED ${missing.length} missing, ${divergent.length} divergent \u2014 run inline-run-setup --persist-out --from <worktree> before worktree removal
|
|
151
|
+
`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
{
|
|
155
|
+
process.exit(main(process.argv.slice(2)));
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
wbsOfTaskFile,
|
|
159
|
+
persistOutCheckUsage,
|
|
160
|
+
main,
|
|
161
|
+
listObligations,
|
|
162
|
+
defaultInvokeRoot,
|
|
163
|
+
compareTrees
|
|
164
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1).
|
|
4
|
+
*
|
|
5
|
+
* Before `git worktree remove`, verify the worktree's durable evidence would not be
|
|
6
|
+
* abandoned: every file the worktree owns under `.spur/run/` (`<wbs>-*` per forwarded
|
|
7
|
+
* task file, `<runId>-*` per forwarded run id) and the whole `.spur/memory/evidence/`
|
|
8
|
+
* tree must exist in the invoking tree with identical bytes. Missing files BLOCK the
|
|
9
|
+
* removal (persist-out was skipped or stale); divergent files block too (persist-out
|
|
10
|
+
* never overwrites — reconcile by hand per execution-batch.md). No new public CLI verb:
|
|
11
|
+
* this is a driver-side assertion step, not a corpus tool.
|
|
12
|
+
*
|
|
13
|
+
* ponytail: file-level comparison only — DB run rows and nested-run deep walks stay
|
|
14
|
+
* persist-out's job (`inline-run-setup --persist-out`); this checks the planes E71
|
|
15
|
+
* names as files. Listing caps (64/prefix, 256 evidence) refuse runaways by name.
|
|
16
|
+
*
|
|
17
|
+
* Node-builtin imports only; pure helpers are exported for tests. Exit 0 ok, 1
|
|
18
|
+
* blocked/divergent, 2 usage error. Normal output is the finding lines themselves.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
23
|
+
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
24
|
+
|
|
25
|
+
const EVIDENCE_DIR = join('.spur', 'memory', 'evidence');
|
|
26
|
+
const RUN_DIR = join('.spur', 'run');
|
|
27
|
+
const NAMED_CAP = 32;
|
|
28
|
+
const PREFIX_CAP = 64;
|
|
29
|
+
const EVIDENCE_CAP = 256;
|
|
30
|
+
|
|
31
|
+
/** Task files own their `<wbs>-*` evidence: leading digits before `_` in the basename. */
|
|
32
|
+
export function wbsOfTaskFile(path: string): string | null {
|
|
33
|
+
const m = /^(\d+)_/.exec(basename(path));
|
|
34
|
+
return m?.[1] ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function listFilesUnder(root: string, rel: string, cap: number): string[] | null {
|
|
38
|
+
const dir = join(root, rel);
|
|
39
|
+
if (!existsSync(dir)) return [];
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
const walk = (cur: string): boolean => {
|
|
42
|
+
for (const entry of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
43
|
+
const p = join(cur, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
if (!walk(p)) return false;
|
|
46
|
+
} else {
|
|
47
|
+
out.push(relative(root, p));
|
|
48
|
+
if (out.length > cap) return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
return walk(dir) ? out : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Evidence files the worktree owns: `.spur/memory/evidence/**` plus `.spur/run/<prefix>-*`
|
|
58
|
+
* direct children per task-file WBS / run id. Returns null on a cap overrun or a listing
|
|
59
|
+
* failure (not a directory, permission denied) — the caller refuses either way.
|
|
60
|
+
*/
|
|
61
|
+
export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]): string[] | null {
|
|
62
|
+
const evidence = listFilesUnder(wtRoot, EVIDENCE_DIR, EVIDENCE_CAP);
|
|
63
|
+
if (evidence === null) return null;
|
|
64
|
+
const out = [...evidence];
|
|
65
|
+
const prefixes = [...wbsList, ...runIds];
|
|
66
|
+
for (const prefix of prefixes) {
|
|
67
|
+
const dir = join(wtRoot, RUN_DIR);
|
|
68
|
+
if (!existsSync(dir)) continue;
|
|
69
|
+
let entries: string[];
|
|
70
|
+
try {
|
|
71
|
+
entries = readdirSync(dir).sort();
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const owned = entries.filter((n) => n.startsWith(`${prefix}-`));
|
|
76
|
+
if (owned.length > PREFIX_CAP) return null;
|
|
77
|
+
out.push(...owned.map((n) => join(RUN_DIR, n)));
|
|
78
|
+
}
|
|
79
|
+
return [...new Set(out)].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Classify obligations against the invoking tree: missing, divergent (byte-differs), or ok. */
|
|
83
|
+
export function compareTrees(
|
|
84
|
+
wtRoot: string,
|
|
85
|
+
invokeRoot: string,
|
|
86
|
+
files: string[],
|
|
87
|
+
): { missing: string[]; divergent: string[]; ok: number } {
|
|
88
|
+
const missing: string[] = [];
|
|
89
|
+
const divergent: string[] = [];
|
|
90
|
+
let ok = 0;
|
|
91
|
+
for (const rel of files) {
|
|
92
|
+
const wtPath = join(wtRoot, rel);
|
|
93
|
+
const invPath = join(invokeRoot, rel);
|
|
94
|
+
if (!existsSync(invPath)) {
|
|
95
|
+
missing.push(rel);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const same = readFileSync(wtPath).equals(readFileSync(invPath));
|
|
100
|
+
if (same) ok++;
|
|
101
|
+
else divergent.push(rel);
|
|
102
|
+
} catch {
|
|
103
|
+
divergent.push(rel);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { missing, divergent, ok };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function persistOutCheckUsage(): string {
|
|
110
|
+
return 'usage: persist-out-check --from <worktree> [--task-file <path>]… [--run-id <id>]… [--root <invoke-tree>]';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The invoking tree is the MAIN repo root — never default to cwd: WT-4 runs from the
|
|
115
|
+
* worktree, and comparing the worktree against itself is a vacuous pass (caught live in
|
|
116
|
+
* task 1067 R4). `--git-common-dir` resolves to the shared `.git`, whose parent is the
|
|
117
|
+
* main tree; inside the main tree itself this falls back to cwd, which is correct there.
|
|
118
|
+
*/
|
|
119
|
+
export function defaultInvokeRoot(cwd: string): string {
|
|
120
|
+
try {
|
|
121
|
+
const run = spawnSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], { encoding: 'utf8' });
|
|
122
|
+
const common = run.stdout.trim();
|
|
123
|
+
if (run.status === 0 && common) return dirname(isAbsolute(common) ? common : join(cwd, common));
|
|
124
|
+
} catch {
|
|
125
|
+
// git unavailable — fall through to cwd
|
|
126
|
+
}
|
|
127
|
+
return cwd;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd())): number {
|
|
131
|
+
let wtRoot = '';
|
|
132
|
+
const wbsList: string[] = [];
|
|
133
|
+
const runIds: string[] = [];
|
|
134
|
+
for (let i = 0; i < argv.length; i++) {
|
|
135
|
+
const arg = argv[i];
|
|
136
|
+
if (arg === '--spur-bin') {
|
|
137
|
+
i++;
|
|
138
|
+
} else if (arg === '--from') {
|
|
139
|
+
wtRoot = argv[++i] ?? '';
|
|
140
|
+
} else if (arg === '--root') {
|
|
141
|
+
invokeRoot = argv[++i] ?? invokeRoot;
|
|
142
|
+
} else if (arg === '--task-file') {
|
|
143
|
+
const wbs = wbsOfTaskFile(argv[++i] ?? '');
|
|
144
|
+
if (wbs) wbsList.push(wbs);
|
|
145
|
+
} else if (arg === '--run-id') {
|
|
146
|
+
const id = argv[++i] ?? '';
|
|
147
|
+
if (id) runIds.push(id);
|
|
148
|
+
} else {
|
|
149
|
+
process.stderr.write(`${persistOutCheckUsage()}\n`);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
|
|
154
|
+
process.stderr.write(
|
|
155
|
+
`${persistOutCheckUsage()}\npersist-out-check: --from must be an existing worktree directory\n`,
|
|
156
|
+
);
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
const files = listObligations(wtRoot, wbsList, runIds);
|
|
160
|
+
if (files === null) {
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
`persist-out-check: BLOCKED — worktree evidence listing failed or exceeded caps (64/prefix, ${EVIDENCE_CAP} evidence files) — inspect by hand\n`,
|
|
163
|
+
);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files);
|
|
167
|
+
const findings = [
|
|
168
|
+
...missing.map((f) => `MISSING ${f}`),
|
|
169
|
+
...divergent.map((f) => `DIVERGENT ${f} — reconcile by hand (persist-out never overwrites)`),
|
|
170
|
+
];
|
|
171
|
+
for (const line of findings.slice(0, NAMED_CAP)) process.stdout.write(`${line}\n`);
|
|
172
|
+
if (findings.length > NAMED_CAP) process.stdout.write(`… +${findings.length - NAMED_CAP} more\n`);
|
|
173
|
+
if (findings.length === 0) {
|
|
174
|
+
process.stdout.write(`persist-out-check: ok — ${ok} evidence file(s) persisted, nothing abandoned\n`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
`persist-out-check: BLOCKED ${missing.length} missing, ${divergent.length} divergent — run inline-run-setup --persist-out --from <worktree> before worktree removal\n`,
|
|
179
|
+
);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (import.meta.main) {
|
|
184
|
+
process.exit(main(process.argv.slice(2)));
|
|
185
|
+
}
|
|
@@ -17,7 +17,8 @@ import { join } from "node:path";
|
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
19
19
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
20
|
-
var
|
|
20
|
+
var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
|
|
21
|
+
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|\u2014)\s*(\(.*\))?\.?$/i;
|
|
21
22
|
var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
22
23
|
var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
23
24
|
var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -74,7 +75,7 @@ function parseReviewFindings(taskContent) {
|
|
|
74
75
|
const priority = (cells[priorityCol] ?? "").trim();
|
|
75
76
|
const finding = (cells[findingCol] ?? "").trim();
|
|
76
77
|
const disposition = dispositionCol === -1 ? "" : (cells[dispositionCol] ?? "").trim();
|
|
77
|
-
if (PRIORITY_PATTERN.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
|
|
78
|
+
if (PRIORITY_PATTERN.test(priority) && !RANGE_PRIORITY.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
|
|
78
79
|
const location = locationOf(cells[locationCol] ?? "", finding);
|
|
79
80
|
out.push(DEFERRED_DISPOSITION.test(disposition) ? { priority, location, text: finding, deferral: disposition } : { priority, location, text: finding });
|
|
80
81
|
}
|
|
@@ -84,7 +85,7 @@ function parseReviewFindings(taskContent) {
|
|
|
84
85
|
return out;
|
|
85
86
|
}
|
|
86
87
|
function splitRow(line) {
|
|
87
|
-
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(
|
|
88
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(/(?<!\\)\|/).map((c) => c.trim());
|
|
88
89
|
}
|
|
89
90
|
function parseDiffMarkers(addedLines) {
|
|
90
91
|
return addedLines.filter((l) => !EXCLUDED_PATHS.some((p) => l.file.startsWith(p))).filter((l) => !l.text.includes(ALLOW_PRAGMA)).filter((l) => MARKER_PATTERN.test(l.text)).map((l) => ({ location: `${l.file}:${l.line}`, text: l.text.trim() }));
|
|
@@ -212,7 +213,8 @@ function renderReport(wbs, items, attemptCount) {
|
|
|
212
213
|
function recordedVerdictPath(runDir, wbs, fs) {
|
|
213
214
|
const evidence = join(runDir, "..", "memory", "evidence");
|
|
214
215
|
const durable = join(evidence, `${wbs}-verdict.json`);
|
|
215
|
-
|
|
216
|
+
const run = join(runDir, `${wbs}-verdict.json`);
|
|
217
|
+
for (const path of [join(runDir, ".."), join(evidence, ".."), evidence, durable, run]) {
|
|
216
218
|
try {
|
|
217
219
|
if (fs.lstatSync(path).isSymbolicLink())
|
|
218
220
|
throw new Error(`residual-scan: symlink evidence path: ${path}`);
|
|
@@ -221,7 +223,44 @@ function recordedVerdictPath(runDir, wbs, fs) {
|
|
|
221
223
|
throw error;
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
|
-
|
|
226
|
+
if (!fs.existsSync(durable))
|
|
227
|
+
return run;
|
|
228
|
+
if (!fs.existsSync(run))
|
|
229
|
+
return durable;
|
|
230
|
+
const newer = (a, b) => mtimeOf(fs, b) > mtimeOf(fs, a) ? b : a;
|
|
231
|
+
return newer(run, durable);
|
|
232
|
+
}
|
|
233
|
+
function mtimeOf(fs, path) {
|
|
234
|
+
try {
|
|
235
|
+
return fs.statSync(path).mtimeMs;
|
|
236
|
+
} catch {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function verdictDisagreementNote(runDir, wbs, fs) {
|
|
241
|
+
const run = join(runDir, `${wbs}-verdict.json`);
|
|
242
|
+
const durable = join(runDir, "..", "memory", "evidence", `${wbs}-verdict.json`);
|
|
243
|
+
if (!fs.existsSync(run) || !fs.existsSync(durable))
|
|
244
|
+
return null;
|
|
245
|
+
const bytes = (path) => {
|
|
246
|
+
try {
|
|
247
|
+
return fs.statSync(path).isFile() ? fs.readFileSync(path, "utf8") : null;
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const runBytes = bytes(run);
|
|
253
|
+
if (runBytes === null || runBytes === bytes(durable))
|
|
254
|
+
return null;
|
|
255
|
+
const value = (path) => {
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(bytes(path) ?? "{}").verdict ?? "?";
|
|
258
|
+
} catch {
|
|
259
|
+
return "?";
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const winner = mtimeOf(fs, durable) > mtimeOf(fs, run) ? "durable" : "run";
|
|
263
|
+
return `residual-fold: ${wbs} verdict copies disagree — run=${run} (${value(run)}) durable=${durable} (${value(durable)})` + ` → chose ${winner} (newer mtime)`;
|
|
225
264
|
}
|
|
226
265
|
|
|
227
266
|
// plugins/sp/lib/spur-bin.ts
|
|
@@ -337,9 +376,7 @@ function loadTask(env, spurBinFlag, wbs, root) {
|
|
|
337
376
|
const featureId = [parsed.feature_id, parsed.frontmatter?.feature_id].find((v) => typeof v === "string") ?? "";
|
|
338
377
|
return { content: typeof parsed.content === "string" ? parsed.content : "", featureId };
|
|
339
378
|
}
|
|
340
|
-
|
|
341
|
-
return JSON.parse(fs.readFileSync(recordedVerdictPath(runDir, wbs, fs), "utf8"));
|
|
342
|
-
}
|
|
379
|
+
var loadVerdict = (runDir, wbs) => JSON.parse(fs.readFileSync(recordedVerdictPath(runDir, wbs, fs), "utf8"));
|
|
343
380
|
function scanMode(opts, env, io) {
|
|
344
381
|
const runDir = join2(opts.root, ".spur", "run");
|
|
345
382
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -355,6 +392,10 @@ function foldMode(opts, _env, io) {
|
|
|
355
392
|
const runDir = join2(opts.root, ".spur", "run");
|
|
356
393
|
const scan = JSON.parse(fs.readFileSync(join2(runDir, `${opts.wbs}-residuals.json`), "utf8"));
|
|
357
394
|
const target = recordedVerdictPath(runDir, opts.wbs, fs);
|
|
395
|
+
const note = verdictDisagreementNote(runDir, opts.wbs, fs);
|
|
396
|
+
if (note !== null)
|
|
397
|
+
io.out(`${note}
|
|
398
|
+
`);
|
|
358
399
|
const verdict = loadVerdict(runDir, opts.wbs);
|
|
359
400
|
const findingsPath = join2(runDir, `${opts.wbs}-test-gate.findings`);
|
|
360
401
|
const fold = foldVerdict(verdict, scan, fs.existsSync(findingsPath) ? fs.readFileSync(findingsPath, "utf8") : "");
|
|
@@ -469,6 +510,7 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
469
510
|
}
|
|
470
511
|
process.exit(main(process.argv.slice(2)));
|
|
471
512
|
export {
|
|
513
|
+
verdictDisagreementNote,
|
|
472
514
|
scanResiduals2 as scanResiduals,
|
|
473
515
|
renderReport,
|
|
474
516
|
recordedVerdictPath,
|
|
@@ -115,9 +115,8 @@ function loadTask(env: ScanEnv, spurBinFlag: string | undefined, wbs: string, ro
|
|
|
115
115
|
[parsed.feature_id, parsed.frontmatter?.feature_id].find((v): v is string => typeof v === 'string') ?? '';
|
|
116
116
|
return { content: typeof parsed.content === 'string' ? parsed.content : '', featureId };
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
118
|
+
const loadVerdict = (runDir: string, wbs: string): VerdictFile =>
|
|
119
|
+
JSON.parse(fs.readFileSync(core.recordedVerdictPath(runDir, wbs, fs), 'utf8'));
|
|
121
120
|
function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
122
121
|
const runDir = join(opts.root, '.spur', 'run');
|
|
123
122
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -135,6 +134,8 @@ function foldMode(opts: ParsedArgs, _env: ScanEnv, io: ScanIo): number {
|
|
|
135
134
|
fs.readFileSync(join(runDir, `${opts.wbs}-residuals.json`), 'utf8'),
|
|
136
135
|
) as core.ResidualArtifact;
|
|
137
136
|
const target = core.recordedVerdictPath(runDir, opts.wbs, fs);
|
|
137
|
+
const note = core.verdictDisagreementNote(runDir, opts.wbs, fs);
|
|
138
|
+
if (note !== null) io.out(`${note}\n`);
|
|
138
139
|
const verdict = loadVerdict(runDir, opts.wbs);
|
|
139
140
|
const findingsPath = join(runDir, `${opts.wbs}-test-gate.findings`);
|
|
140
141
|
const fold = core.foldVerdict(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/task-diffstat.ts
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { fstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
|
|
9
9
|
// plugins/sp/lib/env.ts
|
|
@@ -85,10 +85,14 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
|
|
|
85
85
|
process.stderr.write(`task-diffstat: ${reason} \u2014 failing safe (sensitive)
|
|
86
86
|
`);
|
|
87
87
|
const row2 = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
|
|
88
|
-
|
|
89
|
-
`);
|
|
88
|
+
writeArtifact(abs(relResult), row2);
|
|
90
89
|
return { ...row2, resultFile: relResult, exitCode: 0 };
|
|
91
90
|
};
|
|
91
|
+
let stdoutIsArtifact = false;
|
|
92
|
+
try {
|
|
93
|
+
const [out, art] = [fstatSync(1), statSync(abs(relResult))];
|
|
94
|
+
stdoutIsArtifact = out.dev === art.dev && out.ino === art.ino;
|
|
95
|
+
} catch {}
|
|
92
96
|
let base = "";
|
|
93
97
|
try {
|
|
94
98
|
base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), "utf8").trim();
|
|
@@ -125,9 +129,17 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
|
|
|
125
129
|
const all = [...paths].filter((p) => p.length > 0).sort();
|
|
126
130
|
const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
|
|
127
131
|
const row = { files: all.length, insertions, deletions, paths: all, sensitive };
|
|
128
|
-
|
|
132
|
+
writeArtifact(abs(relResult), row);
|
|
133
|
+
if (stdoutIsArtifact)
|
|
134
|
+
process.stderr.write(`task-diffstat: stdout redirected onto the artifact \u2014 kept; redirect stdout elsewhere
|
|
135
|
+
`);
|
|
136
|
+
return { ...row, resultFile: relResult, exitCode: stdoutIsArtifact ? 1 : 0 };
|
|
137
|
+
}
|
|
138
|
+
function writeArtifact(absPath, row) {
|
|
139
|
+
const tmp = `${absPath}.tmp`;
|
|
140
|
+
writeFileSync(tmp, `${JSON.stringify(row)}
|
|
129
141
|
`);
|
|
130
|
-
|
|
142
|
+
renameSync(tmp, absPath);
|
|
131
143
|
}
|
|
132
144
|
var TASK_DIFFSTAT_USAGE = "usage: task-diffstat (env: wbs)";
|
|
133
145
|
function main(argv, env = getEnvVars(), options = {}) {
|