@gobing-ai/spur 0.4.0 → 0.4.2
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.global.yaml +4 -2
- 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/templates/AGENTS.md +1 -1
- 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 +107 -14
- package/package.json +10 -10
- package/plugins/sp/README.md +12 -3
- package/plugins/sp/agents/super-planner.md +16 -6
- 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-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +4 -1
- package/plugins/sp/commands/dev-run.md +12 -2
- package/plugins/sp/commands/dev-runall.md +32 -15
- package/plugins/sp/hooks/context-owner.ts +56 -0
- package/plugins/sp/hooks/context-post-tool.ts +23 -0
- package/plugins/sp/hooks/context-session-start.ts +49 -27
- package/plugins/sp/hooks/context-session-stop.ts +131 -30
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
- package/plugins/sp/lib/idea-handoff.generated.mjs +43 -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 +351 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +47 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +368 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +35 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30296 -71
- 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 +150 -7
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +488 -0
- package/plugins/sp/scripts/batch-plan.ts +145 -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 +562 -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/pr-reviewing.mjs +19 -5
- package/plugins/sp/scripts/pr-reviewing.ts +34 -9
- package/plugins/sp/scripts/quality-gate.mjs +30213 -61
- 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 +432 -14
- package/plugins/sp/scripts/run-summary.ts +41 -177
- package/plugins/sp/scripts/session-timeline.mjs +109 -11
- package/plugins/sp/scripts/session-timeline.ts +57 -19
- package/plugins/sp/scripts/task-diffstat.mjs +32 -29
- package/plugins/sp/scripts/task-diffstat.ts +34 -34
- 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/indexed-context/SKILL.md +14 -7
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
- package/plugins/sp/skills/session-review/SKILL.md +40 -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 +16 -8
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
- 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 +20 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +33 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -17
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +199 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +156 -1024
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +133 -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 +44 -12
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +340 -165
- 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 +42402 -26889
- package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.B3GQZvj8.js} +1 -1
- package/web/_astro/BoardApp.BT7L2kSb.js +324 -0
- package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
- package/web/_astro/{arc.f4e0OYLg.js → arc.vaT7tPOB.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
- package/web/_astro/channel.D4nG9YLf.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
- package/web/_astro/index.BhSwN8yY.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
- package/web/_astro/{linear.DguUUe6V.js → linear.BzGnBwNL.js} +1 -1
- package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.CSbTpIVz.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/commands/dev-parallel.md +0 -31
- package/web/_astro/BoardApp.Bzr4OXrW.js +0 -324
- package/web/_astro/TaskDetail.LAyR_Pvn.js +0 -1
- package/web/_astro/channel.CUFdtWhm.js +0 -1
- package/web/_astro/index.Dg_3bOaJ.css +0 -1
|
@@ -16,7 +16,7 @@ var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/s
|
|
|
16
16
|
var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
|
|
17
17
|
function globToRegex(glob) {
|
|
18
18
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
|
|
19
|
-
return new RegExp(`^${escaped}
|
|
19
|
+
return new RegExp(`^${escaped}$`, "s");
|
|
20
20
|
}
|
|
21
21
|
var SENSITIVE_MATCHERS = [
|
|
22
22
|
...SENSITIVE_PREFIXES.map((prefix) => ({
|
|
@@ -52,22 +52,12 @@ function countNewlines(path, cwd) {
|
|
|
52
52
|
lines += 1;
|
|
53
53
|
return lines;
|
|
54
54
|
}
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (open !== -1 && close > open && open < arrow && arrow < close) {
|
|
62
|
-
const inner = raw.slice(open + 1, close);
|
|
63
|
-
const innerArrow = inner.indexOf(" => ");
|
|
64
|
-
if (innerArrow !== -1) {
|
|
65
|
-
const head = raw.slice(0, open);
|
|
66
|
-
const tail = raw.slice(close + 1);
|
|
67
|
-
return [head + inner.slice(0, innerArrow) + tail, head + inner.slice(innerArrow + 4) + tail];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return [raw.slice(0, arrow), raw.slice(arrow + 4)];
|
|
55
|
+
function addCounts(add, del) {
|
|
56
|
+
for (const count of [add, del])
|
|
57
|
+
if (count !== "-" && !/^\d+$/.test(count))
|
|
58
|
+
return null;
|
|
59
|
+
const [a, d] = [add === "-" ? 0 : Number(add), del === "-" ? 0 : Number(del)];
|
|
60
|
+
return Number.isSafeInteger(a) && Number.isSafeInteger(d) ? [a, d] : null;
|
|
71
61
|
}
|
|
72
62
|
function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
|
|
73
63
|
const cwd = options.cwd;
|
|
@@ -99,23 +89,37 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
|
|
|
99
89
|
} catch {}
|
|
100
90
|
if (!/^[0-9a-f]{7,40}$/i.test(base))
|
|
101
91
|
return failSafe("run base .spur/run/<wbs>-base.sha missing or malformed");
|
|
102
|
-
const numstat = git(["diff", "--numstat", base]);
|
|
92
|
+
const numstat = git(["diff", "--numstat", "-z", base]);
|
|
103
93
|
if (numstat.status !== 0)
|
|
104
94
|
return failSafe(`git diff --numstat failed (status=${numstat.status})`);
|
|
95
|
+
if (numstat.stdout.length > 0 && !numstat.stdout.endsWith("\x00"))
|
|
96
|
+
return failSafe("git diff --numstat -z output is not NUL-terminated (malformed framing)");
|
|
105
97
|
const paths = new Set;
|
|
106
98
|
let insertions = 0;
|
|
107
99
|
let deletions = 0;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
100
|
+
const records = numstat.stdout.split("\x00").slice(0, -1);
|
|
101
|
+
for (let i = 0;i < records.length; i++) {
|
|
102
|
+
const header = records[i] ?? "";
|
|
103
|
+
const first = header.indexOf("\t");
|
|
104
|
+
const second = header.indexOf("\t", first + 1);
|
|
105
|
+
if (first < 1 || second < first + 2)
|
|
106
|
+
return failSafe("git diff --numstat -z record is missing counts or path");
|
|
107
|
+
const counts = addCounts(header.slice(0, first), header.slice(first + 1, second));
|
|
108
|
+
if (counts === null)
|
|
109
|
+
return failSafe("git diff --numstat -z record has a malformed count");
|
|
110
|
+
insertions += counts[0];
|
|
111
|
+
deletions += counts[1];
|
|
112
|
+
const path = header.slice(second + 1);
|
|
113
|
+
if (path !== "") {
|
|
114
|
+
paths.add(path);
|
|
112
115
|
continue;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
}
|
|
117
|
+
const oldPath = records[++i];
|
|
118
|
+
const newPath = records[++i];
|
|
119
|
+
if (!oldPath || !newPath)
|
|
120
|
+
return failSafe("git diff --numstat -z rename record has incomplete paths");
|
|
121
|
+
paths.add(oldPath);
|
|
122
|
+
paths.add(newPath);
|
|
119
123
|
}
|
|
120
124
|
const others = git(["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
121
125
|
if (others.status !== 0)
|
|
@@ -161,7 +165,6 @@ export {
|
|
|
161
165
|
sensitiveReasonForPath,
|
|
162
166
|
runDiffstat,
|
|
163
167
|
main,
|
|
164
|
-
expandDiffPath,
|
|
165
168
|
TASK_DIFFSTAT_USAGE,
|
|
166
169
|
SENSITIVE_PREFIXES,
|
|
167
170
|
SENSITIVE_GLOBS
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* `precheck`, F96 R1) and the current working tree, then writes:
|
|
8
8
|
* `.spur/run/<wbs>-diffstat.json` `{ files, insertions, deletions, paths[], sensitive }`
|
|
9
9
|
*
|
|
10
|
-
* Counts come from `git diff --numstat <base>` (tracked changes; binary rows count the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* if a miscount
|
|
10
|
+
* Counts come from `git diff --numstat -z <base>` (tracked changes; binary rows count the path
|
|
11
|
+
* but no lines; NUL records carry RAW paths, so a non-ASCII or newline-bearing path reaches the
|
|
12
|
+
* sensitive matchers intact) plus untracked files (`git ls-files --others --exclude-standard -z`,
|
|
13
|
+
* newline-counted — # ponytail: untracked line counts are a newline scan, numstat-grade binary
|
|
14
|
+
* detection is unnecessary here; switch to `--numstat --no-index` if a miscount flips a lane).
|
|
15
15
|
*
|
|
16
16
|
* `sensitive` is true when ANY changed path matches the exported SENSITIVE list (0943 R2c);
|
|
17
17
|
* the workflow's pre-decide guard maps sensitive (or >400 changed lines) to the safety lane
|
|
@@ -85,7 +85,7 @@ function globToRegex(glob: string): RegExp {
|
|
|
85
85
|
.replaceAll('*', '[^/]*')
|
|
86
86
|
.replaceAll('\u0000', '(?:.*/)?')
|
|
87
87
|
.replaceAll('\u0001', '.*');
|
|
88
|
-
return new RegExp(`^${escaped}
|
|
88
|
+
return new RegExp(`^${escaped}$`, 's');
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const SENSITIVE_MATCHERS: Array<{ label: string; test: (path: string) => boolean }> = [
|
|
@@ -125,26 +125,10 @@ function countNewlines(path: string, cwd?: string): number {
|
|
|
125
125
|
return lines;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*/
|
|
133
|
-
export function expandDiffPath(raw: string): string[] {
|
|
134
|
-
const arrow = raw.indexOf(' => ');
|
|
135
|
-
if (arrow === -1) return [raw];
|
|
136
|
-
const open = raw.indexOf('{');
|
|
137
|
-
const close = raw.lastIndexOf('}');
|
|
138
|
-
if (open !== -1 && close > open && open < arrow && arrow < close) {
|
|
139
|
-
const inner = raw.slice(open + 1, close);
|
|
140
|
-
const innerArrow = inner.indexOf(' => ');
|
|
141
|
-
if (innerArrow !== -1) {
|
|
142
|
-
const head = raw.slice(0, open);
|
|
143
|
-
const tail = raw.slice(close + 1);
|
|
144
|
-
return [head + inner.slice(0, innerArrow) + tail, head + inner.slice(innerArrow + 4) + tail];
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return [raw.slice(0, arrow), raw.slice(arrow + 4)];
|
|
128
|
+
function addCounts(add: string, del: string): [number, number] | null {
|
|
129
|
+
for (const count of [add, del]) if (count !== '-' && !/^\d+$/.test(count)) return null;
|
|
130
|
+
const [a, d] = [add === '-' ? 0 : Number(add), del === '-' ? 0 : Number(del)];
|
|
131
|
+
return Number.isSafeInteger(a) && Number.isSafeInteger(d) ? [a, d] : null;
|
|
148
132
|
}
|
|
149
133
|
|
|
150
134
|
/**
|
|
@@ -190,18 +174,34 @@ export function runDiffstat(
|
|
|
190
174
|
}
|
|
191
175
|
if (!/^[0-9a-f]{7,40}$/i.test(base)) return failSafe('run base .spur/run/<wbs>-base.sha missing or malformed');
|
|
192
176
|
|
|
193
|
-
const numstat = git(['diff', '--numstat', base]);
|
|
177
|
+
const numstat = git(['diff', '--numstat', '-z', base]);
|
|
194
178
|
if (numstat.status !== 0) return failSafe(`git diff --numstat failed (status=${numstat.status})`);
|
|
179
|
+
if (numstat.stdout.length > 0 && !numstat.stdout.endsWith('\u0000'))
|
|
180
|
+
return failSafe('git diff --numstat -z output is not NUL-terminated (malformed framing)');
|
|
195
181
|
const paths = new Set<string>();
|
|
196
182
|
let insertions = 0;
|
|
197
183
|
let deletions = 0;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
184
|
+
const records = numstat.stdout.split('\u0000').slice(0, -1); // drop the terminating NUL tail
|
|
185
|
+
for (let i = 0; i < records.length; i++) {
|
|
186
|
+
const header = records[i] ?? '';
|
|
187
|
+
const first = header.indexOf('\t'); // first two tabs only
|
|
188
|
+
const second = header.indexOf('\t', first + 1); // a raw path may itself hold more tabs
|
|
189
|
+
if (first < 1 || second < first + 2) return failSafe('git diff --numstat -z record is missing counts or path');
|
|
190
|
+
const counts = addCounts(header.slice(0, first), header.slice(first + 1, second));
|
|
191
|
+
if (counts === null) return failSafe('git diff --numstat -z record has a malformed count');
|
|
192
|
+
insertions += counts[0];
|
|
193
|
+
deletions += counts[1]; // once per count header, never once per rename half
|
|
194
|
+
const path = header.slice(second + 1);
|
|
195
|
+
if (path !== '') {
|
|
196
|
+
paths.add(path);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// An empty path field is Git's rename/copy form: the next two records are old and new.
|
|
200
|
+
const oldPath = records[++i];
|
|
201
|
+
const newPath = records[++i];
|
|
202
|
+
if (!oldPath || !newPath) return failSafe('git diff --numstat -z rename record has incomplete paths');
|
|
203
|
+
paths.add(oldPath);
|
|
204
|
+
paths.add(newPath);
|
|
205
205
|
}
|
|
206
206
|
const others = git(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
207
207
|
if (others.status !== 0) return failSafe(`git ls-files failed (status=${others.status})`);
|
|
@@ -443,7 +443,137 @@ function runFeatureTransition(env, options = {}) {
|
|
|
443
443
|
`);
|
|
444
444
|
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
445
445
|
}
|
|
446
|
-
var
|
|
446
|
+
var DOC_SYNC_SNAPSHOT_SUFFIX = "wrapup-doc-sync-snapshot.txt";
|
|
447
|
+
var DOC_SYNC_SCOPE_STATUS_SUFFIX = "wrapup-doc-sync-scope.status";
|
|
448
|
+
var DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = "wrapup-doc-supersession.status";
|
|
449
|
+
var SUPERSESSION_PIN = "repo-wide-tests/adr-supersession.test.ts";
|
|
450
|
+
var GIT_UNAVAILABLE_MARKER = "# git-unavailable:";
|
|
451
|
+
var SNAPSHOT_MISSING = "snapshot-missing";
|
|
452
|
+
function porcelainPath(line) {
|
|
453
|
+
const body = line.length >= 3 ? line.slice(3) : line;
|
|
454
|
+
const arrow = body.indexOf(" -> ");
|
|
455
|
+
const target = arrow >= 0 ? body.slice(arrow + 4) : body;
|
|
456
|
+
const trimmed = target.trim();
|
|
457
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
|
|
458
|
+
}
|
|
459
|
+
function isDocSyncAllowedPath(path, allowedExtra) {
|
|
460
|
+
if (allowedExtra.includes(path))
|
|
461
|
+
return true;
|
|
462
|
+
if (!path.startsWith("docs/"))
|
|
463
|
+
return false;
|
|
464
|
+
return !path.startsWith("docs/tasks") && !path.startsWith("docs/features");
|
|
465
|
+
}
|
|
466
|
+
function docSyncScopeViolations(snapshot, current, allowedExtra) {
|
|
467
|
+
const before = new Set(snapshot.split(`
|
|
468
|
+
`).filter((line) => line.trim() !== ""));
|
|
469
|
+
const paths = new Set;
|
|
470
|
+
for (const line of current.split(`
|
|
471
|
+
`)) {
|
|
472
|
+
if (line.trim() === "" || before.has(line))
|
|
473
|
+
continue;
|
|
474
|
+
const path = porcelainPath(line);
|
|
475
|
+
if (!isDocSyncAllowedPath(path, allowedExtra))
|
|
476
|
+
paths.add(path);
|
|
477
|
+
}
|
|
478
|
+
return [...paths].sort();
|
|
479
|
+
}
|
|
480
|
+
function gitPorcelain(cwd) {
|
|
481
|
+
const result = spawnSync("git", ["status", "--porcelain", "-uall"], {
|
|
482
|
+
cwd,
|
|
483
|
+
encoding: "utf8"
|
|
484
|
+
});
|
|
485
|
+
if (result.error !== undefined)
|
|
486
|
+
return { ok: false, listing: "", detail: String(result.error) };
|
|
487
|
+
if (result.status !== 0)
|
|
488
|
+
return { ok: false, listing: "", detail: (result.stderr ?? "").trim() };
|
|
489
|
+
return { ok: true, listing: result.stdout ?? "", detail: "" };
|
|
490
|
+
}
|
|
491
|
+
function wrapupRunPaths(env, cwd) {
|
|
492
|
+
const runId = env.__runId ?? "";
|
|
493
|
+
const rel = (suffix) => join(".spur", "run", `${runId}-${suffix}`);
|
|
494
|
+
return { rel, abs: (suffix) => cwd ? join(cwd, rel(suffix)) : rel(suffix) };
|
|
495
|
+
}
|
|
496
|
+
function docSyncAllowedExtra(env) {
|
|
497
|
+
const runId = env.__runId ?? "";
|
|
498
|
+
return [
|
|
499
|
+
join(".spur", "run", `${runId}-wrapup-learnings.md`),
|
|
500
|
+
join(".spur", "run", `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
|
|
501
|
+
join(".spur", "run", `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`)
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
function runDocSyncSnapshot(env, options = {}) {
|
|
505
|
+
const cwd = options.cwd;
|
|
506
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
507
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
508
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), "");
|
|
509
|
+
const listing = gitPorcelain(cwd);
|
|
510
|
+
if (!listing.ok) {
|
|
511
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}
|
|
512
|
+
`);
|
|
513
|
+
process.stderr.write(`doc-sync-snapshot: git status unavailable \u2014 scope check skips (${listing.detail})
|
|
514
|
+
`);
|
|
515
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
|
|
516
|
+
}
|
|
517
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
|
|
518
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
|
|
519
|
+
}
|
|
520
|
+
function runDocSyncScope(env, options = {}) {
|
|
521
|
+
const cwd = options.cwd;
|
|
522
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
523
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
524
|
+
const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
|
|
525
|
+
const write = (status, detail) => {
|
|
526
|
+
writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}
|
|
527
|
+
`);
|
|
528
|
+
if (status !== "PASS")
|
|
529
|
+
process.stderr.write(`doc-sync-scope: ${detail}
|
|
530
|
+
`);
|
|
531
|
+
return { status, statusFile, violations: status === "PASS" ? [] : [detail], exitCode: 0 };
|
|
532
|
+
};
|
|
533
|
+
const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
|
|
534
|
+
if (snapshot === null) {
|
|
535
|
+
return write("scope-violation", `scope-violation: ${SNAPSHOT_MISSING}`);
|
|
536
|
+
}
|
|
537
|
+
if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
|
|
538
|
+
process.stderr.write(`doc-sync-scope: entry snapshot recorded no git listing \u2014 scope check skipped
|
|
539
|
+
`);
|
|
540
|
+
return write("PASS", "PASS");
|
|
541
|
+
}
|
|
542
|
+
const current = gitPorcelain(cwd);
|
|
543
|
+
if (!current.ok) {
|
|
544
|
+
return write("scope-violation", `scope-violation: git-unavailable (${current.detail})`);
|
|
545
|
+
}
|
|
546
|
+
const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
|
|
547
|
+
if (violations.length === 0)
|
|
548
|
+
return write("PASS", "PASS");
|
|
549
|
+
return write("scope-violation", `scope-violation: ${violations.join(" ")}`);
|
|
550
|
+
}
|
|
551
|
+
function runDocSupersessionCheck(env, options = {}) {
|
|
552
|
+
const cwd = options.cwd;
|
|
553
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
554
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
555
|
+
const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
|
|
556
|
+
const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
|
|
557
|
+
if (!existsSync(pinPath)) {
|
|
558
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
|
|
559
|
+
`);
|
|
560
|
+
return { status: "PASS", statusFile, exitCode: 0 };
|
|
561
|
+
}
|
|
562
|
+
const pin = spawnSync("bun", ["test", SUPERSESSION_PIN], { cwd, encoding: "utf8" });
|
|
563
|
+
if (pin.status === 0) {
|
|
564
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
|
|
565
|
+
`);
|
|
566
|
+
return { status: "PASS", statusFile, exitCode: 0 };
|
|
567
|
+
}
|
|
568
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}
|
|
569
|
+
`);
|
|
570
|
+
const detail = `${pin.stdout ?? ""}${pin.stderr ?? ""}`.trim();
|
|
571
|
+
process.stderr.write(`doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) \u2014 the doc-sync step introduced or kept superseded index content:
|
|
572
|
+
${detail.slice(-2000)}
|
|
573
|
+
`);
|
|
574
|
+
return { status: "supersession-pin-failed", statusFile, exitCode: 0 };
|
|
575
|
+
}
|
|
576
|
+
var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
|
|
447
577
|
function main(argv, env = getEnvVars(), options = {}) {
|
|
448
578
|
const sub = argv[0];
|
|
449
579
|
if (sub === "resolve")
|
|
@@ -456,6 +586,12 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
456
586
|
}
|
|
457
587
|
if (sub === "feature-transition")
|
|
458
588
|
return runFeatureTransition(env, options).exitCode;
|
|
589
|
+
if (sub === "doc-sync-snapshot")
|
|
590
|
+
return runDocSyncSnapshot(env, options).exitCode;
|
|
591
|
+
if (sub === "doc-sync-scope")
|
|
592
|
+
return runDocSyncScope(env, options).exitCode;
|
|
593
|
+
if (sub === "doc-supersession")
|
|
594
|
+
return runDocSupersessionCheck(env, options).exitCode;
|
|
459
595
|
process.stderr.write(`${WRAPUP_STEPS_USAGE}
|
|
460
596
|
`);
|
|
461
597
|
return 2;
|
|
@@ -469,11 +605,23 @@ export {
|
|
|
469
605
|
taskStatusOf,
|
|
470
606
|
runMetrics,
|
|
471
607
|
runFeatureTransition,
|
|
608
|
+
runDocSyncSnapshot,
|
|
609
|
+
runDocSyncScope,
|
|
610
|
+
runDocSupersessionCheck,
|
|
472
611
|
resolveTasks,
|
|
473
612
|
preflightFeature,
|
|
613
|
+
porcelainPath,
|
|
474
614
|
main,
|
|
475
615
|
jqPick,
|
|
616
|
+
isDocSyncAllowedPath,
|
|
617
|
+
docSyncScopeViolations,
|
|
476
618
|
classifySync,
|
|
477
619
|
WRAPUP_STEPS_USAGE,
|
|
478
|
-
WBS_PATTERN
|
|
620
|
+
WBS_PATTERN,
|
|
621
|
+
SUPERSESSION_PIN,
|
|
622
|
+
SNAPSHOT_MISSING,
|
|
623
|
+
GIT_UNAVAILABLE_MARKER,
|
|
624
|
+
DOC_SYNC_SUPERSESSION_STATUS_SUFFIX,
|
|
625
|
+
DOC_SYNC_SNAPSHOT_SUFFIX,
|
|
626
|
+
DOC_SYNC_SCOPE_STATUS_SUFFIX
|
|
479
627
|
};
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
15
15
|
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
16
16
|
* - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
|
|
17
|
+
* - `<runId>-wrapup-doc-sync-snapshot.txt` working listing at doc-sync entry (doc-sync-snapshot)
|
|
18
|
+
* - `<runId>-wrapup-doc-sync-scope.status` `PASS`/`scope-violation: <paths>` (doc-sync-scope)
|
|
19
|
+
* - `<runId>-wrapup-doc-supersession.status` `PASS`/`supersession-pin-failed: <pin>` (doc-supersession)
|
|
17
20
|
*
|
|
18
21
|
* Environment comes from the workflow vars: `__runId`, `tasks`, `feature`, `featureGateCmd`
|
|
19
22
|
* and `spurBin` (split on whitespace into a command plus prefix args, so
|
|
@@ -668,8 +671,204 @@ export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOp
|
|
|
668
671
|
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
669
672
|
}
|
|
670
673
|
|
|
674
|
+
/** Run-scoped snapshot of the working listing taken at doc-sync entry (1147 R1). */
|
|
675
|
+
export const DOC_SYNC_SNAPSHOT_SUFFIX = 'wrapup-doc-sync-snapshot.txt';
|
|
676
|
+
/** Run-scoped verdict of the post-doc-sync write-scope check (1147 R1). */
|
|
677
|
+
export const DOC_SYNC_SCOPE_STATUS_SUFFIX = 'wrapup-doc-sync-scope.status';
|
|
678
|
+
/** Run-scoped verdict of the post-doc-sync supersession-pin re-run (1147 R2). */
|
|
679
|
+
export const DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = 'wrapup-doc-supersession.status';
|
|
680
|
+
/**
|
|
681
|
+
* The supersession pins doc-sync must not resurrect (1147 R2). Re-run right after doc-sync so a
|
|
682
|
+
* re-added delinked index row is attributed to doc-sync instead of to the later `doc-tripwire`
|
|
683
|
+
* backstop, which stays unchanged.
|
|
684
|
+
*/
|
|
685
|
+
export const SUPERSESSION_PIN = 'repo-wide-tests/adr-supersession.test.ts';
|
|
686
|
+
/** Snapshot marker for "git could not list the tree" — the check then skips instead of guessing. */
|
|
687
|
+
export const GIT_UNAVAILABLE_MARKER = '# git-unavailable:';
|
|
688
|
+
|
|
689
|
+
/** Snapshot marker the scope check reports when doc-sync's entry snapshot is absent (fail closed). */
|
|
690
|
+
export const SNAPSHOT_MISSING = 'snapshot-missing';
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* One `git status --porcelain` line → the repo-relative path it names. A rename/copy line
|
|
694
|
+
* (`R old -> new`) reports its target, and a quoted path is unquoted (1147 R1).
|
|
695
|
+
*/
|
|
696
|
+
export function porcelainPath(line: string): string {
|
|
697
|
+
const body = line.length >= 3 ? line.slice(3) : line;
|
|
698
|
+
const arrow = body.indexOf(' -> ');
|
|
699
|
+
const target = arrow >= 0 ? body.slice(arrow + 4) : body;
|
|
700
|
+
const trimmed = target.trim();
|
|
701
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* doc-sync's declared write scope (1147 R1): `docs/**` except the task/feature corpus, the
|
|
706
|
+
* learnings capture the step declares, and the guard's own snapshot/status bookkeeping (never
|
|
707
|
+
* doc-sync edits, and present in the listing on a repeated check).
|
|
708
|
+
*/
|
|
709
|
+
export function isDocSyncAllowedPath(path: string, allowedExtra: readonly string[]): boolean {
|
|
710
|
+
if (allowedExtra.includes(path)) return true;
|
|
711
|
+
if (!path.startsWith('docs/')) return false;
|
|
712
|
+
return !path.startsWith('docs/tasks') && !path.startsWith('docs/features');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Paths doc-sync newly dirtied: the current porcelain listing minus the entry snapshot's lines.
|
|
717
|
+
* A line present in both listings is pre-existing dirt (a sibling task's uncommitted work) and is
|
|
718
|
+
* never attributed to this pass (1147 R1).
|
|
719
|
+
*/
|
|
720
|
+
export function docSyncScopeViolations(snapshot: string, current: string, allowedExtra: readonly string[]): string[] {
|
|
721
|
+
const before = new Set(snapshot.split('\n').filter((line) => line.trim() !== ''));
|
|
722
|
+
const paths = new Set<string>();
|
|
723
|
+
for (const line of current.split('\n')) {
|
|
724
|
+
if (line.trim() === '' || before.has(line)) continue;
|
|
725
|
+
const path = porcelainPath(line);
|
|
726
|
+
if (!isDocSyncAllowedPath(path, allowedExtra)) paths.add(path);
|
|
727
|
+
}
|
|
728
|
+
return [...paths].sort();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** `git status --porcelain -uall` — the listing both the snapshot and the scope check compare. */
|
|
732
|
+
function gitPorcelain(cwd?: string): { ok: boolean; listing: string; detail: string } {
|
|
733
|
+
const result = spawnSync('git', ['status', '--porcelain', '-uall'], {
|
|
734
|
+
cwd,
|
|
735
|
+
encoding: 'utf8',
|
|
736
|
+
});
|
|
737
|
+
if (result.error !== undefined) return { ok: false, listing: '', detail: String(result.error) };
|
|
738
|
+
if (result.status !== 0) return { ok: false, listing: '', detail: (result.stderr ?? '').trim() };
|
|
739
|
+
return { ok: true, listing: result.stdout ?? '', detail: '' };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
interface WrapupRunPaths {
|
|
743
|
+
rel: (suffix: string) => string;
|
|
744
|
+
abs: (suffix: string) => string;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function wrapupRunPaths(env: WrapupStepsEnv, cwd?: string): WrapupRunPaths {
|
|
748
|
+
const runId = env.__runId ?? '';
|
|
749
|
+
const rel = (suffix: string): string => join('.spur', 'run', `${runId}-${suffix}`);
|
|
750
|
+
return { rel, abs: (suffix: string) => (cwd ? join(cwd, rel(suffix)) : rel(suffix)) };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** The four path shapes this check knows are not doc-sync edits (1147 R1). */
|
|
754
|
+
function docSyncAllowedExtra(env: WrapupStepsEnv): string[] {
|
|
755
|
+
const runId = env.__runId ?? '';
|
|
756
|
+
return [
|
|
757
|
+
join('.spur', 'run', `${runId}-wrapup-learnings.md`),
|
|
758
|
+
join('.spur', 'run', `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
|
|
759
|
+
join('.spur', 'run', `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`),
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Outcome of `doc-sync-snapshot`: the recorded entry listing (or a git-unavailable marker). */
|
|
764
|
+
export interface DocSyncSnapshotResult {
|
|
765
|
+
statusFile: string;
|
|
766
|
+
/** True when the listing was captured; false when git could not list the tree. */
|
|
767
|
+
captured: boolean;
|
|
768
|
+
exitCode: number;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* `doc-sync-snapshot` — record the working listing at doc-sync entry (1147 R1), before the model
|
|
773
|
+
* query runs, so the post-doc-sync check can attribute only this step's writes. Never aborts: a
|
|
774
|
+
* git failure is recorded in the snapshot (the check then skips rather than guessing).
|
|
775
|
+
*/
|
|
776
|
+
export function runDocSyncSnapshot(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSyncSnapshotResult {
|
|
777
|
+
const cwd = options.cwd;
|
|
778
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
779
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
780
|
+
// Create the file BEFORE listing so it is present in both listings (never a false violation).
|
|
781
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), '');
|
|
782
|
+
const listing = gitPorcelain(cwd);
|
|
783
|
+
if (!listing.ok) {
|
|
784
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}\n`);
|
|
785
|
+
process.stderr.write(`doc-sync-snapshot: git status unavailable — scope check skips (${listing.detail})\n`);
|
|
786
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
|
|
787
|
+
}
|
|
788
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
|
|
789
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Outcome of `doc-sync-scope`: the recorded verdict plus the offending paths it named. */
|
|
793
|
+
export interface DocSyncScopeResult {
|
|
794
|
+
status: 'PASS' | 'scope-violation';
|
|
795
|
+
statusFile: string;
|
|
796
|
+
violations: string[];
|
|
797
|
+
exitCode: number;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* `doc-sync-scope` — the deterministic write-scope guard (1147 R1): diff the working listing
|
|
802
|
+
* against the entry snapshot and fail the step on any path outside the declared scope. The
|
|
803
|
+
* violating edits are reported, never reverted (the operator decides); the verdict lives in the
|
|
804
|
+
* status file so the workflow guard routes on it.
|
|
805
|
+
*/
|
|
806
|
+
export function runDocSyncScope(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSyncScopeResult {
|
|
807
|
+
const cwd = options.cwd;
|
|
808
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
809
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
810
|
+
const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
|
|
811
|
+
const write = (status: DocSyncScopeResult['status'], detail: string): DocSyncScopeResult => {
|
|
812
|
+
writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}\n`);
|
|
813
|
+
if (status !== 'PASS') process.stderr.write(`doc-sync-scope: ${detail}\n`);
|
|
814
|
+
return { status, statusFile, violations: status === 'PASS' ? [] : [detail], exitCode: 0 };
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
|
|
818
|
+
if (snapshot === null) {
|
|
819
|
+
return write('scope-violation', `scope-violation: ${SNAPSHOT_MISSING}`);
|
|
820
|
+
}
|
|
821
|
+
if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
|
|
822
|
+
process.stderr.write('doc-sync-scope: entry snapshot recorded no git listing — scope check skipped\n');
|
|
823
|
+
return write('PASS', 'PASS');
|
|
824
|
+
}
|
|
825
|
+
const current = gitPorcelain(cwd);
|
|
826
|
+
if (!current.ok) {
|
|
827
|
+
return write('scope-violation', `scope-violation: git-unavailable (${current.detail})`);
|
|
828
|
+
}
|
|
829
|
+
const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
|
|
830
|
+
if (violations.length === 0) return write('PASS', 'PASS');
|
|
831
|
+
return write('scope-violation', `scope-violation: ${violations.join(' ')}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Outcome of `doc-supersession`: the recorded verdict of the supersession-pin re-run. */
|
|
835
|
+
export interface DocSupersessionResult {
|
|
836
|
+
status: 'PASS' | 'supersession-pin-failed';
|
|
837
|
+
statusFile: string;
|
|
838
|
+
exitCode: number;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* `doc-supersession` — re-run the supersession pins right after doc-sync (1147 R2). A doc-sync
|
|
843
|
+
* that resurrects a deliberately delinked index row fails HERE, named with the pin, instead of at
|
|
844
|
+
* the later repo-wide `doc-tripwire` backstop (which is never weakened). A project without the pin
|
|
845
|
+
* skips: this check must stay inert outside the spur source repo.
|
|
846
|
+
*/
|
|
847
|
+
export function runDocSupersessionCheck(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSupersessionResult {
|
|
848
|
+
const cwd = options.cwd;
|
|
849
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
850
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
851
|
+
const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
|
|
852
|
+
const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
|
|
853
|
+
if (!existsSync(pinPath)) {
|
|
854
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), 'PASS\n');
|
|
855
|
+
return { status: 'PASS', statusFile, exitCode: 0 };
|
|
856
|
+
}
|
|
857
|
+
const pin = spawnSync('bun', ['test', SUPERSESSION_PIN], { cwd, encoding: 'utf8' });
|
|
858
|
+
if (pin.status === 0) {
|
|
859
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), 'PASS\n');
|
|
860
|
+
return { status: 'PASS', statusFile, exitCode: 0 };
|
|
861
|
+
}
|
|
862
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}\n`);
|
|
863
|
+
const detail = `${pin.stdout ?? ''}${pin.stderr ?? ''}`.trim();
|
|
864
|
+
process.stderr.write(
|
|
865
|
+
`doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) — the doc-sync step introduced or kept superseded index content:\n${detail.slice(-2000)}\n`,
|
|
866
|
+
);
|
|
867
|
+
return { status: 'supersession-pin-failed', statusFile, exitCode: 0 };
|
|
868
|
+
}
|
|
869
|
+
|
|
671
870
|
export const WRAPUP_STEPS_USAGE =
|
|
672
|
-
'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
|
|
871
|
+
'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
|
|
673
872
|
|
|
674
873
|
export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options: WrapupStepsOptions = {}): number {
|
|
675
874
|
const sub = argv[0];
|
|
@@ -680,6 +879,9 @@ export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options
|
|
|
680
879
|
return 0;
|
|
681
880
|
}
|
|
682
881
|
if (sub === 'feature-transition') return runFeatureTransition(env, options).exitCode;
|
|
882
|
+
if (sub === 'doc-sync-snapshot') return runDocSyncSnapshot(env, options).exitCode;
|
|
883
|
+
if (sub === 'doc-sync-scope') return runDocSyncScope(env, options).exitCode;
|
|
884
|
+
if (sub === 'doc-supersession') return runDocSupersessionCheck(env, options).exitCode;
|
|
683
885
|
process.stderr.write(`${WRAPUP_STEPS_USAGE}\n`);
|
|
684
886
|
return 2;
|
|
685
887
|
}
|