@gobing-ai/spur 0.3.91 → 0.3.93

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.
Files changed (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -9
  3. package/config/plugin-scripts.json +27 -1
  4. package/config/templates/AGENTS.md +4 -0
  5. package/config/templates/docs/02_ROADMAP.md +2 -0
  6. package/config/templates/docs/03_ARCHITECTURE.md +3 -1
  7. package/config/templates/docs/04_DESIGN.md +2 -0
  8. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
  9. package/config/workflow-candidates.json +72 -1
  10. package/config/workflows/feature-lifecycle.yaml +14 -5
  11. package/config/workflows/feature-verification.yaml +36 -27
  12. package/config/workflows/history-anatomy.yaml +16 -25
  13. package/config/workflows/idea-pipeline.yaml +76 -23
  14. package/config/workflows/pr-review.yaml +8 -0
  15. package/config/workflows/task-pipeline.yaml +362 -40
  16. package/config/workflows/wayfinder-resolution.yaml +5 -0
  17. package/config/workflows/wrapup-pipeline.yaml +102 -19
  18. package/package.json +9 -9
  19. package/plugins/sp/README.md +7 -2
  20. package/plugins/sp/agents/super-planner.md +14 -5
  21. package/plugins/sp/commands/dev-dogfood.md +4 -4
  22. package/plugins/sp/commands/dev-fixall.md +8 -5
  23. package/plugins/sp/commands/dev-run.md +8 -2
  24. package/plugins/sp/commands/dev-runall.md +14 -8
  25. package/plugins/sp/commands/dev-verify.md +9 -0
  26. package/plugins/sp/commands/dev-verifyall.md +5 -0
  27. package/plugins/sp/lib/idea-handoff.generated.mjs +306 -301
  28. package/plugins/sp/lib/inline-run.generated.d.mts +18 -0
  29. package/plugins/sp/lib/inline-run.generated.mjs +1468 -0
  30. package/plugins/sp/plugin.json +1 -1
  31. package/plugins/sp/references/environment-lens.md +1 -1
  32. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
  33. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
  34. package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
  35. package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
  36. package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
  37. package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
  38. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -1
  39. package/plugins/sp/scripts/inline-run-setup.mjs +421 -0
  40. package/plugins/sp/scripts/inline-run-setup.ts +325 -78
  41. package/plugins/sp/scripts/quality-gate.mjs +248 -6
  42. package/plugins/sp/scripts/quality-gate.ts +410 -9
  43. package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
  44. package/plugins/sp/scripts/record-feature-sync.ts +84 -0
  45. package/plugins/sp/scripts/residual-scan.mjs +484 -0
  46. package/plugins/sp/scripts/residual-scan.ts +640 -0
  47. package/plugins/sp/scripts/task-diffstat.mjs +156 -0
  48. package/plugins/sp/scripts/task-diffstat.ts +229 -0
  49. package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
  50. package/plugins/sp/scripts/task-size-precheck.ts +8 -3
  51. package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
  52. package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
  53. package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
  54. package/plugins/sp/scripts/wrapup-steps.ts +89 -4
  55. package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
  56. package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
  57. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  58. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
  59. package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
  60. package/plugins/sp/skills/code-verification/SKILL.md +21 -0
  61. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
  62. package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
  63. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
  64. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
  65. package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
  66. package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
  67. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
  68. package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
  69. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  70. package/plugins/sp/skills/session-review/SKILL.md +12 -2
  71. package/plugins/sp/skills/spur-check/SKILL.md +112 -0
  72. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  73. package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
  74. package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
  75. package/plugins/sp/skills/spur-dev/SKILL.md +13 -5
  76. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +18 -3
  77. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  78. package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
  79. package/plugins/sp/skills/spur-dev/references/execution-batch.md +286 -58
  80. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
  81. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +27 -4
  82. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
  83. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +61 -16
  84. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +10 -5
  85. package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
  86. package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
  87. package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
  88. package/schemas/state-machine-workflow.schema.json +4 -0
  89. package/spur.js +22501 -19644
  90. package/web/_astro/BoardApp.CerSBgis.js +192 -0
  91. package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
  92. package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
  93. package/web/_astro/arc.CPwg6Rw0.js +1 -0
  94. package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
  95. package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
  96. package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
  97. package/web/_astro/channel.MYZLKNwy.js +1 -0
  98. package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
  99. package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
  100. package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
  101. package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
  102. package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
  103. package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
  104. package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
  105. package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
  106. package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
  107. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
  108. package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
  109. package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
  110. package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
  111. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
  112. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
  113. package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
  114. package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
  115. package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
  116. package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
  117. package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
  118. package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
  119. package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
  120. package/web/_astro/index.Hjbr15fG.css +1 -0
  121. package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
  122. package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
  123. package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
  124. package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
  125. package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
  126. package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
  127. package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
  128. package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
  129. package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
  130. package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
  131. package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
  132. package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
  133. package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
  134. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
  135. package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
  136. package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
  137. package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
  138. package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
  139. package/web/index.html +2 -2
  140. package/config/workflows/decision-routing-example.yaml +0 -134
  141. package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
  142. package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
  143. package/web/_astro/arc.C0rrflm_.js +0 -1
  144. package/web/_astro/channel.SRrg1P-w.js +0 -1
  145. package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
  146. package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
  147. package/web/_astro/index.Bx6GY4RH.css +0 -1
  148. package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/residual-scan.ts
5
+ import { spawnSync } from "child_process";
6
+ import { createHash } from "crypto";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
8
+ import { tmpdir } from "os";
9
+ import { join } from "path";
10
+
11
+ // plugins/sp/lib/env.ts
12
+ function getEnvVars() {
13
+ return process.env;
14
+ }
15
+
16
+ // plugins/sp/scripts/residual-scan.ts
17
+ var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
18
+ var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
19
+ var PRIORITY_PATTERN = /^P[1-4]/;
20
+ var NONE_FINDING = /^(none|\u2014)$/i;
21
+ var DISPOSITION_HEADER = /^(Disposition|Action|Status|Resolution|Fixed)$/i;
22
+ var RESOLVED_DISPOSITION = /^(FIXED|RESOLVED|DONE)\b/i;
23
+ var DEFERRED_DISPOSITION = /^DEFER(RED)?\b/i;
24
+ var ANCHOR_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
25
+ var RANGE_ANCHOR = /([A-Za-z0-9_./-]+\.[A-Za-z]+):([0-9]+)-[0-9]+/g;
26
+ var EXCLUDED_PATHS = ["docs/tasks", "docs/features/", ".spur/"];
27
+ var ALLOW_PRAGMA = "residual-scan:allow";
28
+ function spurCommand(spurBin) {
29
+ const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
30
+ return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
31
+ }
32
+ function run(cmd, args, cwd) {
33
+ const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
34
+ if (result.error !== undefined)
35
+ return { status: result.status ?? 1, stdout: "" };
36
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
37
+ }
38
+ function makeItemId(category, location, text) {
39
+ const normalized = text.trim().replace(/\s+/g, " ");
40
+ const hex = createHash("sha256").update(`${location}${normalized}`).digest("hex");
41
+ return `${category}:${hex.slice(0, 8)}`;
42
+ }
43
+ function normalizeAnchor(location) {
44
+ return location.replace(RANGE_ANCHOR, "$1:$2");
45
+ }
46
+ function locationOf(locationCell, finding) {
47
+ const cell = locationCell.trim().replace(/`/g, "");
48
+ if (cell.length > 0 && cell !== "\u2014")
49
+ return normalizeAnchor(cell);
50
+ const backtick = finding.match(/`([^`]+)`/);
51
+ return backtick === null ? "" : normalizeAnchor(backtick[1]);
52
+ }
53
+ function parseReviewFindings(taskContent) {
54
+ const section = taskContent.split(/^### Review\b/m)[1];
55
+ if (section === undefined)
56
+ return [];
57
+ const body = section.split(/^### /m)[0];
58
+ const out = [];
59
+ const lines = body.split(`
60
+ `);
61
+ for (let i = 0;i < lines.length; i++) {
62
+ const line = lines[i];
63
+ if (line === undefined || !line.trimStart().startsWith("|"))
64
+ continue;
65
+ const header = splitRow(line);
66
+ const priorityCol = header.findIndex((h) => h.trim() === "Priority");
67
+ if (priorityCol === -1) {
68
+ while (i + 1 < lines.length && lines[i + 1]?.trimStart().startsWith("|"))
69
+ i++;
70
+ continue;
71
+ }
72
+ const findingCol = header.findIndex((h) => h.trim() === "Finding");
73
+ const locationCol = header.findIndex((h) => h.trim() === "Location");
74
+ const dispositionCol = header.findIndex((h) => DISPOSITION_HEADER.test(h.trim()));
75
+ i++;
76
+ const sep = lines[i];
77
+ if (sep !== undefined && /^\s*\|[\s:|-]+\|\s*$/.test(sep))
78
+ i++;
79
+ while (i < lines.length) {
80
+ const row = lines[i];
81
+ if (row === undefined || !row.trimStart().startsWith("|"))
82
+ break;
83
+ const cells = splitRow(row);
84
+ const priority = (cells[priorityCol] ?? "").trim();
85
+ const finding = (cells[findingCol] ?? "").trim();
86
+ const disposition = dispositionCol === -1 ? "" : (cells[dispositionCol] ?? "").trim();
87
+ if (PRIORITY_PATTERN.test(priority) && !NONE_FINDING.test(finding) && finding.length > 0 && !RESOLVED_DISPOSITION.test(disposition)) {
88
+ const location = locationOf(cells[locationCol] ?? "", finding);
89
+ out.push(DEFERRED_DISPOSITION.test(disposition) ? { priority, location, text: finding, deferral: disposition } : { priority, location, text: finding });
90
+ }
91
+ i++;
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ function splitRow(line) {
97
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
98
+ }
99
+ function collectAddedLines(root, base) {
100
+ const out = [];
101
+ const diff = run("git", ["diff", "--unified=0", base], root);
102
+ let file = "";
103
+ let newLine = 0;
104
+ for (const line of diff.stdout.split(`
105
+ `)) {
106
+ if (line.startsWith("+++ b/"))
107
+ file = line.slice(6);
108
+ else if (line.startsWith("@@")) {
109
+ const m = line.match(/\+[0-9]+/);
110
+ newLine = m === null ? newLine : Number.parseInt(m[0].slice(1), 10);
111
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
112
+ out.push({ file, line: newLine, text: line.slice(1) });
113
+ newLine++;
114
+ }
115
+ }
116
+ const untracked = run("git", ["ls-files", "--others", "--exclude-standard"], root);
117
+ for (const f of untracked.stdout.split(`
118
+ `)) {
119
+ if (f.length === 0)
120
+ continue;
121
+ try {
122
+ if (!statSync(join(root, f)).isFile())
123
+ continue;
124
+ } catch {
125
+ continue;
126
+ }
127
+ const content = readFileSync(join(root, f), "utf8").split(`
128
+ `);
129
+ content.forEach((text, idx) => {
130
+ out.push({ file: f, line: idx + 1, text });
131
+ });
132
+ }
133
+ return out;
134
+ }
135
+ function parseDiffMarkers(addedLines) {
136
+ return addedLines.filter((l) => !EXCLUDED_PATHS.some((p) => l.file.startsWith(p))).filter((l) => !l.text.includes(ALLOW_PRAGMA)).filter((l) => MARKER_PATTERN.test(l.text)).map((l) => ({ location: `${l.file}:${l.line}`, text: l.text.trim() }));
137
+ }
138
+ function findUncheckedBoxes(taskContent) {
139
+ const path = "task-file";
140
+ return taskContent.split(`
141
+ `).map((text, idx) => ({ text: text.trim(), line: idx + 1 })).filter((l) => l.text.startsWith("- [ ]")).map((l) => ({ location: `${path}:${l.line}`, text: l.text }));
142
+ }
143
+ function listStagingResidue(tmpDir, wbs) {
144
+ let names;
145
+ try {
146
+ names = readdirSync(tmpDir);
147
+ } catch {
148
+ return [];
149
+ }
150
+ return names.filter((n) => n.startsWith(`${wbs}-`)).filter((n) => {
151
+ try {
152
+ return statSync(join(tmpDir, n)).isFile();
153
+ } catch {
154
+ return false;
155
+ }
156
+ }).map((n) => join(tmpDir, n));
157
+ }
158
+ function readDeferrals(runDir, wbs) {
159
+ const path = join(runDir, `${wbs}-residual-deferrals.json`);
160
+ if (!existsSync(path))
161
+ return [];
162
+ try {
163
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
164
+ if (!Array.isArray(parsed))
165
+ return [];
166
+ return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0);
167
+ } catch {
168
+ return [];
169
+ }
170
+ }
171
+ function classify(items, deferrals) {
172
+ const deferred = new Map(deferrals.map((d) => [d.id, d.reason]));
173
+ return items.map((item) => {
174
+ const id = makeItemId(item.category, item.location, item.text);
175
+ let klass;
176
+ if (item.category === "review-finding")
177
+ klass = item.priority?.startsWith("P4") ? "advisory" : "blocking";
178
+ else if (item.category === "staging-residue")
179
+ klass = "housekeeping";
180
+ else
181
+ klass = "blocking";
182
+ if (klass === "blocking" && item.category !== "unchecked-box") {
183
+ const p3Like = item.category === "diff-marker" || item.category === "review-finding" && (item.priority ?? "").startsWith("P3");
184
+ const reason = deferred.get(id);
185
+ if (p3Like && reason !== undefined && reason.trim().length > 0)
186
+ klass = "deferrable";
187
+ }
188
+ return {
189
+ id,
190
+ category: item.category,
191
+ class: klass,
192
+ priority: item.priority,
193
+ location: item.location,
194
+ text: item.text
195
+ };
196
+ });
197
+ }
198
+ function scanResiduals(root, wbs, tmpDir, taskContent, _env) {
199
+ const runDir = join(root, ".spur", "run");
200
+ const basePath = join(runDir, `${wbs}-base.sha`);
201
+ const base = existsSync(basePath) ? readFileSync(basePath, "utf8").trim() : null;
202
+ const reviewRows = parseReviewFindings(taskContent);
203
+ const tableDeferrals = reviewRows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
204
+ const review = reviewRows.map((r) => ({
205
+ category: "review-finding",
206
+ priority: r.priority,
207
+ location: r.location,
208
+ text: r.text
209
+ }));
210
+ const markers = base === null ? [] : parseDiffMarkers(collectAddedLines(root, base)).map((m) => ({
211
+ category: "diff-marker",
212
+ location: m.location,
213
+ text: m.text
214
+ }));
215
+ const boxes = findUncheckedBoxes(taskContent).map((b) => ({
216
+ category: "unchecked-box",
217
+ location: b.location,
218
+ text: b.text
219
+ }));
220
+ const residue = listStagingResidue(tmpDir, wbs).map((p) => ({
221
+ category: "staging-residue",
222
+ location: p,
223
+ text: p
224
+ }));
225
+ const items = classify([...review, ...markers, ...boxes, ...residue], [...tableDeferrals, ...readDeferrals(runDir, wbs)]);
226
+ const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
227
+ for (const item of items)
228
+ counts[item.class]++;
229
+ return {
230
+ wbs,
231
+ base,
232
+ scanned: {
233
+ "review-finding": true,
234
+ "diff-marker": base !== null,
235
+ "unchecked-box": true,
236
+ "staging-residue": true
237
+ },
238
+ items,
239
+ counts
240
+ };
241
+ }
242
+ function blockingAnchors(items) {
243
+ const anchors = new Set;
244
+ for (const item of items) {
245
+ if (item.class !== "blocking")
246
+ continue;
247
+ for (const m of normalizeAnchor(item.location).matchAll(ANCHOR_PATTERN))
248
+ anchors.add(m[0]);
249
+ }
250
+ return [...anchors];
251
+ }
252
+ function foldVerdict(verdict, scan, existingFindings, maxFindings = 20) {
253
+ const blocking = scan.items.filter((i) => i.class === "blocking");
254
+ const deferrable = scan.items.filter((i) => i.class === "deferrable");
255
+ 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(", ")}` : "");
256
+ const checks = verdict.checks.filter((c) => c.name !== "residual-sweep");
257
+ checks.push({ name: "residual-sweep", status: blocking.length > 0 ? "fail" : "pass", evidence });
258
+ const merged = new Set([
259
+ ...existingFindings.split(/\s+/).filter((a) => a.length > 0),
260
+ ...blockingAnchors(scan.items)
261
+ ]);
262
+ const findings = [...merged].sort().slice(0, maxFindings).map((a) => `${a} `).join("");
263
+ let verdictStatus = verdict.verdict === "PASS" ? "PASS" : "FAIL";
264
+ if (verdict.verdict === "PARTIAL")
265
+ verdictStatus = "PARTIAL";
266
+ else if (verdict.verdict === "FAIL")
267
+ verdictStatus = "FAIL";
268
+ else if (blocking.length > 0)
269
+ verdictStatus = "PARTIAL";
270
+ return { verdict: verdictStatus, checks, findings };
271
+ }
272
+ function renderReport(wbs, items, attemptCount) {
273
+ const lines = [
274
+ `# Residual report \u2014 ${wbs}`,
275
+ "",
276
+ `Attempt: ${attemptCount}`,
277
+ "",
278
+ "| Category | Class | Location | Text |",
279
+ "| --- | --- | --- | --- |"
280
+ ];
281
+ for (const item of items) {
282
+ lines.push(`| ${item.category} | ${item.class} | ${item.location} | ${item.text.replace(/\|/g, "\\|")} |`);
283
+ }
284
+ return `${lines.join(`
285
+ `)}
286
+ `;
287
+ }
288
+ function parseArgs(argv) {
289
+ let mode = "";
290
+ let wbs = "";
291
+ let spurBin;
292
+ let root = process.cwd();
293
+ let tmpDir = tmpdir();
294
+ for (let i = 0;i < argv.length; i++) {
295
+ const a = argv[i];
296
+ if (a === undefined)
297
+ break;
298
+ if (a === "--spur-bin")
299
+ spurBin = argv[++i];
300
+ else if (a === "--root")
301
+ root = argv[++i] ?? root;
302
+ else if (a === "--tmp-dir")
303
+ tmpDir = argv[++i] ?? tmpDir;
304
+ else if (a === "--help" || a === "-h")
305
+ return null;
306
+ else if (mode === "")
307
+ mode = a;
308
+ else if (wbs === "")
309
+ wbs = a;
310
+ }
311
+ if (mode === "" || wbs === "")
312
+ return null;
313
+ return { mode, wbs, spurBin, root, tmpDir };
314
+ }
315
+ function spur(env, spurBinFlag, args, cwd) {
316
+ return run(spurCommand(spurBinFlag ?? env.spurBin).cmd, [...spurCommand(spurBinFlag ?? env.spurBin).prefix, ...args], cwd);
317
+ }
318
+ function loadTask(env, spurBinFlag, wbs, root) {
319
+ const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
320
+ if (res.status !== 0)
321
+ throw new Error(`task show ${wbs} failed`);
322
+ const parsed = JSON.parse(res.stdout);
323
+ const content = typeof parsed.content === "string" ? parsed.content : "";
324
+ const fm = parsed.frontmatter;
325
+ const featureId = typeof parsed.feature_id === "string" ? parsed.feature_id : fm !== null && typeof fm === "object" && typeof fm.feature_id === "string" ? fm.feature_id : "";
326
+ return { content, featureId };
327
+ }
328
+ function loadVerdict(runDir, wbs) {
329
+ return JSON.parse(readFileSync(join(runDir, `${wbs}-verdict.json`), "utf8"));
330
+ }
331
+ function scanMode(opts, env, io) {
332
+ const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
333
+ const runDir = join(opts.root, ".spur", "run");
334
+ mkdirSync(runDir, { recursive: true });
335
+ const artifact = scanResiduals(opts.root, opts.wbs, opts.tmpDir, task.content, env);
336
+ writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(artifact, null, 2)}
337
+ `);
338
+ io.out(`residual-scan: ${opts.wbs} blocking=${artifact.counts.blocking} deferrable=${artifact.counts.deferrable} advisory=${artifact.counts.advisory} housekeeping=${artifact.counts.housekeeping}
339
+ `);
340
+ return 0;
341
+ }
342
+ function foldMode(opts, _env, io) {
343
+ const runDir = join(opts.root, ".spur", "run");
344
+ const scan = JSON.parse(readFileSync(join(runDir, `${opts.wbs}-residuals.json`), "utf8"));
345
+ const verdictPath = join(runDir, `${opts.wbs}-verdict.json`);
346
+ const verdict = loadVerdict(runDir, opts.wbs);
347
+ const findingsPath = join(runDir, `${opts.wbs}-test-gate.findings`);
348
+ const existing = existsSync(findingsPath) ? readFileSync(findingsPath, "utf8") : "";
349
+ const folded = foldVerdict(verdict, scan, existing);
350
+ writeFileSync(verdictPath, `${JSON.stringify({ ...verdict, verdict: folded.verdict, checks: folded.checks }, null, 2)}
351
+ `);
352
+ writeFileSync(findingsPath, folded.findings);
353
+ io.out(`residual-fold: ${opts.wbs} verdict=${folded.verdict} residual-sweep=${folded.checks.find((c) => c.name === "residual-sweep")?.status}
354
+ `);
355
+ return 0;
356
+ }
357
+ function settleMode(opts, env, io) {
358
+ const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
359
+ const runDir = join(opts.root, ".spur", "run");
360
+ const scan = scanResiduals(opts.root, opts.wbs, opts.tmpDir, task.content, env);
361
+ const residualsPath = join(runDir, `${opts.wbs}-residuals.json`);
362
+ const deferred = scan.items.filter((i) => i.class === "deferrable");
363
+ const prior = existsSync(residualsPath) ? JSON.parse(readFileSync(residualsPath, "utf8")) : {};
364
+ if (deferred.length > 0 && prior.followUp === undefined) {
365
+ if (task.featureId === "") {
366
+ io.err(`residual-settle: ${opts.wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${opts.wbs}
367
+ `);
368
+ return 0;
369
+ }
370
+ const created = spur(env, opts.spurBin, ["task", "create", `Residuals from ${opts.wbs}`, "--feature", task.featureId, "--skip-ready", "--json"], opts.root);
371
+ if (created.status !== 0) {
372
+ io.err(`residual-settle: task create failed; re-run: residual-scan settle ${opts.wbs}
373
+ `);
374
+ return 0;
375
+ }
376
+ let wbsNew = "";
377
+ try {
378
+ const parsed = JSON.parse(created.stdout);
379
+ const pick = (o) => typeof o.wbs === "string" ? o.wbs : "";
380
+ wbsNew = pick(parsed) || (parsed.data !== null && typeof parsed.data === "object" ? pick(parsed.data) : "");
381
+ } catch {
382
+ wbsNew = "";
383
+ }
384
+ if (wbsNew === "") {
385
+ io.err(`residual-settle: could not read created task wbs; re-run: residual-scan settle ${opts.wbs}
386
+ `);
387
+ return 0;
388
+ }
389
+ const bg = [
390
+ `Source task: ${opts.wbs} (feature ${task.featureId}) \u2014 deferred residuals filed by residual-scan settle.`,
391
+ "",
392
+ ...deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`)
393
+ ].join(`
394
+ `);
395
+ const bgFile = join(runDir, `${opts.wbs}-residual-background.md`);
396
+ writeFileSync(bgFile, `${bg}
397
+ `);
398
+ const upd = spur(env, opts.spurBin, ["task", "update", wbsNew, "--section", "Background", "--from-file", bgFile], opts.root);
399
+ if (upd.status !== 0) {
400
+ io.err(`residual-settle: background write failed; re-run: residual-scan settle ${opts.wbs}
401
+ `);
402
+ return 0;
403
+ }
404
+ prior.followUp = wbsNew;
405
+ io.out(`residual-settle: filed follow-up ${wbsNew} for ${deferred.length} deferred item(s)
406
+ `);
407
+ }
408
+ for (const path of listStagingResidue(opts.tmpDir, opts.wbs)) {
409
+ try {
410
+ rmSync(path, { force: true });
411
+ } catch {
412
+ io.err(`residual-settle: could not remove ${path}; re-run: residual-scan settle ${opts.wbs}
413
+ `);
414
+ return 0;
415
+ }
416
+ }
417
+ writeFileSync(residualsPath, `${JSON.stringify({ ...scan, ...prior }, null, 2)}
418
+ `);
419
+ return 0;
420
+ }
421
+ function reportMode(opts, env, io) {
422
+ const runDir = join(opts.root, ".spur", "run");
423
+ const verdict = loadVerdict(runDir, opts.wbs);
424
+ const sweep = verdict.checks.find((c) => c.name === "residual-sweep");
425
+ if (sweep === undefined || sweep.status !== "fail")
426
+ return 0;
427
+ const task = loadTask(env, opts.spurBin, opts.wbs, opts.root);
428
+ const scan = scanResiduals(opts.root, opts.wbs, opts.tmpDir, task.content, env);
429
+ const blocking = scan.items.filter((i) => i.class === "blocking");
430
+ const attemptFile = join(runDir, `${opts.wbs}-test-fix-attempt`);
431
+ const attempts = existsSync(attemptFile) ? Number.parseInt(readFileSync(attemptFile, "utf8").trim() || "0", 10) : 0;
432
+ const reportPath = join(runDir, `${opts.wbs}-residual-report.md`);
433
+ writeFileSync(reportPath, renderReport(opts.wbs, blocking, Number.isNaN(attempts) ? 0 : attempts));
434
+ io.out(`Recovery: fix the items in .spur/run/${opts.wbs}-residual-report.md, then /sp:dev-run ${opts.wbs}
435
+ `);
436
+ return 0;
437
+ }
438
+ function main(argv, env = getEnvVars(), options = {}) {
439
+ const io = options.io ?? {
440
+ out: (line) => process.stdout.write(line),
441
+ err: (line) => process.stderr.write(line)
442
+ };
443
+ const opts = parseArgs(argv);
444
+ if (opts === null) {
445
+ io.err(`${RESIDUAL_SCAN_USAGE}
446
+ `);
447
+ return 2;
448
+ }
449
+ const cwd = options.cwd ?? process.cwd();
450
+ const resolved = { ...opts, root: opts.root.startsWith("/") ? opts.root : join(cwd, opts.root) };
451
+ if (resolved.mode === "scan")
452
+ return scanMode(resolved, env, io);
453
+ if (resolved.mode === "fold")
454
+ return foldMode(resolved, env, io);
455
+ if (resolved.mode === "settle")
456
+ return settleMode(resolved, env, io);
457
+ if (resolved.mode === "report")
458
+ return reportMode(resolved, env, io);
459
+ io.err(`${RESIDUAL_SCAN_USAGE}
460
+ `);
461
+ return 2;
462
+ }
463
+ {
464
+ process.exit(main(process.argv.slice(2)));
465
+ }
466
+ export {
467
+ spurCommand,
468
+ scanResiduals,
469
+ renderReport,
470
+ parseReviewFindings,
471
+ parseDiffMarkers,
472
+ normalizeAnchor,
473
+ makeItemId,
474
+ main,
475
+ locationOf,
476
+ listStagingResidue,
477
+ foldVerdict,
478
+ findUncheckedBoxes,
479
+ collectAddedLines,
480
+ classify,
481
+ blockingAnchors,
482
+ RESIDUAL_SCAN_USAGE,
483
+ ALLOW_PRAGMA
484
+ };