@gobing-ai/spur 0.4.4 → 0.4.6
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/config.example.yaml +3 -4
- package/config/config.global.yaml +3 -4
- package/config/rules/docs/sourced-api-claims.yaml +3 -2
- package/config/rules/quality/coverage-gate.yaml +6 -4
- package/config/rules/recommended-pre-check.yaml +11 -9
- package/config/transition-shims.json +0 -14
- package/config/workflows/task-pipeline.yaml +31 -71
- package/package.json +1 -1
- package/plugins/sp/agents/super-reviewer.md +11 -27
- package/plugins/sp/commands/dev-review.md +1 -1
- package/plugins/sp/lib/inline-run.generated.mjs +3 -3
- package/plugins/sp/lib/residual-scan.generated.d.mts +0 -13
- package/plugins/sp/lib/residual-scan.generated.mjs +3 -47
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/commit-guard.mjs +49 -39
- package/plugins/sp/scripts/commit-guard.ts +60 -61
- package/plugins/sp/scripts/persist-out-check.mjs +28 -24
- package/plugins/sp/scripts/persist-out-check.ts +42 -33
- package/plugins/sp/scripts/residual-scan.mjs +5 -50
- package/plugins/sp/scripts/residual-scan.ts +4 -6
- package/plugins/sp/skills/code-verification/SKILL.md +1 -2
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +16 -35
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +4 -2
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +3 -1
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -2
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +7 -11
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +7 -6
- package/spur.js +422 -431
- package/web/_astro/{BoardApp.hF0RAIrg.js → BoardApp.C5jnkkKP.js} +124 -124
- package/web/_astro/{BoardApp.Niubpe6z.js → BoardApp.CaNvQBD_.js} +1 -1
- package/web/_astro/{TaskDetail.DJpapt38.js → TaskDetail.DGKndhI8.js} +1 -1
- package/web/_astro/{arc.BgKcK0i8.js → arc.DHrwdg3G.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.ChP-Ig0V.js → architectureDiagram-3BPJPVTR.pvKpK1s5.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.BVaCBoeN.js → blockDiagram-GPEHLZMM.BQmRcZsS.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.CHSDpEO5.js → c4Diagram-AAUBKEIU.CCLjHeH1.js} +1 -1
- package/web/_astro/channel.C7wG9RxA.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.MYRcYVRa.js → chunk-2J33WTMH.DL6Yuoma.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.S5j22Zqa.js → chunk-4BX2VUAB.CzlyEI7l.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.gciRPvmZ.js → chunk-55IACEB6.B60EWxp1.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CdqrSQkX.js → chunk-727SXJPM.c7v_fRLo.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.CyokdHT3.js → chunk-AQP2D5EJ.8BxLWdDc.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Bg9KJhjW.js → chunk-FMBD7UC4.DmKuxGFT.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.1gxxI-vD.js → chunk-ND2GUHAM.1Zwd4mqV.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.DDEYtCKU.js → chunk-QZHKN3VN.BvYLu-A1.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Ch3zi37T.js → classDiagram-4FO5ZUOK.CEqRt0WU.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Ch3zi37T.js → classDiagram-v2-Q7XG4LA2.CEqRt0WU.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.D2q5AINI.js → cose-bilkent-S5V4N54A.C7JiUdj2.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BA9kC2YT.js → cynefin-OW5HDTMX.B6mZ-1be.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.CHO08nRp.js → dagre-BM42HDAG.BuryfDy6.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BfFQt0LW.js → diagram-2AECGRRQ.dao39YQM.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CHMwMTe4.js → diagram-5GNKFQAL.lnGZe4At.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BJ-7gK-w.js → diagram-KO2AKTUF.uWfhzEfT.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.C5xqcc0t.js → diagram-LMA3HP47.ppq8-198.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.C2pQjfrC.js → diagram-OG6HWLK6.CKSCqDt5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35._FoNyxgC.js → erDiagram-TEJ5UH35.sFSf2yBN.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.B_ENkT2I.js → flowDiagram-I6XJVG4X.BJQjKLbz.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.B3q8h77C.js → ganttDiagram-6RSMTGT7.DAHKNj2N.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.QWZFDboZ.js → gitGraphDiagram-PVQCEYII.BlmzcW_O.js} +1 -1
- package/web/_astro/index.BCY1JJHd.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Cs6eEc92.js → infoDiagram-5YYISTIA.DGhjaqsi.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BU9DJMHA.js → ishikawaDiagram-YF4QCWOH.gKIpgt6F.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.D35k6Jpe.js → journeyDiagram-JHISSGLW.DG3tmP0R.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DBqcAT4m.js → kanban-definition-UN3LZRKU.CgHDSxkR.js} +1 -1
- package/web/_astro/{linear.D-LSNK8W.js → linear.BuGEpcuM.js} +1 -1
- package/web/_astro/{mermaid.core.CSruWVOa.js → mermaid.core.DBVqkWBp.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BiFaUsfj.js → mindmap-definition-RKZ34NQL.D-kOcji9.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.D1KTKqb9.js → pieDiagram-4H26LBE5.BvGPwtZt.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DBHflsMG.js → quadrantDiagram-W4KKPZXB.BKb_9FV6.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.hwX9CNdk.js → requirementDiagram-4Y6WPE33.Cx0fvD8v.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DkI8mJiE.js → sankeyDiagram-5OEKKPKP.fatULi0W.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DMz7jBDx.js → sequenceDiagram-3UESZ5HK.ChHyKqco.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.B6Ef2xJ1.js → stateDiagram-AJRCARHV.BdUpKWyj.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CgoaufB9.js → stateDiagram-v2-BHNVJYJU.fmJxqIm5.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.B3c8h1t-.js → timeline-definition-PNZ67QCA.BayDn5-V.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BdHPAxAb.js → vennDiagram-CIIHVFJN.Bm2Aggwa.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BrJXPPAj.js → wardleyDiagram-YWT4CUSO.DI75O-uG.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.CMH5yFsV.js → xychartDiagram-2RQKCTM6.DL90E7Q7.js} +1 -1
- package/web/index.html +2 -2
- package/config/rules/docs/verdict-confidence.yaml +0 -68
- package/web/_astro/channel.DkpvN2tD.js +0 -1
- package/web/_astro/index.BFyW13JJ.css +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1, 1148 R1).
|
|
4
|
-
*
|
|
5
4
|
* Before `git worktree remove`, verify the worktree's OWNED durable evidence would not be abandoned:
|
|
6
5
|
* every `.spur/run/` and `.spur/memory/evidence/` file starting with a forwarded `<wbs>-`/`<runId>-`
|
|
7
6
|
* prefix must exist in the invoking tree with identical bytes. Unowned evidence is counted
|
|
@@ -17,6 +16,9 @@ const EVIDENCE_DIR = join('.spur', 'memory', 'evidence');
|
|
|
17
16
|
const RUN_DIR = join('.spur', 'run');
|
|
18
17
|
const NAMED_CAP = 32;
|
|
19
18
|
const PREFIX_CAP = 64;
|
|
19
|
+
/** The durable-verdict family persist-out itself transfers (1026/1043) — the one artifact class whose
|
|
20
|
+
* silent abandonment WT-4 exists to prevent, even when the driver forwarded no matching prefix. */
|
|
21
|
+
const DURABLE_FAMILY_RE = /^\d{4}-(?:verdict|feature-verification)\.json$/;
|
|
20
22
|
|
|
21
23
|
/** Task files own their `<wbs>-*` evidence: leading digits before `_` in the basename. */
|
|
22
24
|
export function wbsOfTaskFile(path: string): string | null {
|
|
@@ -27,6 +29,8 @@ export function wbsOfTaskFile(path: string): string | null {
|
|
|
27
29
|
export interface ListObligationsResult {
|
|
28
30
|
files: string[];
|
|
29
31
|
unowned: number;
|
|
32
|
+
/** Every file seen in both planes (recursive) — lets callers audit outside the forwarded prefixes. */
|
|
33
|
+
planeFiles: string[];
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/** Recursively list files under `dir`, relative to `root`. */
|
|
@@ -58,8 +62,9 @@ function collectOwned(rels: string[], prefixes: string[], counts: Map<string, nu
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
|
-
* Evidence the worktree owns: `.spur/memory/evidence/` (recursive) and `.spur/run/` (
|
|
62
|
-
*
|
|
65
|
+
* Evidence the worktree owns: `.spur/memory/evidence/` (recursive) and `.spur/run/` (recursive — the
|
|
66
|
+
* driver keeps run scratch under `.spur/run/<run-id>/`, which a flat scan silently missed) files whose
|
|
67
|
+
* basename starts with a forwarded `<wbs>-`/`<runId>-` prefix. Evidence matching no prefix is
|
|
63
68
|
* counted as `unowned` (1148 R1). Null when no prefix is forwarded or a prefix cap is exceeded.
|
|
64
69
|
*/
|
|
65
70
|
export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]): ListObligationsResult | null {
|
|
@@ -69,19 +74,14 @@ export function listObligations(wtRoot: string, wbsList: string[], runIds: strin
|
|
|
69
74
|
const evDir = join(wtRoot, EVIDENCE_DIR);
|
|
70
75
|
const runDir = join(wtRoot, RUN_DIR);
|
|
71
76
|
const evFiles = walkFiles(evDir, wtRoot);
|
|
72
|
-
const runFiles =
|
|
73
|
-
? readdirSync(runDir, { withFileTypes: true })
|
|
74
|
-
.filter((e) => e.isFile())
|
|
75
|
-
.map((e) => join(RUN_DIR, e.name))
|
|
76
|
-
.sort()
|
|
77
|
-
: [];
|
|
77
|
+
const runFiles = walkFiles(runDir, wtRoot);
|
|
78
78
|
|
|
79
79
|
const files: string[] = [];
|
|
80
80
|
const counts = new Map<string, number>();
|
|
81
81
|
if (!collectOwned(evFiles, prefixes, counts, files)) return null;
|
|
82
82
|
if (!collectOwned(runFiles, prefixes, counts, files)) return null;
|
|
83
83
|
const unowned = evFiles.length - files.filter((f) => f.startsWith(EVIDENCE_DIR)).length;
|
|
84
|
-
return { files: [...new Set(files)].sort(), unowned };
|
|
84
|
+
return { files: [...new Set(files)].sort(), unowned, planeFiles: [...evFiles, ...runFiles] };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/** Classify obligations against the invoking tree: missing, divergent (byte-differs), or ok. */
|
|
@@ -164,14 +164,12 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
|
|
|
164
164
|
}
|
|
165
165
|
if (wbsList.length === 0 && runIds.length === 0) {
|
|
166
166
|
process.stderr.write(
|
|
167
|
-
`${persistOutCheckUsage()}\npersist-out-check: at least one --task-file or --run-id
|
|
167
|
+
`${persistOutCheckUsage()}\npersist-out-check: at least one --task-file or --run-id required\n`,
|
|
168
168
|
);
|
|
169
169
|
return 2;
|
|
170
170
|
}
|
|
171
171
|
if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
|
|
172
|
-
process.stderr.write(
|
|
173
|
-
`${persistOutCheckUsage()}\npersist-out-check: --from must be an existing worktree directory\n`,
|
|
174
|
-
);
|
|
172
|
+
process.stderr.write(`${persistOutCheckUsage()}\npersist-out-check: --from must be an existing directory\n`);
|
|
175
173
|
return 2;
|
|
176
174
|
}
|
|
177
175
|
const foreignDivergent = new Set<string>();
|
|
@@ -182,27 +180,26 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
|
|
|
182
180
|
join(wtRoot, '.spur', 'run', 'persist-out.json'),
|
|
183
181
|
].filter(Boolean);
|
|
184
182
|
for (const cand of candidates) {
|
|
185
|
-
if (existsSync(cand))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
183
|
+
if (!existsSync(cand)) continue;
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(readFileSync(cand, 'utf8'));
|
|
186
|
+
if (Array.isArray(parsed.evidenceSkipped)) {
|
|
187
|
+
for (const item of parsed.evidenceSkipped) {
|
|
188
|
+
if (item.reason === 'foreign-divergent' && typeof item.name === 'string') {
|
|
189
|
+
foreignDivergent.add(item.name);
|
|
190
|
+
foreignDivergent.add(join(EVIDENCE_DIR, item.name));
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(parsed.skipped)) {
|
|
195
|
+
for (const item of parsed.skipped) {
|
|
196
|
+
if (typeof item.id === 'string' && typeof item.reason === 'string') {
|
|
197
|
+
skips.push({ id: item.id, reason: item.reason });
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
} catch {}
|
|
206
203
|
}
|
|
207
204
|
const obligations = listObligations(wtRoot, wbsList, runIds);
|
|
208
205
|
if (obligations === null) {
|
|
@@ -211,11 +208,21 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
|
|
|
211
208
|
);
|
|
212
209
|
return 1;
|
|
213
210
|
}
|
|
214
|
-
const { files, unowned } = obligations;
|
|
211
|
+
const { files, unowned, planeFiles } = obligations;
|
|
212
|
+
// Session-incident class (runall-1208-1209-bdcd): the driver forwarded only the batch run id, no
|
|
213
|
+
// prefix matched, and the check passed at 0 obligations while the worktree held its verdicts. Family
|
|
214
|
+
// files outside the prefixes (not foreign-divergent) block teardown; non-family scratch never does.
|
|
215
|
+
const unforwarded = planeFiles
|
|
216
|
+
.filter((rel) => DURABLE_FAMILY_RE.test(basename(rel)))
|
|
217
|
+
.filter((rel) => !files.includes(rel) && !foreignDivergent.has(rel) && !foreignDivergent.has(basename(rel)));
|
|
218
|
+
const unforwardedFindings = unforwarded.map(
|
|
219
|
+
(f) => `UNFORWARDED ${f} — durable verdict/receipt outside the forwarded prefixes`,
|
|
220
|
+
);
|
|
215
221
|
const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files, foreignDivergent);
|
|
216
222
|
const findings = [
|
|
217
223
|
...missing.map((f) => `MISSING ${f}`),
|
|
218
224
|
...divergent.map((f) => `DIVERGENT ${f} — reconcile by hand (persist-out never overwrites)`),
|
|
225
|
+
...unforwardedFindings,
|
|
219
226
|
];
|
|
220
227
|
for (const line of findings.slice(0, NAMED_CAP)) process.stdout.write(`${line}\n`);
|
|
221
228
|
if (findings.length > NAMED_CAP) process.stdout.write(`… +${findings.length - NAMED_CAP} more\n`);
|
|
@@ -230,7 +237,9 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
|
|
|
230
237
|
return 0;
|
|
231
238
|
}
|
|
232
239
|
process.stderr.write(
|
|
233
|
-
`persist-out-check: BLOCKED ${missing.length} missing, ${divergent.length} divergent
|
|
240
|
+
`persist-out-check: BLOCKED ${missing.length} missing, ${divergent.length} divergent` +
|
|
241
|
+
`${unforwarded.length > 0 ? `, ${unforwarded.length} unforwarded durable verdict/receipt` : ''}` +
|
|
242
|
+
' — run inline-run-setup --persist-out --from <worktree> (--task-file <wbs>_<slug>.md) before worktree removal\n',
|
|
234
243
|
);
|
|
235
244
|
return 1;
|
|
236
245
|
}
|
|
@@ -33,14 +33,6 @@ function hasClaimMarker(evidence) {
|
|
|
33
33
|
function hasCitationForm(evidence) {
|
|
34
34
|
return CITATION_FORMS.some((re) => re.test(evidence));
|
|
35
35
|
}
|
|
36
|
-
var CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
|
|
37
|
-
function extractAnswerConfidence(text) {
|
|
38
|
-
const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
|
|
39
|
-
if (matches.length !== 1)
|
|
40
|
-
return;
|
|
41
|
-
const value = (matches[0]?.[1] ?? "").toUpperCase();
|
|
42
|
-
return CONFIDENCE_LEVELS.includes(value) ? value : undefined;
|
|
43
|
-
}
|
|
44
36
|
var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
|
|
45
37
|
var PRIORITY_PATTERN = /^P[1-4]/;
|
|
46
38
|
var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
|
|
@@ -212,13 +204,6 @@ function blockingReviewFindings(taskContent, deferrals) {
|
|
|
212
204
|
}
|
|
213
205
|
function reviewContractViolations(taskContent) {
|
|
214
206
|
const violations = [];
|
|
215
|
-
const body = reviewBody(taskContent);
|
|
216
|
-
if (body !== null && extractAnswerConfidence(body.replace(/\*\*/g, "")) === undefined) {
|
|
217
|
-
violations.push({
|
|
218
|
-
location: "task-file:Review",
|
|
219
|
-
text: "Review report carries no single valid `**Confidence:** HIGH | MEDIUM | LOW` line — " + "state the level the report can defend (HIGH: every requirement row MET, every finding " + "dispositioned, evidence cited; MEDIUM/LOW: name the gap)."
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
207
|
for (const row of parseReviewFindings(taskContent)) {
|
|
223
208
|
if (hasClaimMarker(row.text) && !hasCitationForm(row.text)) {
|
|
224
209
|
violations.push({
|
|
@@ -229,32 +214,8 @@ function reviewContractViolations(taskContent) {
|
|
|
229
214
|
}
|
|
230
215
|
return violations;
|
|
231
216
|
}
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
return null;
|
|
235
|
-
const level = extractAnswerConfidence(reviewAnswer.replace(/\*\*/g, ""));
|
|
236
|
-
if (level === "HIGH" || level === "MEDIUM")
|
|
237
|
-
return null;
|
|
238
|
-
return {
|
|
239
|
-
location: "review-answer:Confidence",
|
|
240
|
-
text: level === undefined ? "The review answer carries no valid `**Confidence:** HIGH | MEDIUM | LOW` line — a " + "PASS must be graded, or the operator must set ackLowConfidence" : `The review answer graded its PASS ${level} — HIGH/MEDIUM cross unadorned, ` + "LOW/absent needs the operator’s ackLowConfidence"
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
function reviewGradingInput(root, reviewAnswer, ackLowConfidence, fs) {
|
|
244
|
-
if (reviewAnswer === undefined)
|
|
245
|
-
return;
|
|
246
|
-
const path = join(root, reviewAnswer);
|
|
247
|
-
let isFile = false;
|
|
248
|
-
try {
|
|
249
|
-
isFile = fs.statSync(path).isFile();
|
|
250
|
-
} catch {
|
|
251
|
-
isFile = false;
|
|
252
|
-
}
|
|
253
|
-
return { answer: isFile ? fs.readFileSync(path, "utf8") : "", ackLowConfidence };
|
|
254
|
-
}
|
|
255
|
-
function buildReviewGateArtifact(wbs, taskContent, deferrals, review) {
|
|
256
|
-
const confidence = review === undefined ? null : reviewConfidenceViolation(review.answer, review.ackLowConfidence);
|
|
257
|
-
const contractViolations = [...reviewContractViolations(taskContent), ...confidence === null ? [] : [confidence]];
|
|
217
|
+
function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
218
|
+
const contractViolations = reviewContractViolations(taskContent);
|
|
258
219
|
const items = [
|
|
259
220
|
...blockingReviewFindings(taskContent, deferrals),
|
|
260
221
|
...contractViolations.map((v) => ({
|
|
@@ -390,7 +351,6 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
|
390
351
|
let spurBin;
|
|
391
352
|
let root = cwd;
|
|
392
353
|
let tmpDir = defaultTmpDir;
|
|
393
|
-
let reviewAnswer;
|
|
394
354
|
for (let i = 0;i < argv.length; i++) {
|
|
395
355
|
const a = argv[i];
|
|
396
356
|
if (a === undefined)
|
|
@@ -401,8 +361,6 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
|
401
361
|
root = argv[++i] ?? root;
|
|
402
362
|
else if (a === "--tmp-dir")
|
|
403
363
|
tmpDir = argv[++i] ?? tmpDir;
|
|
404
|
-
else if (a === "--review-answer")
|
|
405
|
-
reviewAnswer = argv[++i];
|
|
406
364
|
else if (a === "--help" || a === "-h")
|
|
407
365
|
return null;
|
|
408
366
|
else if (mode === "")
|
|
@@ -410,7 +368,7 @@ function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
|
410
368
|
else if (wbs === "")
|
|
411
369
|
wbs = a;
|
|
412
370
|
}
|
|
413
|
-
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir
|
|
371
|
+
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
414
372
|
}
|
|
415
373
|
|
|
416
374
|
// plugins/sp/lib/spur-bin.ts
|
|
@@ -420,7 +378,7 @@ function spurCommand(spurBin) {
|
|
|
420
378
|
}
|
|
421
379
|
|
|
422
380
|
// plugins/sp/scripts/residual-scan.ts
|
|
423
|
-
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]
|
|
381
|
+
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
424
382
|
function run(cmd, args, cwd) {
|
|
425
383
|
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
426
384
|
if (result.error !== undefined)
|
|
@@ -515,8 +473,7 @@ function reviewGateMode(opts, env, io) {
|
|
|
515
473
|
const runDir = join2(opts.root, ".spur", "run");
|
|
516
474
|
fs.mkdirSync(runDir, { recursive: true });
|
|
517
475
|
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
518
|
-
const
|
|
519
|
-
const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
|
|
476
|
+
const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
520
477
|
fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
|
|
521
478
|
`);
|
|
522
479
|
io.out(gate.note);
|
|
@@ -671,9 +628,7 @@ export {
|
|
|
671
628
|
parseScanArgs,
|
|
672
629
|
recordedVerdictPath,
|
|
673
630
|
renderReport,
|
|
674
|
-
reviewConfidenceViolation,
|
|
675
631
|
reviewContractViolations,
|
|
676
|
-
reviewGradingInput,
|
|
677
632
|
scanResiduals2 as scanResiduals,
|
|
678
633
|
verdictDisagreementNote
|
|
679
634
|
};
|
|
@@ -10,7 +10,7 @@ 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|review-gate> <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 };
|
|
@@ -106,16 +106,14 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
106
106
|
/**
|
|
107
107
|
* Task 1122 R2: the review-time gate — the record sweep's review-finding slice
|
|
108
108
|
* (core.buildReviewGateArtifact) at the review PASS edges; writes the same `<wbs>-residuals.json`
|
|
109
|
-
* the test-fix hop reads. Exit 1 on an open P1-P3 finding
|
|
110
|
-
*
|
|
111
|
-
* cannot sit inline in the edge because ADR-115 caps a guard at 5 logical commands).
|
|
109
|
+
* the test-fix hop reads. Exit 1 on an open P1-P3 finding. The answer-grade rule (1204) was
|
|
110
|
+
* retired with the asserted confidence machinery (F511); findings stay the gate.
|
|
112
111
|
*/
|
|
113
112
|
function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
114
113
|
const runDir = join(opts.root, '.spur', 'run');
|
|
115
114
|
fs.mkdirSync(runDir, { recursive: true });
|
|
116
115
|
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
117
|
-
const
|
|
118
|
-
const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs), grading);
|
|
116
|
+
const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
119
117
|
fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
|
|
120
118
|
io.out(gate.note);
|
|
121
119
|
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
@@ -341,7 +341,6 @@ Verdict: PASS (or PARTIAL / FAIL)
|
|
|
341
341
|
|
|
342
342
|
```markdown
|
|
343
343
|
Verdict: PASS
|
|
344
|
-
Confidence: HIGH
|
|
345
344
|
|
|
346
345
|
### Per-Requirement Traceability
|
|
347
346
|
| Req | Status | Evidence |
|
|
@@ -360,7 +359,7 @@ Confidence: HIGH
|
|
|
360
359
|
```
|
|
361
360
|
|
|
362
361
|
The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant — `Status` in column 2 — and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
|
|
363
|
-
The answer
|
|
362
|
+
The answer carries no `Confidence:` line (F511, task 1207): model-asserted confidence is not verification — a verifier that can write a PASS can write `HIGH`. What the old level compressed now lives in the evidence cells themselves: cite fresh executable evidence and name residual gaps explicitly. Legacy answers that still carry a confidence line parse (the value is ignored); no lint code reads it and the artifact and recorded `## Testing` section carry no confidence value.
|
|
364
363
|
The answer MUST cite a source for external API/library claims in Requirement/AC evidence cells: a cell asserting a scoped-package (`@scope/name`), `node_modules/`, semver-version (`x.y.z`), or backticked function-call (`` `name(args)` ``) claim is rejected with `evidence-citation` unless the SAME cell carries a citation — a repo `path:line(-end)` (backticked or bare), the external named-origin form (@origin `path` line N), or a URL. Cross-cell references ("receipt above") do not count; marker-free receipts ("bun test 398 pass / 0 fail") need no citation.
|
|
365
364
|
**MUST NOT:** place a `Severity` column between `Req` and `Status`. The parser tolerates variants
|
|
366
365
|
as defense-in-depth; this contract is canonical.
|
|
@@ -35,42 +35,21 @@ interface VerifyVerdict {
|
|
|
35
35
|
status: 'pass' | 'fail' | 'warn';
|
|
36
36
|
evidence: string;
|
|
37
37
|
}>;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
confidence?: 'HIGH' | 'MEDIUM' | 'LOW';
|
|
38
|
+
// F511 (task 1207): no asserted `confidence` field. Legacy artifacts that still carry one
|
|
39
|
+
// parse with the value ignored — unknown legacy keys are tolerated on read.
|
|
41
40
|
}
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
##
|
|
43
|
+
## No asserted confidence (F511, task 1207)
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Presence and vocabulary are not enough — the level has to match the rows in the same answer, so the
|
|
56
|
-
lint rejects a level that contradicts its own evidence. Two rejection classes, both addressed to the
|
|
57
|
-
`Confidence:` line and both capped by the shared `ANSWER_LINT_MAX_FINDINGS` budget:
|
|
58
|
-
|
|
59
|
-
- `confidence-unwarranted` — `HIGH` while any requirement row normalizes to `PARTIAL`/`UNMET`, any
|
|
60
|
-
acceptance-criteria row does not normalize to `MET`, or any `MET` row's evidence is hedged.
|
|
61
|
-
A `HIGH` claim says the evidence needs no re-check; a non-MET or hedged row says the opposite.
|
|
62
|
-
- `confidence-understated` — `LOW` while at least one requirement row exists and every requirement
|
|
63
|
-
and acceptance-criteria row normalizes to `MET`. The evidence already did the work, so `LOW`
|
|
64
|
-
misreports the state as unproven.
|
|
65
|
-
|
|
66
|
-
`MEDIUM` carries no coherence rule in either direction: it is the level a verifier uses for a caveat
|
|
67
|
-
with no row to point at, and closing it would force either an overclaimed `HIGH` or an unusable
|
|
68
|
-
`LOW`. The level is normalized to uppercase before the decision, so `high` is judged as `HIGH`.
|
|
69
|
-
|
|
70
|
-
Set the level from the rows, not the mood: all rows `MET` with fresh evidence is `HIGH`; a
|
|
71
|
-
`PARTIAL`/`UNMET` row or a hedge means `MEDIUM`, not `HIGH`. An unwarranted level does not merely
|
|
72
|
-
warn — the lint rejects the answer and no verdict artifact is written, so the level is worth
|
|
73
|
-
choosing deliberately before the verdict line is finalized.
|
|
45
|
+
Verdict artifacts carry no `confidence` field and verify answers no `Confidence:` line. The
|
|
46
|
+
1068/1133 machinery that required, linted and coherence-checked a model-asserted
|
|
47
|
+
`HIGH | MEDIUM | LOW` was retired: a verifier that can write a PASS can write `HIGH`, so the grade
|
|
48
|
+
verifies nothing. What the level compressed belongs in the evidence cells — cite fresh executable
|
|
49
|
+
evidence and name residual gaps explicitly. The done gate no longer denies a confidence-less PASS:
|
|
50
|
+
it checks the proof digests, which are the part a hand-edited artifact cannot fake. The DERIVED
|
|
51
|
+
confidence family is a different thing and stays: feature-verification receipts (ADR-131, 1196),
|
|
52
|
+
the decision subsystem threshold, history-anatomy reading.
|
|
74
53
|
|
|
75
54
|
## Compatibility alias: `scenario` row key
|
|
76
55
|
|
|
@@ -124,9 +103,11 @@ is part of the contract so weak proof is visible to the pipeline and to reviewer
|
|
|
124
103
|
- `n/a`: explicitly justified non-applicability.
|
|
125
104
|
|
|
126
105
|
Every CORE behavior-bearing AC requires executable evidence: at least one `test` or `command` row.
|
|
127
|
-
`spur task verdict` treats AC rows as core and behavior-bearing by default
|
|
128
|
-
`[non-core]`, `[non-behavior]
|
|
129
|
-
|
|
106
|
+
`spur task verdict` treats AC rows as core and behavior-bearing by default. Only the task author
|
|
107
|
+
can exempt an AC: the `[advisory]`, `[non-core]`, `[non-behavior]` or `[docs-only]` tag must be on
|
|
108
|
+
the AC item in the task file's `## Acceptance Criteria`. The verifier must not add these tags to
|
|
109
|
+
answer rows; a tag that appears only in the answer is ignored and flagged by an advisory
|
|
110
|
+
`ac-exemption-not-in-task` check. A MET behavior AC with only `static-ref`, `manual-review`, or `llm-judge` evidence is
|
|
130
111
|
downgraded to `PARTIAL` and emits an `evidence-rule-failed` check. CLI-surface changes should also
|
|
131
112
|
emit a `cli-golden-path-present` check backed by a golden-path `--json` command invocation.
|
|
132
113
|
|
|
@@ -377,8 +377,10 @@ rows), there is no checklist label: cite each row by its exact scenario title
|
|
|
377
377
|
matches nothing and the verdict is rejected.
|
|
378
378
|
|
|
379
379
|
**Evidence rule (behavior-bearing AC):** an AC row with `status: MET` on a behavior-bearing id
|
|
380
|
-
(no `[advisory]`/`[non-core]`/`[non-behavior]`/`[docs-only]` marker
|
|
381
|
-
`command` evidence; any other evidence type downgrades the row to
|
|
380
|
+
(no `[advisory]`/`[non-core]`/`[non-behavior]`/`[docs-only]` marker **on the AC item in the task
|
|
381
|
+
file**) MUST carry `test` or `command` evidence; any other evidence type downgrades the row to
|
|
382
|
+
`PARTIAL` and caps the verdict. Tags written only in the answer are ignored and reported by the
|
|
383
|
+
advisory `ac-exemption-not-in-task` check.
|
|
382
384
|
|
|
383
385
|
**Worked example** — a minimal PASS-producing answer file:
|
|
384
386
|
|
|
@@ -219,7 +219,9 @@ clean at authoring time.
|
|
|
219
219
|
### Which tags exempt a row from executable evidence
|
|
220
220
|
|
|
221
221
|
A `MET` row is silently demoted to `PARTIAL` unless it carries `test` or `command` evidence — the
|
|
222
|
-
executable-evidence rule. Five tags opt a row out, because not every scenario is behavioral
|
|
222
|
+
executable-evidence rule. Five tags opt a row out, because not every scenario is behavioral. The
|
|
223
|
+
tag counts only when the task author writes it on the AC item in the task file; `spur task verdict`
|
|
224
|
+
ignores (and flags) a tag that appears only in the verify answer:
|
|
223
225
|
|
|
224
226
|
`[doc-only]` · `[docs-only]` · `[non-behavior]` · `[advisory]` · `[non-core]`
|
|
225
227
|
|
|
@@ -139,8 +139,9 @@ silently incomplete (H6 shipped at 23/48 that way, with one verdict carrying an
|
|
|
139
139
|
parses zero AC rows while lint still reports PASS.
|
|
140
140
|
3. A behavioral AC marked `MET` with a non-executable evidence type (`static-ref`,
|
|
141
141
|
`manual-review`, `llm-judge`) is **downgraded to PARTIAL** by `task verdict`, making the whole
|
|
142
|
-
verdict PARTIAL. Use `test`/`command` (grep-based verification counts as `command`)
|
|
143
|
-
AC
|
|
142
|
+
verdict PARTIAL. Use `test`/`command` (grep-based verification counts as `command`). Only the
|
|
143
|
+
task author can exempt an AC, by tagging the AC item in the task file `[non-behavior]`/`[advisory]`;
|
|
144
|
+
the same tag added in the verify answer alone is ignored.
|
|
144
145
|
|
|
145
146
|
**Invariant:** a force-done task has a non-empty `done_reason` naming the timeout, a verdict
|
|
146
147
|
artifact whose AC rows cover every declared scenario, and a green lint/test run recorded in
|
|
@@ -171,17 +171,13 @@ Two branches reach the bounded repair hop, not one (session finding after 1088):
|
|
|
171
171
|
an on-graph, bounded path to remediation instead of dead-ending the run, and the fix re-enters
|
|
172
172
|
quality → review → verify on a freshly captured proof digest.
|
|
173
173
|
|
|
174
|
-
The completion gate
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
out-of-vocabulary value fails the read closed (`invalid confidence field`), so a hand-driven
|
|
182
|
-
`task update <wbs> done` — which is how this pipeline itself reaches `done` (`--no-lifecycle`) —
|
|
183
|
-
cannot certify a verdict that carries no confidence. `LOW` is accepted at that gate; the
|
|
184
|
-
acknowledgement rule above stays the pipeline's.
|
|
174
|
+
The completion gate reads deterministic evidence only (F511, task 1207): both `verify → record` and
|
|
175
|
+
`record → done` check the proof digests in `.spur/run/<wbs>-verdict.json` — verdict PASS, matching
|
|
176
|
+
run/definition digests, review stage completed. No confidence is read, stated or acknowledged; the
|
|
177
|
+
asserted `Confidence:` line and the `ackLowConfidence` operator var were retired because a model
|
|
178
|
+
that can write a PASS can write `HIGH` — the grade verified nothing. What protects a hand-driven
|
|
179
|
+
`task update <wbs> done` is unchanged: the digests must match the run's captured proof, and the
|
|
180
|
+
review stage must actually be completed in the artifact.
|
|
185
181
|
|
|
186
182
|
Agentic steps use the pure slash inputs declared by each YAML `agent.run` action (ADR-043). The
|
|
187
183
|
workflow engine dispatches them; the interactive driver invokes their backing skills in-session.
|
|
@@ -844,9 +844,10 @@ leave no `.spur/run/<wbs>-check-receipt.json`. The deciding artifacts are then
|
|
|
844
844
|
1. **Drive the pipeline** for anything that must be certified: only a digest-bound full receipt
|
|
845
845
|
satisfies a review/verify/record boundary (`sp-spur-check`), and the `test` hop owns the capture.
|
|
846
846
|
2. **When a host-driven pass is unavoidable, say so in the verdict** — name the status file and gate
|
|
847
|
-
log as the evidence and
|
|
848
|
-
checked path
|
|
849
|
-
failure mode this note exists to prevent (observed across
|
|
847
|
+
log as the evidence, and mark the reliance explicitly in the evidence cell (a host-driven gate is
|
|
848
|
+
not re-runnable proof from a checked path; the `sp-code-verification` ladder is). Claiming a
|
|
849
|
+
receipt that was never written is the failure mode this note exists to prevent (observed across
|
|
850
|
+
four A91 tasks, 2026-10-12).
|
|
850
851
|
|
|
851
852
|
**A mismatch names the drifted paths (R5).** At gate entry the pipeline snapshots the non-corpus tree
|
|
852
853
|
state (`git status --porcelain=v1 -uall` plus `git diff --name-only HEAD`, excluding
|
|
@@ -977,14 +978,14 @@ Each item below cost a driver cycle to discover. They are contracts, not tips.
|
|
|
977
978
|
`.spur/run/<wbs>-diffstat.json` and never on command output. Never the bare unguarded
|
|
978
979
|
source-repo path form — the script-contract check forbids it on a shipped surface because it
|
|
979
980
|
shadows the installed twin in a consumer.
|
|
980
|
-
- **The verify answer grammar is exact.** One `Verdict:` line,
|
|
981
|
+
- **The verify answer grammar is exact.** One `Verdict:` line, then
|
|
981
982
|
`### Per-Requirement Traceability` (`| Req | Status | Evidence |`) and
|
|
982
983
|
`### Acceptance Criteria Verification` (`| AC | Status | Evidence Type | Evidence |`). Requirement
|
|
983
984
|
statuses are `MET | PARTIAL | UNMET`; AC statuses add `N/A`; every AC row id must match a declared
|
|
984
985
|
scenario title verbatim, including its `(req: ...)` suffix. A `MET` row whose evidence hedges
|
|
985
986
|
(`likely`, `probably`, `seems`, `appears`, `possibly`, `presumably`) is rejected as
|
|
986
|
-
`evidence-hedged
|
|
987
|
-
|
|
987
|
+
`evidence-hedged`. No `Confidence:` line exists (F511, task 1207) — legacy answers that carry one
|
|
988
|
+
parse with the value ignored. `spur task verdict` lints before writing, so a rejected answer leaves no
|
|
988
989
|
artifact and exits non-zero.
|
|
989
990
|
- **Harness tooling refuses direct task-file writes.** Resolve a merge conflict in a task file by
|
|
990
991
|
choosing a git side, then applying the corpus delta through `spur task update` (for example
|