@gobing-ai/spur 0.4.8 → 0.4.9
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/pipeline-budgets.json +2 -2
- package/config/rules/boundary/config-loading-ownership.yaml +1 -0
- package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
- package/config/rules/boundary/sp-script-placement.yaml +1 -1
- package/config/rules/docs/sourced-api-claims.yaml +45 -7
- package/config/rules/strict/runtime-boundaries.yaml +40 -1
- package/config/rules/structure/protected-files.yaml +1 -1
- package/config/rules/surface/check-cli-surface.yaml +33 -0
- package/config/rules/typescript/no-debugger.yaml +3 -0
- package/config/workflow-candidates.json +2 -2
- package/config/workflows/feature-verification.yaml +6 -11
- package/config/workflows/history-anatomy.yaml +14 -28
- package/config/workflows/idea-pipeline.yaml +6 -19
- package/config/workflows/pr-review.yaml +9 -16
- package/config/workflows/task-pipeline.yaml +16 -23
- package/config/workflows/wrapup-pipeline.yaml +21 -63
- package/package.json +3 -3
- package/plugins/sp/README.md +70 -37
- package/plugins/sp/agents/super-planner.md +5 -5
- package/plugins/sp/agents/super-reviewer.md +2 -2
- package/plugins/sp/commands/dev-changelog.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-pr-review.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +1 -1
- package/plugins/sp/commands/dev-runall.md +4 -4
- package/plugins/sp/commands/dev-verify.md +4 -6
- package/plugins/sp/commands/dev-verifyall.md +4 -5
- package/plugins/sp/hooks/hooks.json +11 -1
- package/plugins/sp/lib/context-post-tool.ts +1 -1
- package/plugins/sp/lib/context-session-start.ts +1 -1
- package/plugins/sp/lib/context-session-stop.ts +1 -1
- package/plugins/sp/lib/destructive-policy.ts +65 -1
- package/plugins/sp/plugin.json +2 -5
- package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
- package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
- package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
- package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
- package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
- package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
- package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -8
- package/plugins/sp/skills/code-verification/references/secu-review.md +4 -3
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
- package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
- package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/parallel-execution/SKILL.md +1 -1
- package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
- package/plugins/sp/skills/session-review/SKILL.md +3 -3
- package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
- package/plugins/sp/skills/spur-check/SKILL.md +14 -13
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
- package/plugins/sp/skills/spur-cli/references/agent.md +12 -0
- package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/history.md +3 -0
- package/plugins/sp/skills/spur-cli/references/self.md +5 -3
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
- package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
- package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -12
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +8 -11
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
- package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
- package/spur.js +41901 -30598
- package/config/plugin-scripts.json +0 -116
- package/config/script-placement-baseline.json +0 -33
- package/plugins/sp/hooks/careful-guard.ts +0 -74
- package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
- package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
- package/plugins/sp/lib/dogfood-report.ts +0 -278
- package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
- package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
- package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
- package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
- package/plugins/sp/lib/inline-run-app.ts +0 -68
- package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
- package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
- package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
- package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
- package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
- package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
- package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
- package/plugins/sp/lib/run-summary-core.ts +0 -225
- package/plugins/sp/lib/spur-bin.ts +0 -36
- package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
- package/plugins/sp/lib/step-profile.generated.mjs +0 -174
- package/plugins/sp/lib/transcript.ts +0 -112
- package/plugins/sp/scripts/batch-plan.mjs +0 -1096
- package/plugins/sp/scripts/batch-plan.ts +0 -145
- package/plugins/sp/scripts/batch-preflight.mjs +0 -423
- package/plugins/sp/scripts/batch-preflight.ts +0 -270
- package/plugins/sp/scripts/commit-guard.mjs +0 -226
- package/plugins/sp/scripts/commit-guard.ts +0 -248
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
- package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
- package/plugins/sp/scripts/idea-handoff.mjs +0 -32
- package/plugins/sp/scripts/idea-handoff.ts +0 -47
- package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
- package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
- package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
- package/plugins/sp/scripts/inline-run-setup.ts +0 -198
- package/plugins/sp/scripts/persist-out-check.mjs +0 -217
- package/plugins/sp/scripts/persist-out-check.ts +0 -249
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
- package/plugins/sp/scripts/pr-reviewing.ts +0 -952
- package/plugins/sp/scripts/quality-gate.mjs +0 -30491
- package/plugins/sp/scripts/quality-gate.ts +0 -80
- package/plugins/sp/scripts/residual-scan.mjs +0 -634
- package/plugins/sp/scripts/residual-scan.ts +0 -248
- package/plugins/sp/scripts/run-summary.mjs +0 -758
- package/plugins/sp/scripts/run-summary.ts +0 -107
- package/plugins/sp/scripts/script-root.mjs +0 -129
- package/plugins/sp/scripts/script-root.ts +0 -199
- package/plugins/sp/scripts/session-timeline.mjs +0 -359
- package/plugins/sp/scripts/session-timeline.ts +0 -190
- package/plugins/sp/scripts/task-diffstat.mjs +0 -181
- package/plugins/sp/scripts/task-diffstat.ts +0 -219
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
- package/plugins/sp/scripts/wrapup-steps.ts +0 -891
|
@@ -1,634 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @bun
|
|
3
|
-
|
|
4
|
-
// plugins/sp/scripts/residual-scan.ts
|
|
5
|
-
import { spawnSync } from "child_process";
|
|
6
|
-
import * as fs from "fs";
|
|
7
|
-
import { tmpdir } from "os";
|
|
8
|
-
import { join as join2 } from "path";
|
|
9
|
-
|
|
10
|
-
// plugins/sp/lib/env.ts
|
|
11
|
-
function getEnvVars() {
|
|
12
|
-
return process.env;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// plugins/sp/lib/residual-scan.generated.mjs
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
|
-
var CLAIM_MARKERS = [
|
|
19
|
-
/@[a-z0-9._-]+\/[a-z0-9._-]+/i,
|
|
20
|
-
/\bnode_modules\//,
|
|
21
|
-
/\b\d+\.\d+\.\d+\b/,
|
|
22
|
-
/`[A-Za-z_$][\w.$]*\s*\([^`]*\)`/
|
|
23
|
-
];
|
|
24
|
-
var CITATION_FORMS = [
|
|
25
|
-
/`[^`\s]+:\d+(?:-\d+)?`/,
|
|
26
|
-
/`[^`]+`\s+lines?\s+\d+/i,
|
|
27
|
-
/\b[\w./-]+\.[A-Za-z0-9]+:\d+(?:-\d+)?\b/,
|
|
28
|
-
/https?:\/\/\S+/i
|
|
29
|
-
];
|
|
30
|
-
function hasClaimMarker(evidence) {
|
|
31
|
-
return CLAIM_MARKERS.some((re) => re.test(evidence));
|
|
32
|
-
}
|
|
33
|
-
function hasCitationForm(evidence) {
|
|
34
|
-
return CITATION_FORMS.some((re) => re.test(evidence));
|
|
35
|
-
}
|
|
36
|
-
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
37
|
-
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
38
|
-
var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
|
|
39
|
-
var NONE_FINDING = /^(none( found)?|no (findings?|issues?)( found)?|\u2014)\s*(\(.*\))?\.?$/i;
|
|
40
|
-
var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
|
|
41
|
-
var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
|
|
42
|
-
var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
|
|
43
|
-
var ANCHOR_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
|
|
44
|
-
var RANGE_ANCHOR = /([A-Za-z0-9_./-]+\.[A-Za-z]+):([0-9]+)-[0-9]+/g;
|
|
45
|
-
var EXCLUDED_PATHS = ["docs/tasks", "docs/features/", ".spur/"];
|
|
46
|
-
var ALLOW_PRAGMA = "residual-scan:allow";
|
|
47
|
-
function makeItemId(category, location, text) {
|
|
48
|
-
const normalized = text.trim().replace(/\s+/g, " ");
|
|
49
|
-
const hex = createHash("sha256").update(`${location}${normalized}`).digest("hex");
|
|
50
|
-
return `${category}:${hex.slice(0, 8)}`;
|
|
51
|
-
}
|
|
52
|
-
function normalizeAnchor(location) {
|
|
53
|
-
return location.replace(RANGE_ANCHOR, "$1:$2");
|
|
54
|
-
}
|
|
55
|
-
function locationOf(locationCell, finding) {
|
|
56
|
-
const cell = locationCell.trim().replace(/`/g, "");
|
|
57
|
-
if (cell.length > 0 && cell !== "—")
|
|
58
|
-
return normalizeAnchor(cell);
|
|
59
|
-
const backtick = finding.match(/`([^`]+)`/)?.[1];
|
|
60
|
-
return backtick === undefined ? "" : normalizeAnchor(backtick);
|
|
61
|
-
}
|
|
62
|
-
function reviewBody(taskContent) {
|
|
63
|
-
const section = taskContent.split(/^### Review\b/m)[1];
|
|
64
|
-
return section === undefined ? null : section.split(/^### /m)[0] ?? "";
|
|
65
|
-
}
|
|
66
|
-
function parseReviewFindings(taskContent) {
|
|
67
|
-
const section = reviewBody(taskContent);
|
|
68
|
-
if (section === null)
|
|
69
|
-
return [];
|
|
70
|
-
const body = section;
|
|
71
|
-
const out = [];
|
|
72
|
-
const lines = body.split(`
|
|
73
|
-
`);
|
|
74
|
-
for (let i = 0;i < lines.length; i++) {
|
|
75
|
-
const line = lines[i];
|
|
76
|
-
if (line === undefined || !line.trimStart().startsWith("|"))
|
|
77
|
-
continue;
|
|
78
|
-
const header = splitRow(line);
|
|
79
|
-
const priorityCol = header.findIndex((h) => h.trim() === "Priority");
|
|
80
|
-
if (priorityCol === -1) {
|
|
81
|
-
while (i + 1 < lines.length && lines[i + 1]?.trimStart().startsWith("|"))
|
|
82
|
-
i++;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const findingCol = header.findIndex((h) => h.trim() === "Finding");
|
|
86
|
-
const locationCol = header.findIndex((h) => h.trim() === "Location");
|
|
87
|
-
const dispositionCol = header.findIndex((h) => DISPOSITION_HEADER.test(h.trim()));
|
|
88
|
-
i++;
|
|
89
|
-
const sep = lines[i];
|
|
90
|
-
if (sep !== undefined && /^\s*\|[\s:|-]+\|\s*$/.test(sep))
|
|
91
|
-
i++;
|
|
92
|
-
while (i < lines.length) {
|
|
93
|
-
const row = lines[i];
|
|
94
|
-
if (row === undefined || !row.trimStart().startsWith("|"))
|
|
95
|
-
break;
|
|
96
|
-
const cells = splitRow(row);
|
|
97
|
-
const priority = (cells[priorityCol] ?? "").trim();
|
|
98
|
-
const finding = (cells[findingCol] ?? "").trim();
|
|
99
|
-
const disposition = dispositionCol === -1 ? "" : (cells[dispositionCol] ?? "").trim();
|
|
100
|
-
if (PRIORITY_PATTERN.test(priority) && !RANGE_PRIORITY.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
|
|
101
|
-
const location = locationOf(cells[locationCol] ?? "", finding);
|
|
102
|
-
out.push(DEFERRED_DISPOSITION.test(disposition) ? { priority, location, text: finding, deferral: disposition } : { priority, location, text: finding });
|
|
103
|
-
}
|
|
104
|
-
i++;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
function splitRow(line) {
|
|
110
|
-
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(/(?<!\\)\|/).map((c) => c.trim());
|
|
111
|
-
}
|
|
112
|
-
function parseDiffMarkers(addedLines) {
|
|
113
|
-
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() }));
|
|
114
|
-
}
|
|
115
|
-
function findUncheckedBoxes(taskContent) {
|
|
116
|
-
const path = "task-file";
|
|
117
|
-
return taskContent.split(`
|
|
118
|
-
`).map((text, idx) => ({ text: text.trim(), line: idx + 1 })).filter((l) => l.text.startsWith("- [ ]")).map((l) => ({ location: `${path}:${l.line}`, text: l.text }));
|
|
119
|
-
}
|
|
120
|
-
function classify(items, deferrals) {
|
|
121
|
-
const deferred = new Map(deferrals.map((d) => [d.id, d.reason]));
|
|
122
|
-
return items.map((item) => {
|
|
123
|
-
const id = makeItemId(item.category, item.location, item.text);
|
|
124
|
-
let klass;
|
|
125
|
-
if (item.category === "review-finding")
|
|
126
|
-
klass = item.priority?.startsWith("P4") ? "advisory" : "blocking";
|
|
127
|
-
else if (item.category === "staging-residue")
|
|
128
|
-
klass = "housekeeping";
|
|
129
|
-
else
|
|
130
|
-
klass = "blocking";
|
|
131
|
-
if (klass === "blocking" && item.category !== "unchecked-box") {
|
|
132
|
-
const p3Like = item.category === "diff-marker" || item.category === "review-finding" && (item.priority ?? "").startsWith("P3");
|
|
133
|
-
const reason = deferred.get(id);
|
|
134
|
-
if (p3Like && reason !== undefined && reason.trim().length > 0)
|
|
135
|
-
klass = "deferrable";
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
id,
|
|
139
|
-
category: item.category,
|
|
140
|
-
class: klass,
|
|
141
|
-
priority: item.priority,
|
|
142
|
-
location: item.location,
|
|
143
|
-
text: item.text
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
function scanResiduals(inputs) {
|
|
148
|
-
const { wbs, base, taskContent, addedLines, stagingResidue, deferrals } = inputs;
|
|
149
|
-
const reviewRows = parseReviewFindings(taskContent);
|
|
150
|
-
const tableDeferrals = reviewRows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
|
|
151
|
-
const review = reviewRows.map((r) => ({
|
|
152
|
-
category: "review-finding",
|
|
153
|
-
priority: r.priority,
|
|
154
|
-
location: r.location,
|
|
155
|
-
text: r.text
|
|
156
|
-
}));
|
|
157
|
-
const markers = base === null ? [] : parseDiffMarkers(addedLines).map((m) => ({
|
|
158
|
-
category: "diff-marker",
|
|
159
|
-
location: m.location,
|
|
160
|
-
text: m.text
|
|
161
|
-
}));
|
|
162
|
-
const boxes = findUncheckedBoxes(taskContent).map((b) => ({
|
|
163
|
-
category: "unchecked-box",
|
|
164
|
-
location: b.location,
|
|
165
|
-
text: b.text
|
|
166
|
-
}));
|
|
167
|
-
const residue = stagingResidue.map((p) => ({
|
|
168
|
-
category: "staging-residue",
|
|
169
|
-
location: p,
|
|
170
|
-
text: p
|
|
171
|
-
}));
|
|
172
|
-
const items = classify([...review, ...markers, ...boxes, ...residue], [...tableDeferrals, ...deferrals]);
|
|
173
|
-
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
174
|
-
for (const item of items)
|
|
175
|
-
counts[item.class]++;
|
|
176
|
-
return {
|
|
177
|
-
wbs,
|
|
178
|
-
base,
|
|
179
|
-
scanned: {
|
|
180
|
-
"review-finding": true,
|
|
181
|
-
"diff-marker": base !== null,
|
|
182
|
-
"unchecked-box": true,
|
|
183
|
-
"staging-residue": true,
|
|
184
|
-
"review-contract": false
|
|
185
|
-
},
|
|
186
|
-
items,
|
|
187
|
-
counts
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
function parseDeferralEntries(raw) {
|
|
191
|
-
const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
|
|
192
|
-
return (Array.isArray(raw) ? raw : []).filter(isDeferral);
|
|
193
|
-
}
|
|
194
|
-
function blockingReviewFindings(taskContent, deferrals) {
|
|
195
|
-
const rows = parseReviewFindings(taskContent);
|
|
196
|
-
const tableDeferrals = rows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
|
|
197
|
-
const items = rows.map((r) => ({
|
|
198
|
-
category: "review-finding",
|
|
199
|
-
priority: r.priority,
|
|
200
|
-
location: r.location,
|
|
201
|
-
text: r.text
|
|
202
|
-
}));
|
|
203
|
-
return classify(items, [...tableDeferrals, ...deferrals]);
|
|
204
|
-
}
|
|
205
|
-
function reviewContractViolations(taskContent) {
|
|
206
|
-
const violations = [];
|
|
207
|
-
for (const row of parseReviewFindings(taskContent)) {
|
|
208
|
-
if (hasClaimMarker(row.text) && !hasCitationForm(row.text)) {
|
|
209
|
-
violations.push({
|
|
210
|
-
location: row.location.length > 0 ? row.location : "task-file:Review",
|
|
211
|
-
text: `${row.priority} finding asserts an external API/library/version claim without a ` + "same-cell citation (add a repo `path:line`, an external named-origin form, or a URL " + `to the Finding cell): ${row.text.slice(0, 80)}`
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return violations;
|
|
216
|
-
}
|
|
217
|
-
function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
218
|
-
const contractViolations = reviewContractViolations(taskContent);
|
|
219
|
-
const items = [
|
|
220
|
-
...blockingReviewFindings(taskContent, deferrals),
|
|
221
|
-
...contractViolations.map((v) => ({
|
|
222
|
-
id: makeItemId("review-contract", v.location, v.text),
|
|
223
|
-
category: "review-contract",
|
|
224
|
-
class: "blocking",
|
|
225
|
-
location: v.location,
|
|
226
|
-
text: v.text
|
|
227
|
-
}))
|
|
228
|
-
];
|
|
229
|
-
const blocking = items.filter((i) => i.class === "blocking");
|
|
230
|
-
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
231
|
-
for (const item of items)
|
|
232
|
-
counts[item.class]++;
|
|
233
|
-
const artifact = {
|
|
234
|
-
wbs,
|
|
235
|
-
base: null,
|
|
236
|
-
scanned: {
|
|
237
|
-
"review-finding": true,
|
|
238
|
-
"diff-marker": false,
|
|
239
|
-
"unchecked-box": false,
|
|
240
|
-
"staging-residue": false,
|
|
241
|
-
"review-contract": true
|
|
242
|
-
},
|
|
243
|
-
items,
|
|
244
|
-
counts
|
|
245
|
-
};
|
|
246
|
-
const note = `residual-review-gate: ${wbs} blocking=${counts.blocking}` + (blocking.length > 0 ? ` ids=${blocking.map((i) => i.id).join(",")} anchors=${blockingAnchors(blocking).join(",")}` : "") + `
|
|
247
|
-
`;
|
|
248
|
-
return { artifact, note };
|
|
249
|
-
}
|
|
250
|
-
function blockingAnchors(items) {
|
|
251
|
-
const anchors = new Set;
|
|
252
|
-
for (const item of items) {
|
|
253
|
-
if (item.class !== "blocking")
|
|
254
|
-
continue;
|
|
255
|
-
for (const m of normalizeAnchor(item.location).matchAll(ANCHOR_PATTERN))
|
|
256
|
-
anchors.add(m[0]);
|
|
257
|
-
}
|
|
258
|
-
return [...anchors];
|
|
259
|
-
}
|
|
260
|
-
function foldVerdict(verdict, scan, existingFindings, maxFindings = 20) {
|
|
261
|
-
const blocking = scan.items.filter((i) => i.class === "blocking");
|
|
262
|
-
const deferrable = scan.items.filter((i) => i.class === "deferrable");
|
|
263
|
-
const evidence = `blocking=${blocking.length} deferrable=${deferrable.length} advisory=${scan.counts.advisory} housekeeping=${scan.counts.housekeeping}` + (blocking.length > 0 ? `; blocking ids: ${blocking.map((i) => i.id).join(", ")}` : "") + (deferrable.length > 0 ? `; deferrable ids: ${deferrable.map((i) => i.id).join(", ")}` : "");
|
|
264
|
-
const checks = verdict.checks.filter((c) => c.name !== "residual-sweep");
|
|
265
|
-
checks.push({ name: "residual-sweep", status: blocking.length > 0 ? "fail" : "pass", evidence });
|
|
266
|
-
const merged = new Set([
|
|
267
|
-
...existingFindings.split(/\s+/).filter((a) => a.length > 0),
|
|
268
|
-
...blockingAnchors(scan.items)
|
|
269
|
-
]);
|
|
270
|
-
const findings = [...merged].sort().slice(0, maxFindings).map((a) => `${a} `).join("");
|
|
271
|
-
let verdictStatus = verdict.verdict === "PASS" ? "PASS" : "FAIL";
|
|
272
|
-
if (verdict.verdict === "PARTIAL")
|
|
273
|
-
verdictStatus = "PARTIAL";
|
|
274
|
-
else if (verdict.verdict === "FAIL")
|
|
275
|
-
verdictStatus = "FAIL";
|
|
276
|
-
else if (blocking.length > 0)
|
|
277
|
-
verdictStatus = "PARTIAL";
|
|
278
|
-
return { verdict: verdictStatus, checks, findings };
|
|
279
|
-
}
|
|
280
|
-
function renderReport(wbs, items, attemptCount) {
|
|
281
|
-
const lines = [
|
|
282
|
-
`# Residual report — ${wbs}`,
|
|
283
|
-
"",
|
|
284
|
-
`Attempt: ${attemptCount}`,
|
|
285
|
-
"",
|
|
286
|
-
"| Category | Class | Location | Text |",
|
|
287
|
-
"| --- | --- | --- | --- |"
|
|
288
|
-
];
|
|
289
|
-
for (const item of items) {
|
|
290
|
-
lines.push(`| ${item.category} | ${item.class} | ${item.location} | ${item.text.replace(/\|/g, "\\|")} |`);
|
|
291
|
-
}
|
|
292
|
-
return `${lines.join(`
|
|
293
|
-
`)}
|
|
294
|
-
`;
|
|
295
|
-
}
|
|
296
|
-
function recordedVerdictPath(runDir, wbs, fs) {
|
|
297
|
-
const evidence = join(runDir, "..", "memory", "evidence");
|
|
298
|
-
const durable = join(evidence, `${wbs}-verdict.json`);
|
|
299
|
-
const run = join(runDir, `${wbs}-verdict.json`);
|
|
300
|
-
for (const path of [join(runDir, ".."), join(evidence, ".."), evidence, durable, run]) {
|
|
301
|
-
try {
|
|
302
|
-
if (fs.lstatSync(path).isSymbolicLink())
|
|
303
|
-
throw new Error(`residual-scan: symlink evidence path: ${path}`);
|
|
304
|
-
} catch (error) {
|
|
305
|
-
if (error.code !== "ENOENT")
|
|
306
|
-
throw error;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (!fs.existsSync(durable))
|
|
310
|
-
return run;
|
|
311
|
-
if (!fs.existsSync(run))
|
|
312
|
-
return durable;
|
|
313
|
-
const newer = (a, b) => mtimeOf(fs, b) > mtimeOf(fs, a) ? b : a;
|
|
314
|
-
return newer(run, durable);
|
|
315
|
-
}
|
|
316
|
-
function mtimeOf(fs, path) {
|
|
317
|
-
try {
|
|
318
|
-
return fs.statSync(path).mtimeMs;
|
|
319
|
-
} catch {
|
|
320
|
-
return 0;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
function verdictDisagreementNote(runDir, wbs, fs) {
|
|
324
|
-
const run = join(runDir, `${wbs}-verdict.json`);
|
|
325
|
-
const durable = join(runDir, "..", "memory", "evidence", `${wbs}-verdict.json`);
|
|
326
|
-
if (!fs.existsSync(run) || !fs.existsSync(durable))
|
|
327
|
-
return null;
|
|
328
|
-
const bytes = (path) => {
|
|
329
|
-
try {
|
|
330
|
-
return fs.statSync(path).isFile() ? fs.readFileSync(path, "utf8") : null;
|
|
331
|
-
} catch {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
const runBytes = bytes(run);
|
|
336
|
-
if (runBytes === null || runBytes === bytes(durable))
|
|
337
|
-
return null;
|
|
338
|
-
const value = (path) => {
|
|
339
|
-
try {
|
|
340
|
-
return JSON.parse(bytes(path) ?? "{}").verdict ?? "?";
|
|
341
|
-
} catch {
|
|
342
|
-
return "?";
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
const winner = mtimeOf(fs, durable) > mtimeOf(fs, run) ? "durable" : "run";
|
|
346
|
-
return `residual-fold: ${wbs} verdict copies disagree — run=${run} (${value(run)}) durable=${durable} (${value(durable)})` + ` → chose ${winner} (newer mtime)`;
|
|
347
|
-
}
|
|
348
|
-
function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
349
|
-
let mode = "";
|
|
350
|
-
let wbs = "";
|
|
351
|
-
let spurBin;
|
|
352
|
-
let root = cwd;
|
|
353
|
-
let tmpDir = defaultTmpDir;
|
|
354
|
-
for (let i = 0;i < argv.length; i++) {
|
|
355
|
-
const a = argv[i];
|
|
356
|
-
if (a === undefined)
|
|
357
|
-
break;
|
|
358
|
-
if (a === "--spur-bin")
|
|
359
|
-
spurBin = argv[++i];
|
|
360
|
-
else if (a === "--root")
|
|
361
|
-
root = argv[++i] ?? root;
|
|
362
|
-
else if (a === "--tmp-dir")
|
|
363
|
-
tmpDir = argv[++i] ?? tmpDir;
|
|
364
|
-
else if (a === "--help" || a === "-h")
|
|
365
|
-
return null;
|
|
366
|
-
else if (mode === "")
|
|
367
|
-
mode = a;
|
|
368
|
-
else if (wbs === "")
|
|
369
|
-
wbs = a;
|
|
370
|
-
}
|
|
371
|
-
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// plugins/sp/lib/spur-bin.ts
|
|
375
|
-
function spurCommand(spurBin) {
|
|
376
|
-
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
377
|
-
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// plugins/sp/scripts/residual-scan.ts
|
|
381
|
-
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
382
|
-
function run(cmd, args, cwd) {
|
|
383
|
-
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
384
|
-
if (result.error !== undefined)
|
|
385
|
-
return { status: result.status ?? 1, stdout: "" };
|
|
386
|
-
return { status: result.status ?? 1, stdout: result.stdout ?? "" };
|
|
387
|
-
}
|
|
388
|
-
function spur(env, spurBinFlag, args, cwd) {
|
|
389
|
-
const { cmd, prefix } = spurCommand(spurBinFlag ?? env.spurBin);
|
|
390
|
-
return run(cmd, [...prefix, ...args], cwd);
|
|
391
|
-
}
|
|
392
|
-
function isRegularFile(path) {
|
|
393
|
-
try {
|
|
394
|
-
return fs.statSync(path).isFile();
|
|
395
|
-
} catch {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
function collectAddedLines(root, base) {
|
|
400
|
-
const out = [];
|
|
401
|
-
const diff = run("git", ["diff", "--unified=0", base], root);
|
|
402
|
-
let file = "";
|
|
403
|
-
let newLine = 0;
|
|
404
|
-
for (const line of diff.stdout.split(`
|
|
405
|
-
`)) {
|
|
406
|
-
if (line.startsWith("+++ b/"))
|
|
407
|
-
file = line.slice(6);
|
|
408
|
-
else if (line.startsWith("@@"))
|
|
409
|
-
newLine = Number.parseInt((line.match(/\+[0-9]+/) ?? ["+0"])[0].slice(1), 10);
|
|
410
|
-
else if (line.startsWith("+") && !line.startsWith("+++"))
|
|
411
|
-
out.push({ file, line: newLine++, text: line.slice(1) });
|
|
412
|
-
}
|
|
413
|
-
const untracked = run("git", ["ls-files", "--others", "--exclude-standard"], root);
|
|
414
|
-
for (const f of untracked.stdout.split(`
|
|
415
|
-
`)) {
|
|
416
|
-
if (f.length === 0 || !isRegularFile(join2(root, f)))
|
|
417
|
-
continue;
|
|
418
|
-
for (const [idx, text] of fs.readFileSync(join2(root, f), "utf8").split(`
|
|
419
|
-
`).entries())
|
|
420
|
-
out.push({ file: f, line: idx + 1, text });
|
|
421
|
-
}
|
|
422
|
-
return out;
|
|
423
|
-
}
|
|
424
|
-
function listStagingResidue(tmpDir, wbs) {
|
|
425
|
-
let names;
|
|
426
|
-
try {
|
|
427
|
-
names = fs.readdirSync(tmpDir);
|
|
428
|
-
} catch {
|
|
429
|
-
return [];
|
|
430
|
-
}
|
|
431
|
-
return names.filter((n) => n.startsWith(`${wbs}-`) && isRegularFile(join2(tmpDir, n))).map((n) => join2(tmpDir, n));
|
|
432
|
-
}
|
|
433
|
-
function readDeferrals(runDir, wbs) {
|
|
434
|
-
const path = join2(runDir, `${wbs}-residual-deferrals.json`);
|
|
435
|
-
if (!fs.existsSync(path))
|
|
436
|
-
return [];
|
|
437
|
-
try {
|
|
438
|
-
return parseDeferralEntries(JSON.parse(fs.readFileSync(path, "utf8")));
|
|
439
|
-
} catch {
|
|
440
|
-
return [];
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
function scanResiduals2(root, wbs, tmpDir, taskContent, _env) {
|
|
444
|
-
const runDir = join2(root, ".spur", "run");
|
|
445
|
-
const basePath = join2(runDir, `${wbs}-base.sha`);
|
|
446
|
-
const base = fs.existsSync(basePath) ? fs.readFileSync(basePath, "utf8").trim() : null;
|
|
447
|
-
const stagingResidue = listStagingResidue(tmpDir, wbs);
|
|
448
|
-
const deferrals = readDeferrals(runDir, wbs);
|
|
449
|
-
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
450
|
-
return scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
|
|
451
|
-
}
|
|
452
|
-
function loadTask(env, spurBinFlag, wbs, root) {
|
|
453
|
-
const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
|
|
454
|
-
if (res.status !== 0)
|
|
455
|
-
throw new Error(`task show ${wbs} failed`);
|
|
456
|
-
const parsed = JSON.parse(res.stdout);
|
|
457
|
-
const featureId = [parsed.feature_id, parsed.frontmatter?.feature_id].find((v) => typeof v === "string") ?? "";
|
|
458
|
-
return { content: typeof parsed.content === "string" ? parsed.content : "", featureId };
|
|
459
|
-
}
|
|
460
|
-
var loadVerdict = (runDir, wbs) => JSON.parse(fs.readFileSync(recordedVerdictPath(runDir, wbs, fs), "utf8"));
|
|
461
|
-
function scanMode(opts, env, io) {
|
|
462
|
-
const runDir = join2(opts.root, ".spur", "run");
|
|
463
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
464
|
-
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
465
|
-
const artifact = scanResiduals2(opts.root, opts.wbs, opts.tmpDir, content, env);
|
|
466
|
-
fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}
|
|
467
|
-
`);
|
|
468
|
-
io.out(`residual-scan: ${opts.wbs} blocking=${artifact.counts.blocking} deferrable=${artifact.counts.deferrable} advisory=${artifact.counts.advisory} housekeeping=${artifact.counts.housekeeping}
|
|
469
|
-
`);
|
|
470
|
-
return 0;
|
|
471
|
-
}
|
|
472
|
-
function reviewGateMode(opts, env, io) {
|
|
473
|
-
const runDir = join2(opts.root, ".spur", "run");
|
|
474
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
475
|
-
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
476
|
-
const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
477
|
-
fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
|
|
478
|
-
`);
|
|
479
|
-
io.out(gate.note);
|
|
480
|
-
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
481
|
-
}
|
|
482
|
-
function foldMode(opts, _env, io) {
|
|
483
|
-
const runDir = join2(opts.root, ".spur", "run");
|
|
484
|
-
const scan = JSON.parse(fs.readFileSync(join2(runDir, `${opts.wbs}-residuals.json`), "utf8"));
|
|
485
|
-
const target = recordedVerdictPath(runDir, opts.wbs, fs);
|
|
486
|
-
const note = verdictDisagreementNote(runDir, opts.wbs, fs);
|
|
487
|
-
if (note !== null)
|
|
488
|
-
io.out(`${note}
|
|
489
|
-
`);
|
|
490
|
-
const verdict = loadVerdict(runDir, opts.wbs);
|
|
491
|
-
const findingsPath = join2(runDir, `${opts.wbs}-test-gate.findings`);
|
|
492
|
-
const fold = foldVerdict(verdict, scan, fs.existsSync(findingsPath) ? fs.readFileSync(findingsPath, "utf8") : "");
|
|
493
|
-
const bytes = `${JSON.stringify({ ...verdict, verdict: fold.verdict, checks: fold.checks }, null, 2)}
|
|
494
|
-
`;
|
|
495
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
496
|
-
try {
|
|
497
|
-
fs.writeFileSync(temporary, bytes, { flag: "wx" });
|
|
498
|
-
fs.renameSync(temporary, target);
|
|
499
|
-
} finally {
|
|
500
|
-
fs.rmSync(temporary, { force: true });
|
|
501
|
-
}
|
|
502
|
-
const scratch = join2(runDir, `${opts.wbs}-verdict.json`);
|
|
503
|
-
if (target !== scratch)
|
|
504
|
-
fs.writeFileSync(scratch, bytes);
|
|
505
|
-
fs.writeFileSync(findingsPath, fold.findings);
|
|
506
|
-
io.out(`residual-fold: ${opts.wbs} verdict=${fold.verdict} residual-sweep=${fold.checks.find((c) => c.name === "residual-sweep")?.status}
|
|
507
|
-
`);
|
|
508
|
-
return 0;
|
|
509
|
-
}
|
|
510
|
-
function settleMode(opts, env, io) {
|
|
511
|
-
const wbs = opts.wbs;
|
|
512
|
-
const task = loadTask(env, opts.spurBin, wbs, opts.root);
|
|
513
|
-
const runDir = join2(opts.root, ".spur", "run");
|
|
514
|
-
const scan = scanResiduals2(opts.root, wbs, opts.tmpDir, task.content, env);
|
|
515
|
-
const residualsPath = join2(runDir, `${wbs}-residuals.json`);
|
|
516
|
-
let prior = {};
|
|
517
|
-
if (fs.existsSync(residualsPath))
|
|
518
|
-
prior = JSON.parse(fs.readFileSync(residualsPath, "utf8"));
|
|
519
|
-
const deferred = scan.items.filter((i) => i.class === "deferrable");
|
|
520
|
-
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
521
|
-
const args = ["task", "create", `Residuals from ${wbs}`, "--skip-ready", "--json"];
|
|
522
|
-
const created = spur(env, opts.spurBin, args, opts.root);
|
|
523
|
-
let wbsNew = "";
|
|
524
|
-
try {
|
|
525
|
-
const parsed = JSON.parse(created.stdout);
|
|
526
|
-
const data = typeof parsed.data === "object" && parsed.data !== null ? parsed.data : {};
|
|
527
|
-
wbsNew = [parsed.wbs, data.wbs].find((v) => typeof v === "string") ?? "";
|
|
528
|
-
} catch {
|
|
529
|
-
wbsNew = "";
|
|
530
|
-
}
|
|
531
|
-
if (created.status !== 0 || wbsNew === "") {
|
|
532
|
-
io.err(`residual-settle: task create failed or unreadable wbs; re-run: residual-scan settle ${wbs}
|
|
533
|
-
`);
|
|
534
|
-
return 0;
|
|
535
|
-
}
|
|
536
|
-
const rows = deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`);
|
|
537
|
-
const head = `Source task: ${wbs}${task.featureId === "" ? "" : ` (feature ${task.featureId})`} \u2014 deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
|
|
538
|
-
const bgFile = join2(runDir, `${wbs}-residual-background.md`);
|
|
539
|
-
fs.writeFileSync(bgFile, `${[head, "", ...rows].join(`
|
|
540
|
-
`)}
|
|
541
|
-
`);
|
|
542
|
-
const updArgs = ["task", "update", wbsNew, "--section", "Background", "--from-file", bgFile];
|
|
543
|
-
if (spur(env, opts.spurBin, updArgs, opts.root).status !== 0) {
|
|
544
|
-
io.err(`residual-settle: background write failed; re-run: residual-scan settle ${wbs}
|
|
545
|
-
`);
|
|
546
|
-
return 0;
|
|
547
|
-
}
|
|
548
|
-
prior.followUp = wbsNew;
|
|
549
|
-
io.out(`residual-settle: filed follow-up ${wbsNew} for ${deferred.length} deferred item(s)
|
|
550
|
-
`);
|
|
551
|
-
}
|
|
552
|
-
for (const path of listStagingResidue(opts.tmpDir, wbs)) {
|
|
553
|
-
try {
|
|
554
|
-
fs.rmSync(path, { force: true });
|
|
555
|
-
} catch {
|
|
556
|
-
io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${wbs}
|
|
557
|
-
`);
|
|
558
|
-
return 0;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
fs.writeFileSync(residualsPath, `${JSON.stringify({ ...scan, ...prior }, null, 2)}
|
|
562
|
-
`);
|
|
563
|
-
return 0;
|
|
564
|
-
}
|
|
565
|
-
function reportMode(opts, env, io) {
|
|
566
|
-
const runDir = join2(opts.root, ".spur", "run");
|
|
567
|
-
const verdictPath = recordedVerdictPath(runDir, opts.wbs, fs);
|
|
568
|
-
if (!fs.existsSync(verdictPath)) {
|
|
569
|
-
io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}
|
|
570
|
-
`);
|
|
571
|
-
return 0;
|
|
572
|
-
}
|
|
573
|
-
const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
|
|
574
|
-
if (sweep === undefined || sweep.status !== "fail")
|
|
575
|
-
return 0;
|
|
576
|
-
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
577
|
-
const scan = scanResiduals2(opts.root, opts.wbs, opts.tmpDir, content, env);
|
|
578
|
-
const blocking = scan.items.filter((i) => i.class === "blocking");
|
|
579
|
-
const attemptFile = join2(runDir, `${opts.wbs}-test-fix-attempt`);
|
|
580
|
-
const attempts = fs.existsSync(attemptFile) ? Number.parseInt(fs.readFileSync(attemptFile, "utf8").trim() || "0", 10) : 0;
|
|
581
|
-
fs.writeFileSync(join2(runDir, `${opts.wbs}-residual-report.md`), renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts));
|
|
582
|
-
io.out(`Recovery: fix the items in .spur/run/${opts.wbs}-residual-report.md, then /sp:dev-run ${opts.wbs}
|
|
583
|
-
`);
|
|
584
|
-
return 0;
|
|
585
|
-
}
|
|
586
|
-
function main(argv, env = getEnvVars(), options = {}) {
|
|
587
|
-
const io = options.io ?? {
|
|
588
|
-
out: (line) => process.stdout.write(line),
|
|
589
|
-
err: (line) => process.stderr.write(line)
|
|
590
|
-
};
|
|
591
|
-
const cwd = options.cwd ?? process.cwd();
|
|
592
|
-
const opts = parseScanArgs(argv, cwd, tmpdir());
|
|
593
|
-
const modes = {
|
|
594
|
-
scan: scanMode,
|
|
595
|
-
fold: foldMode,
|
|
596
|
-
settle: settleMode,
|
|
597
|
-
report: reportMode,
|
|
598
|
-
"review-gate": reviewGateMode
|
|
599
|
-
};
|
|
600
|
-
const modeFn = opts === null ? undefined : modes[opts.mode];
|
|
601
|
-
if (opts === null || modeFn === undefined) {
|
|
602
|
-
io.err(`${RESIDUAL_SCAN_USAGE}
|
|
603
|
-
`);
|
|
604
|
-
return 2;
|
|
605
|
-
}
|
|
606
|
-
const resolved = { ...opts, root: opts.root.startsWith("/") ? opts.root : join2(cwd, opts.root) };
|
|
607
|
-
return modeFn(resolved, env, io);
|
|
608
|
-
}
|
|
609
|
-
process.exit(main(process.argv.slice(2)));
|
|
610
|
-
export {
|
|
611
|
-
ALLOW_PRAGMA,
|
|
612
|
-
RESIDUAL_SCAN_USAGE,
|
|
613
|
-
blockingAnchors,
|
|
614
|
-
blockingReviewFindings,
|
|
615
|
-
buildReviewGateArtifact,
|
|
616
|
-
classify,
|
|
617
|
-
collectAddedLines,
|
|
618
|
-
findUncheckedBoxes,
|
|
619
|
-
foldVerdict,
|
|
620
|
-
listStagingResidue,
|
|
621
|
-
locationOf,
|
|
622
|
-
main,
|
|
623
|
-
makeItemId,
|
|
624
|
-
normalizeAnchor,
|
|
625
|
-
parseDeferralEntries,
|
|
626
|
-
parseDiffMarkers,
|
|
627
|
-
parseReviewFindings,
|
|
628
|
-
parseScanArgs,
|
|
629
|
-
recordedVerdictPath,
|
|
630
|
-
renderReport,
|
|
631
|
-
reviewContractViolations,
|
|
632
|
-
scanResiduals2 as scanResiduals,
|
|
633
|
-
verdictDisagreementNote
|
|
634
|
-
};
|