@gobing-ai/spur 0.3.95 → 0.3.96
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 +0 -54
- package/config/rules/boundary/sp-script-placement.yaml +19 -0
- package/config/rules/strict/runtime-boundaries.yaml +5 -0
- package/config/rules/structure/test-location.yaml +2 -0
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
- package/config/rules/typescript/output-boundaries.yaml +15 -2
- package/config/script-placement-baseline.json +51 -0
- package/config/workflows/feature-verification.yaml +1 -1
- package/config/workflows/history-anatomy.yaml +23 -11
- package/config/workflows/idea-pipeline.yaml +80 -85
- package/config/workflows/pr-review.yaml +18 -9
- package/config/workflows/task-pipeline.yaml +42 -95
- package/config/workflows/wrapup-pipeline.yaml +89 -37
- package/package.json +1 -1
- package/plugins/sp/README.md +8 -10
- package/plugins/sp/agents/super-reviewer.md +43 -18
- package/plugins/sp/commands/dev-fixgha.md +83 -0
- package/plugins/sp/commands/dev-gitmsg.md +8 -6
- package/plugins/sp/commands/dev-gtd.md +2 -2
- package/plugins/sp/commands/dev-idea.md +22 -12
- package/plugins/sp/commands/dev-plan.md +8 -9
- package/plugins/sp/commands/dev-review.md +22 -13
- package/plugins/sp/commands/dev-verifyall.md +1 -1
- package/plugins/sp/commands/spur-init.md +2 -2
- package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
- package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
- package/plugins/sp/lib/inline-run.generated.mjs +25 -8
- package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
- package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
- package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
- package/plugins/sp/lib/spur-bin.ts +36 -0
- package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
- package/plugins/sp/lib/step-profile.generated.mjs +174 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
- package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
- package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
- package/plugins/sp/scripts/inline-run-setup.ts +104 -667
- package/plugins/sp/scripts/quality-gate.mjs +34 -19
- package/plugins/sp/scripts/quality-gate.ts +12 -658
- package/plugins/sp/scripts/residual-scan.mjs +135 -156
- package/plugins/sp/scripts/residual-scan.ts +102 -499
- package/plugins/sp/scripts/script-root.mjs +5 -1
- package/plugins/sp/scripts/script-root.ts +5 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
- package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +10 -32
- package/plugins/sp/scripts/wrapup-steps.ts +8 -36
- package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
- package/plugins/sp/skills/code-verification/SKILL.md +34 -9
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
- package/plugins/sp/skills/functional-review/SKILL.md +7 -4
- package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
- package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
- package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
- package/plugins/sp/skills/spur-cli/references/features.md +17 -6
- package/plugins/sp/skills/spur-cli/references/init.md +17 -16
- package/plugins/sp/skills/spur-cli/references/self.md +3 -2
- package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
- package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
- package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +77 -34
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
- package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
- package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -3
- package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
- package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
- package/spur.js +1495 -369
- package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.BIjMatT1.js} +1 -1
- package/web/_astro/{BoardApp.BxJuwD7I.js → BoardApp.GvjIe9Z6.js} +42 -42
- package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.BZM3EAFx.js} +1 -1
- package/web/_astro/{arc.CyjRvNMY.js → arc.HkRiZnoI.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.BDdK-tgZ.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
- package/web/_astro/channel.DlhX2MEt.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.BraxQ2Nt.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.Dmdxgd-M.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
- package/web/_astro/index.EoZlzLK-.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
- package/web/_astro/{linear.zRsuuDTE.js → linear.BnzMBgo_.js} +1 -1
- package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DSqeFu2Y.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
- package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
- package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
- package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
- package/plugins/sp/scripts/record-feature-sync.ts +0 -84
- package/plugins/sp/scripts/script-contract-check.ts +0 -506
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
- package/plugins/sp/scripts/task-size-precheck.ts +0 -212
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
- package/web/_astro/channel.CI6N_tCg.js +0 -1
- package/web/_astro/index.CENnIEqT.css +0 -1
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/residual-scan.ts
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
-
import { createHash } from "crypto";
|
|
7
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
8
7
|
import { tmpdir } from "os";
|
|
9
8
|
import { join } from "path";
|
|
@@ -13,11 +12,11 @@ function getEnvVars() {
|
|
|
13
12
|
return process.env;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
// plugins/sp/
|
|
17
|
-
|
|
15
|
+
// plugins/sp/lib/residual-scan.generated.mjs
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
18
17
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
19
18
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
20
|
-
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)
|
|
19
|
+
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|—)\s*(\(.*\))?\.?$/i;
|
|
21
20
|
var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
22
21
|
var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
23
22
|
var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
@@ -25,16 +24,6 @@ var ANCHOR_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
|
|
|
25
24
|
var RANGE_ANCHOR = /([A-Za-z0-9_./-]+\.[A-Za-z]+):([0-9]+)-[0-9]+/g;
|
|
26
25
|
var EXCLUDED_PATHS = ["docs/tasks", "docs/features/", ".spur/"];
|
|
27
26
|
var ALLOW_PRAGMA = "residual-scan:allow";
|
|
28
|
-
function spurCommand(spurBin) {
|
|
29
|
-
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
30
|
-
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
31
|
-
}
|
|
32
|
-
function run(cmd, args, cwd) {
|
|
33
|
-
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
34
|
-
if (result.error !== undefined)
|
|
35
|
-
return { status: result.status ?? 1, stdout: "" };
|
|
36
|
-
return { status: result.status ?? 1, stdout: result.stdout ?? "" };
|
|
37
|
-
}
|
|
38
27
|
function makeItemId(category, location, text) {
|
|
39
28
|
const normalized = text.trim().replace(/\s+/g, " ");
|
|
40
29
|
const hex = createHash("sha256").update(`${location}${normalized}`).digest("hex");
|
|
@@ -45,7 +34,7 @@ function normalizeAnchor(location) {
|
|
|
45
34
|
}
|
|
46
35
|
function locationOf(locationCell, finding) {
|
|
47
36
|
const cell = locationCell.trim().replace(/`/g, "");
|
|
48
|
-
if (cell.length > 0 && cell !== "
|
|
37
|
+
if (cell.length > 0 && cell !== "—")
|
|
49
38
|
return normalizeAnchor(cell);
|
|
50
39
|
const backtick = finding.match(/`([^`]+)`/)?.[1];
|
|
51
40
|
return backtick === undefined ? "" : normalizeAnchor(backtick);
|
|
@@ -96,42 +85,6 @@ function parseReviewFindings(taskContent) {
|
|
|
96
85
|
function splitRow(line) {
|
|
97
86
|
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
98
87
|
}
|
|
99
|
-
function collectAddedLines(root, base) {
|
|
100
|
-
const out = [];
|
|
101
|
-
const diff = run("git", ["diff", "--unified=0", base], root);
|
|
102
|
-
let file = "";
|
|
103
|
-
let newLine = 0;
|
|
104
|
-
for (const line of diff.stdout.split(`
|
|
105
|
-
`)) {
|
|
106
|
-
if (line.startsWith("+++ b/"))
|
|
107
|
-
file = line.slice(6);
|
|
108
|
-
else if (line.startsWith("@@")) {
|
|
109
|
-
const m = line.match(/\+[0-9]+/);
|
|
110
|
-
newLine = m === null ? newLine : Number.parseInt(m[0].slice(1), 10);
|
|
111
|
-
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
112
|
-
out.push({ file, line: newLine, text: line.slice(1) });
|
|
113
|
-
newLine++;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const untracked = run("git", ["ls-files", "--others", "--exclude-standard"], root);
|
|
117
|
-
for (const f of untracked.stdout.split(`
|
|
118
|
-
`)) {
|
|
119
|
-
if (f.length === 0)
|
|
120
|
-
continue;
|
|
121
|
-
try {
|
|
122
|
-
if (!statSync(join(root, f)).isFile())
|
|
123
|
-
continue;
|
|
124
|
-
} catch {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const content = readFileSync(join(root, f), "utf8").split(`
|
|
128
|
-
`);
|
|
129
|
-
content.forEach((text, idx) => {
|
|
130
|
-
out.push({ file: f, line: idx + 1, text });
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
88
|
function parseDiffMarkers(addedLines) {
|
|
136
89
|
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() }));
|
|
137
90
|
}
|
|
@@ -140,34 +93,6 @@ function findUncheckedBoxes(taskContent) {
|
|
|
140
93
|
return taskContent.split(`
|
|
141
94
|
`).map((text, idx) => ({ text: text.trim(), line: idx + 1 })).filter((l) => l.text.startsWith("- [ ]")).map((l) => ({ location: `${path}:${l.line}`, text: l.text }));
|
|
142
95
|
}
|
|
143
|
-
function listStagingResidue(tmpDir, wbs) {
|
|
144
|
-
let names;
|
|
145
|
-
try {
|
|
146
|
-
names = readdirSync(tmpDir);
|
|
147
|
-
} catch {
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
return names.filter((n) => n.startsWith(`${wbs}-`)).filter((n) => {
|
|
151
|
-
try {
|
|
152
|
-
return statSync(join(tmpDir, n)).isFile();
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}).map((n) => join(tmpDir, n));
|
|
157
|
-
}
|
|
158
|
-
function readDeferrals(runDir, wbs) {
|
|
159
|
-
const path = join(runDir, `${wbs}-residual-deferrals.json`);
|
|
160
|
-
if (!existsSync(path))
|
|
161
|
-
return [];
|
|
162
|
-
try {
|
|
163
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
164
|
-
if (!Array.isArray(parsed))
|
|
165
|
-
return [];
|
|
166
|
-
return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0);
|
|
167
|
-
} catch {
|
|
168
|
-
return [];
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
96
|
function classify(items, deferrals) {
|
|
172
97
|
const deferred = new Map(deferrals.map((d) => [d.id, d.reason]));
|
|
173
98
|
return items.map((item) => {
|
|
@@ -195,10 +120,8 @@ function classify(items, deferrals) {
|
|
|
195
120
|
};
|
|
196
121
|
});
|
|
197
122
|
}
|
|
198
|
-
function scanResiduals(
|
|
199
|
-
const
|
|
200
|
-
const basePath = join(runDir, `${wbs}-base.sha`);
|
|
201
|
-
const base = existsSync(basePath) ? readFileSync(basePath, "utf8").trim() : null;
|
|
123
|
+
function scanResiduals(inputs) {
|
|
124
|
+
const { wbs, base, taskContent, addedLines, stagingResidue, deferrals } = inputs;
|
|
202
125
|
const reviewRows = parseReviewFindings(taskContent);
|
|
203
126
|
const tableDeferrals = reviewRows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
|
|
204
127
|
const review = reviewRows.map((r) => ({
|
|
@@ -207,7 +130,7 @@ function scanResiduals(root, wbs, tmpDir, taskContent, _env) {
|
|
|
207
130
|
location: r.location,
|
|
208
131
|
text: r.text
|
|
209
132
|
}));
|
|
210
|
-
const markers = base === null ? [] : parseDiffMarkers(
|
|
133
|
+
const markers = base === null ? [] : parseDiffMarkers(addedLines).map((m) => ({
|
|
211
134
|
category: "diff-marker",
|
|
212
135
|
location: m.location,
|
|
213
136
|
text: m.text
|
|
@@ -217,12 +140,12 @@ function scanResiduals(root, wbs, tmpDir, taskContent, _env) {
|
|
|
217
140
|
location: b.location,
|
|
218
141
|
text: b.text
|
|
219
142
|
}));
|
|
220
|
-
const residue =
|
|
143
|
+
const residue = stagingResidue.map((p) => ({
|
|
221
144
|
category: "staging-residue",
|
|
222
145
|
location: p,
|
|
223
146
|
text: p
|
|
224
147
|
}));
|
|
225
|
-
const items = classify([...review, ...markers, ...boxes, ...residue], [...tableDeferrals, ...
|
|
148
|
+
const items = classify([...review, ...markers, ...boxes, ...residue], [...tableDeferrals, ...deferrals]);
|
|
226
149
|
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
227
150
|
for (const item of items)
|
|
228
151
|
counts[item.class]++;
|
|
@@ -271,7 +194,7 @@ function foldVerdict(verdict, scan, existingFindings, maxFindings = 20) {
|
|
|
271
194
|
}
|
|
272
195
|
function renderReport(wbs, items, attemptCount) {
|
|
273
196
|
const lines = [
|
|
274
|
-
`# Residual report
|
|
197
|
+
`# Residual report — ${wbs}`,
|
|
275
198
|
"",
|
|
276
199
|
`Attempt: ${attemptCount}`,
|
|
277
200
|
"",
|
|
@@ -285,6 +208,87 @@ function renderReport(wbs, items, attemptCount) {
|
|
|
285
208
|
`)}
|
|
286
209
|
`;
|
|
287
210
|
}
|
|
211
|
+
|
|
212
|
+
// plugins/sp/lib/spur-bin.ts
|
|
213
|
+
function spurCommand(spurBin) {
|
|
214
|
+
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
215
|
+
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// plugins/sp/scripts/residual-scan.ts
|
|
219
|
+
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
220
|
+
function run(cmd, args, cwd) {
|
|
221
|
+
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
222
|
+
if (result.error !== undefined)
|
|
223
|
+
return { status: result.status ?? 1, stdout: "" };
|
|
224
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? "" };
|
|
225
|
+
}
|
|
226
|
+
function spur(env, spurBinFlag, args, cwd) {
|
|
227
|
+
const { cmd, prefix } = spurCommand(spurBinFlag ?? env.spurBin);
|
|
228
|
+
return run(cmd, [...prefix, ...args], cwd);
|
|
229
|
+
}
|
|
230
|
+
function isRegularFile(path) {
|
|
231
|
+
try {
|
|
232
|
+
return statSync(path).isFile();
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function collectAddedLines(root, base) {
|
|
238
|
+
const out = [];
|
|
239
|
+
const diff = run("git", ["diff", "--unified=0", base], root);
|
|
240
|
+
let file = "";
|
|
241
|
+
let newLine = 0;
|
|
242
|
+
for (const line of diff.stdout.split(`
|
|
243
|
+
`)) {
|
|
244
|
+
if (line.startsWith("+++ b/"))
|
|
245
|
+
file = line.slice(6);
|
|
246
|
+
else if (line.startsWith("@@"))
|
|
247
|
+
newLine = Number.parseInt((line.match(/\+[0-9]+/) ?? ["+0"])[0].slice(1), 10);
|
|
248
|
+
else if (line.startsWith("+") && !line.startsWith("+++"))
|
|
249
|
+
out.push({ file, line: newLine++, text: line.slice(1) });
|
|
250
|
+
}
|
|
251
|
+
const untracked = run("git", ["ls-files", "--others", "--exclude-standard"], root);
|
|
252
|
+
for (const f of untracked.stdout.split(`
|
|
253
|
+
`)) {
|
|
254
|
+
if (f.length === 0 || !isRegularFile(join(root, f)))
|
|
255
|
+
continue;
|
|
256
|
+
for (const [idx, text] of readFileSync(join(root, f), "utf8").split(`
|
|
257
|
+
`).entries())
|
|
258
|
+
out.push({ file: f, line: idx + 1, text });
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function listStagingResidue(tmpDir, wbs) {
|
|
263
|
+
let names;
|
|
264
|
+
try {
|
|
265
|
+
names = readdirSync(tmpDir);
|
|
266
|
+
} catch {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
return names.filter((n) => n.startsWith(`${wbs}-`) && isRegularFile(join(tmpDir, n))).map((n) => join(tmpDir, n));
|
|
270
|
+
}
|
|
271
|
+
function readDeferrals(runDir, wbs) {
|
|
272
|
+
const path = join(runDir, `${wbs}-residual-deferrals.json`);
|
|
273
|
+
if (!existsSync(path))
|
|
274
|
+
return [];
|
|
275
|
+
const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
278
|
+
return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
|
|
279
|
+
} catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function scanResiduals2(root, wbs, tmpDir, taskContent, _env) {
|
|
284
|
+
const runDir = join(root, ".spur", "run");
|
|
285
|
+
const basePath = join(runDir, `${wbs}-base.sha`);
|
|
286
|
+
const base = existsSync(basePath) ? readFileSync(basePath, "utf8").trim() : null;
|
|
287
|
+
const stagingResidue = listStagingResidue(tmpDir, wbs);
|
|
288
|
+
const deferrals = readDeferrals(runDir, wbs);
|
|
289
|
+
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
290
|
+
return scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
|
|
291
|
+
}
|
|
288
292
|
function parseArgs(argv) {
|
|
289
293
|
let mode = "";
|
|
290
294
|
let wbs = "";
|
|
@@ -308,31 +312,24 @@ function parseArgs(argv) {
|
|
|
308
312
|
else if (wbs === "")
|
|
309
313
|
wbs = a;
|
|
310
314
|
}
|
|
311
|
-
|
|
312
|
-
return null;
|
|
313
|
-
return { mode, wbs, spurBin, root, tmpDir };
|
|
314
|
-
}
|
|
315
|
-
function spur(env, spurBinFlag, args, cwd) {
|
|
316
|
-
return run(spurCommand(spurBinFlag ?? env.spurBin).cmd, [...spurCommand(spurBinFlag ?? env.spurBin).prefix, ...args], cwd);
|
|
315
|
+
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
317
316
|
}
|
|
318
317
|
function loadTask(env, spurBinFlag, wbs, root) {
|
|
319
318
|
const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
|
|
320
319
|
if (res.status !== 0)
|
|
321
320
|
throw new Error(`task show ${wbs} failed`);
|
|
322
321
|
const parsed = JSON.parse(res.stdout);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
const featureId = typeof parsed.feature_id === "string" ? parsed.feature_id : fm !== null && typeof fm === "object" && typeof fm.feature_id === "string" ? fm.feature_id : "";
|
|
326
|
-
return { content, featureId };
|
|
322
|
+
const featureId = [parsed.feature_id, parsed.frontmatter?.feature_id].find((v) => typeof v === "string") ?? "";
|
|
323
|
+
return { content: typeof parsed.content === "string" ? parsed.content : "", featureId };
|
|
327
324
|
}
|
|
328
325
|
function loadVerdict(runDir, wbs) {
|
|
329
326
|
return JSON.parse(readFileSync(join(runDir, `${wbs}-verdict.json`), "utf8"));
|
|
330
327
|
}
|
|
331
328
|
function scanMode(opts, env, io) {
|
|
332
|
-
const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
333
329
|
const runDir = join(opts.root, ".spur", "run");
|
|
334
330
|
mkdirSync(runDir, { recursive: true });
|
|
335
|
-
const
|
|
331
|
+
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
332
|
+
const artifact = scanResiduals2(opts.root, opts.wbs, opts.tmpDir, content, env);
|
|
336
333
|
writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}
|
|
337
334
|
`);
|
|
338
335
|
io.out(`residual-scan: ${opts.wbs} blocking=${artifact.counts.blocking} deferrable=${artifact.counts.deferrable} advisory=${artifact.counts.advisory} housekeeping=${artifact.counts.housekeeping}
|
|
@@ -345,59 +342,54 @@ function foldMode(opts, _env, io) {
|
|
|
345
342
|
const verdictPath = join(runDir, `${opts.wbs}-verdict.json`);
|
|
346
343
|
const verdict = loadVerdict(runDir, opts.wbs);
|
|
347
344
|
const findingsPath = join(runDir, `${opts.wbs}-test-gate.findings`);
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
writeFileSync(verdictPath, `${JSON.stringify({ ...verdict, verdict: folded.verdict, checks: folded.checks }, null, 2)}
|
|
345
|
+
const fold = foldVerdict(verdict, scan, existsSync(findingsPath) ? readFileSync(findingsPath, "utf8") : "");
|
|
346
|
+
writeFileSync(verdictPath, `${JSON.stringify({ ...verdict, verdict: fold.verdict, checks: fold.checks }, null, 2)}
|
|
351
347
|
`);
|
|
352
|
-
writeFileSync(findingsPath,
|
|
353
|
-
io.out(`residual-fold: ${opts.wbs} verdict=${
|
|
348
|
+
writeFileSync(findingsPath, fold.findings);
|
|
349
|
+
io.out(`residual-fold: ${opts.wbs} verdict=${fold.verdict} residual-sweep=${fold.checks.find((c) => c.name === "residual-sweep")?.status}
|
|
354
350
|
`);
|
|
355
351
|
return 0;
|
|
356
352
|
}
|
|
357
353
|
function settleMode(opts, env, io) {
|
|
358
|
-
const
|
|
354
|
+
const wbs = opts.wbs;
|
|
355
|
+
const task = loadTask(env, opts.spurBin, wbs, opts.root);
|
|
359
356
|
const runDir = join(opts.root, ".spur", "run");
|
|
360
|
-
const scan =
|
|
361
|
-
const residualsPath = join(runDir, `${
|
|
357
|
+
const scan = scanResiduals2(opts.root, wbs, opts.tmpDir, task.content, env);
|
|
358
|
+
const residualsPath = join(runDir, `${wbs}-residuals.json`);
|
|
359
|
+
let prior = {};
|
|
360
|
+
if (existsSync(residualsPath))
|
|
361
|
+
prior = JSON.parse(readFileSync(residualsPath, "utf8"));
|
|
362
362
|
const deferred = scan.items.filter((i) => i.class === "deferrable");
|
|
363
|
-
const prior = existsSync(residualsPath) ? JSON.parse(readFileSync(residualsPath, "utf8")) : {};
|
|
364
363
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
365
364
|
if (task.featureId === "") {
|
|
366
|
-
io.err(`residual-settle: ${
|
|
367
|
-
`);
|
|
368
|
-
return 0;
|
|
369
|
-
}
|
|
370
|
-
const created = spur(env, opts.spurBin, ["task", "create", `Residuals from ${opts.wbs}`, "--feature", task.featureId, "--skip-ready", "--json"], opts.root);
|
|
371
|
-
if (created.status !== 0) {
|
|
372
|
-
io.err(`residual-settle: task create failed; re-run: residual-scan settle ${opts.wbs}
|
|
365
|
+
io.err(`residual-settle: ${wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${wbs}
|
|
373
366
|
`);
|
|
374
367
|
return 0;
|
|
375
368
|
}
|
|
369
|
+
const args = ["task", "create", `Residuals from ${wbs}`, "--feature", task.featureId, "--skip-ready", "--json"];
|
|
370
|
+
const created = spur(env, opts.spurBin, args, opts.root);
|
|
376
371
|
let wbsNew = "";
|
|
377
372
|
try {
|
|
378
373
|
const parsed = JSON.parse(created.stdout);
|
|
379
|
-
const
|
|
380
|
-
wbsNew =
|
|
374
|
+
const data = typeof parsed.data === "object" && parsed.data !== null ? parsed.data : {};
|
|
375
|
+
wbsNew = [parsed.wbs, data.wbs].find((v) => typeof v === "string") ?? "";
|
|
381
376
|
} catch {
|
|
382
377
|
wbsNew = "";
|
|
383
378
|
}
|
|
384
|
-
if (wbsNew === "") {
|
|
385
|
-
io.err(`residual-settle:
|
|
379
|
+
if (created.status !== 0 || wbsNew === "") {
|
|
380
|
+
io.err(`residual-settle: task create failed or unreadable wbs; re-run: residual-scan settle ${wbs}
|
|
386
381
|
`);
|
|
387
382
|
return 0;
|
|
388
383
|
}
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
`);
|
|
395
|
-
const bgFile = join(runDir, `${opts.wbs}-residual-background.md`);
|
|
396
|
-
writeFileSync(bgFile, `${bg}
|
|
384
|
+
const rows = deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`);
|
|
385
|
+
const head = `Source task: ${wbs} (feature ${task.featureId}) \u2014 deferred residuals filed by residual-scan settle.`;
|
|
386
|
+
const bgFile = join(runDir, `${wbs}-residual-background.md`);
|
|
387
|
+
writeFileSync(bgFile, `${[head, "", ...rows].join(`
|
|
388
|
+
`)}
|
|
397
389
|
`);
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
400
|
-
io.err(`residual-settle: background write failed; re-run: residual-scan settle ${
|
|
390
|
+
const updArgs = ["task", "update", wbsNew, "--section", "Background", "--from-file", bgFile];
|
|
391
|
+
if (spur(env, opts.spurBin, updArgs, opts.root).status !== 0) {
|
|
392
|
+
io.err(`residual-settle: background write failed; re-run: residual-scan settle ${wbs}
|
|
401
393
|
`);
|
|
402
394
|
return 0;
|
|
403
395
|
}
|
|
@@ -405,11 +397,11 @@ function settleMode(opts, env, io) {
|
|
|
405
397
|
io.out(`residual-settle: filed follow-up ${wbsNew} for ${deferred.length} deferred item(s)
|
|
406
398
|
`);
|
|
407
399
|
}
|
|
408
|
-
for (const path of listStagingResidue(opts.tmpDir,
|
|
400
|
+
for (const path of listStagingResidue(opts.tmpDir, wbs)) {
|
|
409
401
|
try {
|
|
410
402
|
rmSync(path, { force: true });
|
|
411
403
|
} catch {
|
|
412
|
-
io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${
|
|
404
|
+
io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${wbs}
|
|
413
405
|
`);
|
|
414
406
|
return 0;
|
|
415
407
|
}
|
|
@@ -420,17 +412,15 @@ function settleMode(opts, env, io) {
|
|
|
420
412
|
}
|
|
421
413
|
function reportMode(opts, env, io) {
|
|
422
414
|
const runDir = join(opts.root, ".spur", "run");
|
|
423
|
-
const
|
|
424
|
-
const sweep = verdict.checks.find((c) => c.name === "residual-sweep");
|
|
415
|
+
const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
|
|
425
416
|
if (sweep === undefined || sweep.status !== "fail")
|
|
426
417
|
return 0;
|
|
427
|
-
const
|
|
428
|
-
const scan =
|
|
418
|
+
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
419
|
+
const scan = scanResiduals2(opts.root, opts.wbs, opts.tmpDir, content, env);
|
|
429
420
|
const blocking = scan.items.filter((i) => i.class === "blocking");
|
|
430
421
|
const attemptFile = join(runDir, `${opts.wbs}-test-fix-attempt`);
|
|
431
422
|
const attempts = existsSync(attemptFile) ? Number.parseInt(readFileSync(attemptFile, "utf8").trim() || "0", 10) : 0;
|
|
432
|
-
|
|
433
|
-
writeFileSync(reportPath, renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts));
|
|
423
|
+
writeFileSync(join(runDir, `${opts.wbs}-residual-report.md`), renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts));
|
|
434
424
|
io.out(`Recovery: fix the items in .spur/run/${opts.wbs}-residual-report.md, then /sp:dev-run ${opts.wbs}
|
|
435
425
|
`);
|
|
436
426
|
return 0;
|
|
@@ -440,32 +430,21 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
440
430
|
out: (line) => process.stdout.write(line),
|
|
441
431
|
err: (line) => process.stderr.write(line)
|
|
442
432
|
};
|
|
433
|
+
const cwd = options.cwd ?? process.cwd();
|
|
443
434
|
const opts = parseArgs(argv);
|
|
444
|
-
|
|
435
|
+
const modes = { scan: scanMode, fold: foldMode, settle: settleMode, report: reportMode };
|
|
436
|
+
const modeFn = opts === null ? undefined : modes[opts.mode];
|
|
437
|
+
if (opts === null || modeFn === undefined) {
|
|
445
438
|
io.err(`${RESIDUAL_SCAN_USAGE}
|
|
446
439
|
`);
|
|
447
440
|
return 2;
|
|
448
441
|
}
|
|
449
|
-
const cwd = options.cwd ?? process.cwd();
|
|
450
442
|
const resolved = { ...opts, root: opts.root.startsWith("/") ? opts.root : join(cwd, opts.root) };
|
|
451
|
-
|
|
452
|
-
return scanMode(resolved, env, io);
|
|
453
|
-
if (resolved.mode === "fold")
|
|
454
|
-
return foldMode(resolved, env, io);
|
|
455
|
-
if (resolved.mode === "settle")
|
|
456
|
-
return settleMode(resolved, env, io);
|
|
457
|
-
if (resolved.mode === "report")
|
|
458
|
-
return reportMode(resolved, env, io);
|
|
459
|
-
io.err(`${RESIDUAL_SCAN_USAGE}
|
|
460
|
-
`);
|
|
461
|
-
return 2;
|
|
462
|
-
}
|
|
463
|
-
{
|
|
464
|
-
process.exit(main(process.argv.slice(2)));
|
|
443
|
+
return modeFn(resolved, env, io);
|
|
465
444
|
}
|
|
445
|
+
process.exit(main(process.argv.slice(2)));
|
|
466
446
|
export {
|
|
467
|
-
|
|
468
|
-
scanResiduals,
|
|
447
|
+
scanResiduals2 as scanResiduals,
|
|
469
448
|
renderReport,
|
|
470
449
|
parseReviewFindings,
|
|
471
450
|
parseDiffMarkers,
|