@gobing-ai/spur 0.4.0 → 0.4.1
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/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/plugin-scripts.json +15 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +216 -57
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +11 -1
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +14 -0
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +3 -1
- package/plugins/sp/commands/dev-run.md +12 -2
- package/plugins/sp/commands/dev-runall.md +18 -10
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/lib/idea-handoff.generated.mjs +41 -5
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
- package/plugins/sp/lib/inline-run.generated.mjs +333 -45
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +121 -6
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +551 -80
- package/plugins/sp/scripts/inline-run-setup.ts +52 -78
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +86 -31
- package/plugins/sp/scripts/residual-scan.ts +28 -30
- package/plugins/sp/scripts/run-summary.mjs +421 -14
- package/plugins/sp/scripts/run-summary.ts +41 -177
- package/plugins/sp/scripts/session-timeline.mjs +109 -12
- package/plugins/sp/scripts/session-timeline.ts +71 -19
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +3 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +38 -7
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
- package/plugins/sp/skills/spur-cli/references/decision.md +7 -6
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -16
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -1020
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +35 -5
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +343 -163
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +20728 -16630
- package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/{BoardApp.Bzr4OXrW.js → BoardApp.D6sjAJr_.js} +173 -173
- package/web/_astro/{TaskDetail.LAyR_Pvn.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.f4e0OYLg.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.DguUUe6V.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/channel.CUFdtWhm.js +0 -1
- package/web/_astro/index.Dg_3bOaJ.css +0 -1
|
@@ -17,12 +17,12 @@ import { appendFileSync } from 'node:fs';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { getEnvVars } from '../lib/env';
|
|
19
19
|
import type { QualityGateEnv, QualityGateOptions } from '../lib/quality-gate.generated.mjs';
|
|
20
|
-
import {
|
|
20
|
+
import { resolveReceiptReuse, runDeferredGate, runLightGate, runQualityGate } from '../lib/quality-gate.generated.mjs';
|
|
21
21
|
|
|
22
22
|
export * from '../lib/quality-gate.generated.mjs';
|
|
23
23
|
|
|
24
24
|
export const QUALITY_GATE_USAGE =
|
|
25
|
-
'usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
|
|
25
|
+
'usage: quality-gate.ts <run|recheck|light|deferred|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
|
|
26
26
|
|
|
27
27
|
export function main(
|
|
28
28
|
argv: string[],
|
|
@@ -30,7 +30,7 @@ export function main(
|
|
|
30
30
|
options: QualityGateOptions = {},
|
|
31
31
|
): number {
|
|
32
32
|
const mode = argv[0];
|
|
33
|
-
if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'status') {
|
|
33
|
+
if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'deferred' && mode !== 'status') {
|
|
34
34
|
process.stderr.write(`${QUALITY_GATE_USAGE}\n`);
|
|
35
35
|
return 2;
|
|
36
36
|
}
|
|
@@ -42,10 +42,26 @@ export function main(
|
|
|
42
42
|
const env: QualityGateEnv = { ...rawEnv, wbs };
|
|
43
43
|
if (mode === 'light') {
|
|
44
44
|
runLightGate(env);
|
|
45
|
+
} else if (mode === 'deferred') {
|
|
46
|
+
// 1111 R1: light tier for per-task feedback, `DEFERRED`/`FAIL` for the status token —
|
|
47
|
+
// exit 0 on DEFERRED so the YAML's `; exit 0` wrapper and the action result agree.
|
|
48
|
+
return runDeferredGate(env, options).status === 'DEFERRED' ? 0 : 1;
|
|
45
49
|
} else if (mode === 'status') {
|
|
46
50
|
const runDir = join(options.cwd ?? '.', '.spur', 'run');
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
// Task 1136 R3: the gate owns its reuse identity — recompute the proof-input fingerprint
|
|
52
|
+
// before consulting the receipt, so a caller-copied digest can never trigger reuse.
|
|
53
|
+
const decision = resolveReceiptReuse(
|
|
54
|
+
join(runDir, `${env.wbs}-check-receipt.json`),
|
|
55
|
+
env,
|
|
56
|
+
options.cwd ?? process.cwd(),
|
|
57
|
+
options.recomputeFingerprint,
|
|
58
|
+
);
|
|
59
|
+
const verdict = decision.receiptStatus;
|
|
60
|
+
if (decision.refusal !== undefined) {
|
|
61
|
+
const line = `check.reuse-refused — ${decision.refusal}\n`;
|
|
62
|
+
process.stdout.write(line); // tee: stdout and the log
|
|
63
|
+
appendFileSync(join(runDir, `${env.wbs}-test-gate.log`), line);
|
|
64
|
+
} else if (verdict.reuse) {
|
|
49
65
|
// 0940 R3: reuse is observable in the gate log and on stdout (the action result
|
|
50
66
|
// `data`); the `{reuse, reason}` JSON stays the last stdout line for machine readers.
|
|
51
67
|
const line = `check.reused — full-tier receipt reused for input digest ${env.proofDigest ?? ''}\n`;
|
|
@@ -164,6 +164,43 @@ function scanResiduals(inputs) {
|
|
|
164
164
|
counts
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
|
+
function parseDeferralEntries(raw) {
|
|
168
|
+
const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
|
|
169
|
+
return (Array.isArray(raw) ? raw : []).filter(isDeferral);
|
|
170
|
+
}
|
|
171
|
+
function blockingReviewFindings(taskContent, deferrals) {
|
|
172
|
+
const rows = parseReviewFindings(taskContent);
|
|
173
|
+
const tableDeferrals = rows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
|
|
174
|
+
const items = rows.map((r) => ({
|
|
175
|
+
category: "review-finding",
|
|
176
|
+
priority: r.priority,
|
|
177
|
+
location: r.location,
|
|
178
|
+
text: r.text
|
|
179
|
+
}));
|
|
180
|
+
return classify(items, [...tableDeferrals, ...deferrals]);
|
|
181
|
+
}
|
|
182
|
+
function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
183
|
+
const items = blockingReviewFindings(taskContent, deferrals);
|
|
184
|
+
const blocking = items.filter((i) => i.class === "blocking");
|
|
185
|
+
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
186
|
+
for (const item of items)
|
|
187
|
+
counts[item.class]++;
|
|
188
|
+
const artifact = {
|
|
189
|
+
wbs,
|
|
190
|
+
base: null,
|
|
191
|
+
scanned: {
|
|
192
|
+
"review-finding": true,
|
|
193
|
+
"diff-marker": false,
|
|
194
|
+
"unchecked-box": false,
|
|
195
|
+
"staging-residue": false
|
|
196
|
+
},
|
|
197
|
+
items,
|
|
198
|
+
counts
|
|
199
|
+
};
|
|
200
|
+
const note = `residual-review-gate: ${wbs} blocking=${counts.blocking}` + (blocking.length > 0 ? ` ids=${blocking.map((i) => i.id).join(",")} anchors=${blockingAnchors(blocking).join(",")}` : "") + `
|
|
201
|
+
`;
|
|
202
|
+
return { artifact, note };
|
|
203
|
+
}
|
|
167
204
|
function blockingAnchors(items) {
|
|
168
205
|
const anchors = new Set;
|
|
169
206
|
for (const item of items) {
|
|
@@ -262,6 +299,31 @@ function verdictDisagreementNote(runDir, wbs, fs) {
|
|
|
262
299
|
const winner = mtimeOf(fs, durable) > mtimeOf(fs, run) ? "durable" : "run";
|
|
263
300
|
return `residual-fold: ${wbs} verdict copies disagree — run=${run} (${value(run)}) durable=${durable} (${value(durable)})` + ` → chose ${winner} (newer mtime)`;
|
|
264
301
|
}
|
|
302
|
+
function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
303
|
+
let mode = "";
|
|
304
|
+
let wbs = "";
|
|
305
|
+
let spurBin;
|
|
306
|
+
let root = cwd;
|
|
307
|
+
let tmpDir = defaultTmpDir;
|
|
308
|
+
for (let i = 0;i < argv.length; i++) {
|
|
309
|
+
const a = argv[i];
|
|
310
|
+
if (a === undefined)
|
|
311
|
+
break;
|
|
312
|
+
if (a === "--spur-bin")
|
|
313
|
+
spurBin = argv[++i];
|
|
314
|
+
else if (a === "--root")
|
|
315
|
+
root = argv[++i] ?? root;
|
|
316
|
+
else if (a === "--tmp-dir")
|
|
317
|
+
tmpDir = argv[++i] ?? tmpDir;
|
|
318
|
+
else if (a === "--help" || a === "-h")
|
|
319
|
+
return null;
|
|
320
|
+
else if (mode === "")
|
|
321
|
+
mode = a;
|
|
322
|
+
else if (wbs === "")
|
|
323
|
+
wbs = a;
|
|
324
|
+
}
|
|
325
|
+
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
326
|
+
}
|
|
265
327
|
|
|
266
328
|
// plugins/sp/lib/spur-bin.ts
|
|
267
329
|
function spurCommand(spurBin) {
|
|
@@ -270,7 +332,7 @@ function spurCommand(spurBin) {
|
|
|
270
332
|
}
|
|
271
333
|
|
|
272
334
|
// plugins/sp/scripts/residual-scan.ts
|
|
273
|
-
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
335
|
+
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
274
336
|
function run(cmd, args, cwd) {
|
|
275
337
|
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
276
338
|
if (result.error !== undefined)
|
|
@@ -326,10 +388,8 @@ function readDeferrals(runDir, wbs) {
|
|
|
326
388
|
const path = join2(runDir, `${wbs}-residual-deferrals.json`);
|
|
327
389
|
if (!fs.existsSync(path))
|
|
328
390
|
return [];
|
|
329
|
-
const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
|
|
330
391
|
try {
|
|
331
|
-
|
|
332
|
-
return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
|
|
392
|
+
return parseDeferralEntries(JSON.parse(fs.readFileSync(path, "utf8")));
|
|
333
393
|
} catch {
|
|
334
394
|
return [];
|
|
335
395
|
}
|
|
@@ -343,31 +403,6 @@ function scanResiduals2(root, wbs, tmpDir, taskContent, _env) {
|
|
|
343
403
|
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
344
404
|
return scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
|
|
345
405
|
}
|
|
346
|
-
function parseArgs(argv) {
|
|
347
|
-
let mode = "";
|
|
348
|
-
let wbs = "";
|
|
349
|
-
let spurBin;
|
|
350
|
-
let root = process.cwd();
|
|
351
|
-
let tmpDir = tmpdir();
|
|
352
|
-
for (let i = 0;i < argv.length; i++) {
|
|
353
|
-
const a = argv[i];
|
|
354
|
-
if (a === undefined)
|
|
355
|
-
break;
|
|
356
|
-
if (a === "--spur-bin")
|
|
357
|
-
spurBin = argv[++i];
|
|
358
|
-
else if (a === "--root")
|
|
359
|
-
root = argv[++i] ?? root;
|
|
360
|
-
else if (a === "--tmp-dir")
|
|
361
|
-
tmpDir = argv[++i] ?? tmpDir;
|
|
362
|
-
else if (a === "--help" || a === "-h")
|
|
363
|
-
return null;
|
|
364
|
-
else if (mode === "")
|
|
365
|
-
mode = a;
|
|
366
|
-
else if (wbs === "")
|
|
367
|
-
wbs = a;
|
|
368
|
-
}
|
|
369
|
-
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
370
|
-
}
|
|
371
406
|
function loadTask(env, spurBinFlag, wbs, root) {
|
|
372
407
|
const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
|
|
373
408
|
if (res.status !== 0)
|
|
@@ -388,6 +423,16 @@ function scanMode(opts, env, io) {
|
|
|
388
423
|
`);
|
|
389
424
|
return 0;
|
|
390
425
|
}
|
|
426
|
+
function reviewGateMode(opts, env, io) {
|
|
427
|
+
const runDir = join2(opts.root, ".spur", "run");
|
|
428
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
429
|
+
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
430
|
+
const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
431
|
+
fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
|
|
432
|
+
`);
|
|
433
|
+
io.out(gate.note);
|
|
434
|
+
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
435
|
+
}
|
|
391
436
|
function foldMode(opts, _env, io) {
|
|
392
437
|
const runDir = join2(opts.root, ".spur", "run");
|
|
393
438
|
const scan = JSON.parse(fs.readFileSync(join2(runDir, `${opts.wbs}-residuals.json`), "utf8"));
|
|
@@ -492,8 +537,14 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
492
537
|
err: (line) => process.stderr.write(line)
|
|
493
538
|
};
|
|
494
539
|
const cwd = options.cwd ?? process.cwd();
|
|
495
|
-
const opts =
|
|
496
|
-
const modes = {
|
|
540
|
+
const opts = parseScanArgs(argv, cwd, tmpdir());
|
|
541
|
+
const modes = {
|
|
542
|
+
scan: scanMode,
|
|
543
|
+
fold: foldMode,
|
|
544
|
+
settle: settleMode,
|
|
545
|
+
report: reportMode,
|
|
546
|
+
"review-gate": reviewGateMode
|
|
547
|
+
};
|
|
497
548
|
const modeFn = opts === null ? undefined : modes[opts.mode];
|
|
498
549
|
if (opts === null || modeFn === undefined) {
|
|
499
550
|
io.err(`${RESIDUAL_SCAN_USAGE}
|
|
@@ -509,8 +560,10 @@ export {
|
|
|
509
560
|
scanResiduals2 as scanResiduals,
|
|
510
561
|
renderReport,
|
|
511
562
|
recordedVerdictPath,
|
|
563
|
+
parseScanArgs,
|
|
512
564
|
parseReviewFindings,
|
|
513
565
|
parseDiffMarkers,
|
|
566
|
+
parseDeferralEntries,
|
|
514
567
|
normalizeAnchor,
|
|
515
568
|
makeItemId,
|
|
516
569
|
main,
|
|
@@ -520,6 +573,8 @@ export {
|
|
|
520
573
|
findUncheckedBoxes,
|
|
521
574
|
collectAddedLines,
|
|
522
575
|
classify,
|
|
576
|
+
buildReviewGateArtifact,
|
|
577
|
+
blockingReviewFindings,
|
|
523
578
|
blockingAnchors,
|
|
524
579
|
RESIDUAL_SCAN_USAGE,
|
|
525
580
|
ALLOW_PRAGMA
|
|
@@ -10,12 +10,12 @@ import { spurCommand } from '../lib/spur-bin';
|
|
|
10
10
|
|
|
11
11
|
export * from '../lib/residual-scan.generated.mjs';
|
|
12
12
|
export const RESIDUAL_SCAN_USAGE =
|
|
13
|
-
'usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
|
|
13
|
+
'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
|
|
14
14
|
export type ScanEnv = { spurBin?: string } & Record<string, string | undefined>;
|
|
15
15
|
export type ScanIo = { out: (line: string) => void; err: (line: string) => void };
|
|
16
16
|
export type ScanOptions = { cwd?: string; io?: ScanIo };
|
|
17
17
|
type RunResult = { status: number; stdout: string };
|
|
18
|
-
type ParsedArgs =
|
|
18
|
+
type ParsedArgs = core.ParsedScanArgs;
|
|
19
19
|
type ModeFn = (o: ParsedArgs, e: ScanEnv, i: ScanIo) => number;
|
|
20
20
|
type ScanTask = { content: string; featureId: string };
|
|
21
21
|
type VerdictFile = { verdict: string; checks: Array<{ name: string; status: string; evidence: string }> };
|
|
@@ -66,15 +66,8 @@ export function listStagingResidue(tmpDir: string, wbs: string): string[] {
|
|
|
66
66
|
function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
|
|
67
67
|
const path = join(runDir, `${wbs}-residual-deferrals.json`);
|
|
68
68
|
if (!fs.existsSync(path)) return [];
|
|
69
|
-
const isDeferral = (e: unknown): e is core.Deferral =>
|
|
70
|
-
typeof e === 'object' &&
|
|
71
|
-
e !== null &&
|
|
72
|
-
typeof (e as core.Deferral).id === 'string' &&
|
|
73
|
-
typeof (e as core.Deferral).reason === 'string' &&
|
|
74
|
-
(e as core.Deferral).reason.trim().length > 0;
|
|
75
69
|
try {
|
|
76
|
-
|
|
77
|
-
return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
|
|
70
|
+
return core.parseDeferralEntries(JSON.parse(fs.readFileSync(path, 'utf8')));
|
|
78
71
|
} catch {
|
|
79
72
|
return [];
|
|
80
73
|
}
|
|
@@ -88,24 +81,6 @@ export function scanResiduals(root: string, wbs: string, tmpDir: string, taskCon
|
|
|
88
81
|
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
89
82
|
return core.scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
|
|
90
83
|
}
|
|
91
|
-
function parseArgs(argv: string[]): ParsedArgs | null {
|
|
92
|
-
let mode = '';
|
|
93
|
-
let wbs = '';
|
|
94
|
-
let spurBin: string | undefined;
|
|
95
|
-
let root = process.cwd();
|
|
96
|
-
let tmpDir = tmpdir();
|
|
97
|
-
for (let i = 0; i < argv.length; i++) {
|
|
98
|
-
const a = argv[i];
|
|
99
|
-
if (a === undefined) break;
|
|
100
|
-
if (a === '--spur-bin') spurBin = argv[++i];
|
|
101
|
-
else if (a === '--root') root = argv[++i] ?? root;
|
|
102
|
-
else if (a === '--tmp-dir') tmpDir = argv[++i] ?? tmpDir;
|
|
103
|
-
else if (a === '--help' || a === '-h') return null;
|
|
104
|
-
else if (mode === '') mode = a;
|
|
105
|
-
else if (wbs === '') wbs = a;
|
|
106
|
-
}
|
|
107
|
-
return mode === '' || wbs === '' ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
108
|
-
}
|
|
109
84
|
function loadTask(env: ScanEnv, spurBinFlag: string | undefined, wbs: string, root: string): ScanTask {
|
|
110
85
|
const res = spur(env, spurBinFlag, ['task', 'show', wbs, '--json'], root);
|
|
111
86
|
if (res.status !== 0) throw new Error(`task show ${wbs} failed`);
|
|
@@ -128,6 +103,23 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
128
103
|
);
|
|
129
104
|
return 0;
|
|
130
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Task 1122 R2: the review-time gate — the record sweep's review-finding slice with the same
|
|
108
|
+
* parse/classify/deferrals (core.buildReviewGateArtifact), applied at the review PASS edges
|
|
109
|
+
* before verify spends the cycle. Writes the same `<wbs>-residuals.json` artifact the test-fix
|
|
110
|
+
* hop already appends to the gate log (its remediation input); the record sweep stays the final
|
|
111
|
+
* authority and overwrites it. Exit 1 on an open P1-P3 finding fails the review PASS edge closed
|
|
112
|
+
* into review-fail-triage.
|
|
113
|
+
*/
|
|
114
|
+
function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
115
|
+
const runDir = join(opts.root, '.spur', 'run');
|
|
116
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
117
|
+
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
118
|
+
const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
119
|
+
fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
|
|
120
|
+
io.out(gate.note);
|
|
121
|
+
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
122
|
+
}
|
|
131
123
|
function foldMode(opts: ParsedArgs, _env: ScanEnv, io: ScanIo): number {
|
|
132
124
|
const runDir = join(opts.root, '.spur', 'run');
|
|
133
125
|
const scan = JSON.parse(
|
|
@@ -236,8 +228,14 @@ export function main(argv: string[], env: ScanEnv = getEnvVars(), options: ScanO
|
|
|
236
228
|
err: (line) => process.stderr.write(line),
|
|
237
229
|
};
|
|
238
230
|
const cwd = options.cwd ?? process.cwd();
|
|
239
|
-
const opts =
|
|
240
|
-
const modes: Record<string, ModeFn> = {
|
|
231
|
+
const opts = core.parseScanArgs(argv, cwd, tmpdir());
|
|
232
|
+
const modes: Record<string, ModeFn> = {
|
|
233
|
+
scan: scanMode,
|
|
234
|
+
fold: foldMode,
|
|
235
|
+
settle: settleMode,
|
|
236
|
+
report: reportMode,
|
|
237
|
+
'review-gate': reviewGateMode,
|
|
238
|
+
};
|
|
241
239
|
const modeFn = opts === null ? undefined : modes[opts.mode];
|
|
242
240
|
if (opts === null || modeFn === undefined) {
|
|
243
241
|
io.err(`${RESIDUAL_SCAN_USAGE}\n`);
|