@gobing-ai/spur 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -2
  3. package/config/rules/boundary/config-loading-ownership.yaml +1 -0
  4. package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
  5. package/config/rules/boundary/sp-script-placement.yaml +1 -1
  6. package/config/rules/docs/sourced-api-claims.yaml +45 -7
  7. package/config/rules/strict/runtime-boundaries.yaml +40 -1
  8. package/config/rules/structure/protected-files.yaml +1 -1
  9. package/config/rules/surface/check-cli-surface.yaml +33 -0
  10. package/config/rules/typescript/no-debugger.yaml +3 -0
  11. package/config/workflow-candidates.json +2 -2
  12. package/config/workflows/feature-verification.yaml +6 -11
  13. package/config/workflows/history-anatomy.yaml +14 -28
  14. package/config/workflows/idea-pipeline.yaml +6 -19
  15. package/config/workflows/pr-review.yaml +9 -16
  16. package/config/workflows/task-pipeline.yaml +16 -23
  17. package/config/workflows/wrapup-pipeline.yaml +21 -63
  18. package/package.json +3 -3
  19. package/plugins/sp/README.md +70 -37
  20. package/plugins/sp/agents/super-planner.md +5 -5
  21. package/plugins/sp/agents/super-reviewer.md +2 -2
  22. package/plugins/sp/commands/dev-changelog.md +1 -1
  23. package/plugins/sp/commands/dev-daily.md +1 -1
  24. package/plugins/sp/commands/dev-pr-review.md +1 -1
  25. package/plugins/sp/commands/dev-review-session.md +1 -1
  26. package/plugins/sp/commands/dev-runall.md +4 -4
  27. package/plugins/sp/commands/dev-verify.md +4 -6
  28. package/plugins/sp/commands/dev-verifyall.md +4 -5
  29. package/plugins/sp/hooks/hooks.json +11 -1
  30. package/plugins/sp/lib/context-post-tool.ts +1 -1
  31. package/plugins/sp/lib/context-session-start.ts +1 -1
  32. package/plugins/sp/lib/context-session-stop.ts +1 -1
  33. package/plugins/sp/lib/destructive-policy.ts +65 -1
  34. package/plugins/sp/plugin.json +2 -5
  35. package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
  36. package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
  37. package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
  38. package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
  39. package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
  40. package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
  41. package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
  42. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  43. package/plugins/sp/skills/code-verification/SKILL.md +8 -8
  44. package/plugins/sp/skills/code-verification/references/secu-review.md +4 -3
  45. package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
  46. package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
  47. package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
  48. package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
  49. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
  50. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  51. package/plugins/sp/skills/parallel-execution/SKILL.md +1 -1
  52. package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
  53. package/plugins/sp/skills/session-review/SKILL.md +3 -3
  54. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  55. package/plugins/sp/skills/spur-check/SKILL.md +14 -13
  56. package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
  57. package/plugins/sp/skills/spur-cli/references/agent.md +12 -0
  58. package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
  59. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  60. package/plugins/sp/skills/spur-cli/references/history.md +3 -0
  61. package/plugins/sp/skills/spur-cli/references/self.md +5 -3
  62. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
  63. package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
  64. package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
  65. package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
  66. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
  67. package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -12
  68. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
  69. package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
  70. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
  71. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +8 -11
  72. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
  73. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  74. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
  75. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
  76. package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
  77. package/spur.js +41901 -30598
  78. package/config/plugin-scripts.json +0 -116
  79. package/config/script-placement-baseline.json +0 -33
  80. package/plugins/sp/hooks/careful-guard.ts +0 -74
  81. package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
  82. package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
  83. package/plugins/sp/lib/dogfood-report.ts +0 -278
  84. package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
  85. package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
  86. package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
  87. package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
  88. package/plugins/sp/lib/inline-run-app.ts +0 -68
  89. package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
  90. package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
  91. package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
  92. package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
  93. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
  94. package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
  95. package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
  96. package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
  97. package/plugins/sp/lib/run-summary-core.ts +0 -225
  98. package/plugins/sp/lib/spur-bin.ts +0 -36
  99. package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
  100. package/plugins/sp/lib/step-profile.generated.mjs +0 -174
  101. package/plugins/sp/lib/transcript.ts +0 -112
  102. package/plugins/sp/scripts/batch-plan.mjs +0 -1096
  103. package/plugins/sp/scripts/batch-plan.ts +0 -145
  104. package/plugins/sp/scripts/batch-preflight.mjs +0 -423
  105. package/plugins/sp/scripts/batch-preflight.ts +0 -270
  106. package/plugins/sp/scripts/commit-guard.mjs +0 -226
  107. package/plugins/sp/scripts/commit-guard.ts +0 -248
  108. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
  109. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
  110. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
  111. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
  112. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
  113. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
  114. package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
  115. package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
  116. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
  117. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
  118. package/plugins/sp/scripts/idea-handoff.mjs +0 -32
  119. package/plugins/sp/scripts/idea-handoff.ts +0 -47
  120. package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
  121. package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
  122. package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
  123. package/plugins/sp/scripts/inline-run-setup.ts +0 -198
  124. package/plugins/sp/scripts/persist-out-check.mjs +0 -217
  125. package/plugins/sp/scripts/persist-out-check.ts +0 -249
  126. package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
  127. package/plugins/sp/scripts/pr-reviewing.ts +0 -952
  128. package/plugins/sp/scripts/quality-gate.mjs +0 -30491
  129. package/plugins/sp/scripts/quality-gate.ts +0 -80
  130. package/plugins/sp/scripts/residual-scan.mjs +0 -634
  131. package/plugins/sp/scripts/residual-scan.ts +0 -248
  132. package/plugins/sp/scripts/run-summary.mjs +0 -758
  133. package/plugins/sp/scripts/run-summary.ts +0 -107
  134. package/plugins/sp/scripts/script-root.mjs +0 -129
  135. package/plugins/sp/scripts/script-root.ts +0 -199
  136. package/plugins/sp/scripts/session-timeline.mjs +0 -359
  137. package/plugins/sp/scripts/session-timeline.ts +0 -190
  138. package/plugins/sp/scripts/task-diffstat.mjs +0 -181
  139. package/plugins/sp/scripts/task-diffstat.ts +0 -219
  140. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
  141. package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
  142. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  143. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
  144. package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
  145. package/plugins/sp/scripts/wrapup-steps.ts +0 -891
@@ -1,265 +0,0 @@
1
- /**
2
- * dogfood pipeline-driving detection — the pure detector core.
3
- *
4
- * Extracted from `plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts` (task 1173, plan
5
- * P7/ADR-130): the reusable detection logic lives in lib, and the script keeps only the live
6
- * Phase 1.0 CLI gate. Callers and tests may keep importing the script — it re-exports this module.
7
- *
8
- * Contract (0274 §3 dogfood-pipeline-detect, 0277 R1):
9
- * - Word-boundary matchers with `-` treated as a word char (NOT a boundary).
10
- * A token counts only when it is a distinct hyphen-word, not when it is a
11
- * substring of a longer alphanumeric or hyphen run. This removes the
12
- * leading-space dependency of the @1.1 prose detector.
13
- * - Two token shapes, both with strict `[^\w-]` boundaries:
14
- * (a) complete tokens — `--next`, `dev-run`, `dev-runall`, `dev-wrap`,
15
- * `dev-wrapall`, `dev-idea` — matched as whole hyphen-words so the
16
- * slash form `/sp:dev-run` matches on its `dev-run` tail whether or
17
- * not it is preceded by a space, but `--next` does NOT match inside
18
- * `--next-gen` (the trailing `-gen` breaks the boundary).
19
- * (b) bare nouns — `run`, `runall`, `wrap`, `wrapall`, `idea` — matched
20
- * only as standalone words, so `run` matches in
21
- * `bun ... task run 0042` but NOT inside `runaway`/`prerun`, and
22
- * `idea` does NOT match inside `idealist`/`ideal`.
23
- * - Listing both shapes is what makes the detector leading-space invariant
24
- * (the slash form is caught by its `dev-*` tail; the bare noun is caught
25
- * when it appears as its own word) without letting `run` leak into every
26
- * `-run-` identifier.
27
- *
28
- * Non-goals of detectPipelineDriving: decides ambiguity only. It does NOT decide
29
- * observe vs fix — any explicit `--max-retry` value proceeds regardless.
30
- * Implement-heavy advisory is a separate helper (detectImplementHeavy).
31
- */
32
-
33
- /**
34
- * Tokens whose presence makes a testee pipeline-driving. Ordered for stable
35
- * diagnostics. Two shapes per the contract above: complete `dev-*` / `--next`
36
- * tokens first, then bare nouns.
37
- */
38
- const PIPELINE_TOKENS = [
39
- '--next',
40
- 'dev-runall',
41
- 'dev-wrapall',
42
- 'dev-refineall',
43
- 'dev-verifyall',
44
- 'dev-run',
45
- 'dev-wrap',
46
- 'dev-idea',
47
- 'refineall',
48
- 'verifyall',
49
- 'runall',
50
- 'wrapall',
51
- 'run',
52
- 'wrap',
53
- 'idea',
54
- ] as const;
55
-
56
- /** Refuse message when pipeline-driving and `--max-retry` was not passed (exact string). */
57
- export const PIPELINE_DRIVING_REFUSE_MESSAGE =
58
- '⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)';
59
-
60
- /**
61
- * Refuse message when the testee carries a mutating `--fix` mode (`--fix all` /
62
- * `--fix blockers-first`) and `--max-retry` was not passed. A mutating fix mode
63
- * mutates the working tree independent of pipeline-driving (task 0293 R1/R2):
64
- * `--max-retry 0` bounds the **driver** only — the testee's own `--fix` pass
65
- * still mutates the tree — so the message must not imply otherwise.
66
- */
67
- export const MUTATING_FIX_REFUSE_MESSAGE =
68
- '⚠ mutating --fix mode detected (--fix all | --fix blockers-first); pass --max-retry 0 (observe-only for the driver; the testee still mutates the tree) or --max-retry N (fix mode, driver + testee both mutate)';
69
-
70
- /** Advisory when pipeline-driving + implement-heavy derived steps (exact string). */
71
- export const IMPLEMENT_HEAVY_ADVISORY_MESSAGE =
72
- '⚠ implement-heavy pipeline dogfood: prefer --max-retry 0 (observe-only) or step-split; operator --max-retry N overrides';
73
-
74
- /**
75
- * Tokens that mark a derived step as implement-heavy (mutates product/code or
76
- * drives a mutating pipeline leg). Deliberately excludes verify/review/unit-only
77
- * surfaces even when they carry `--next`.
78
- */
79
- const IMPLEMENT_HEAVY_TOKENS = [
80
- 'dev-runall',
81
- 'dev-wrapall',
82
- 'dev-run',
83
- 'dev-wrap',
84
- 'dev-idea',
85
- 'runall',
86
- 'wrapall',
87
- // bare `run` / `wrap` / `idea` — only when not clearly a non-mutating surface
88
- 'run',
89
- 'wrap',
90
- 'idea',
91
- 'implement',
92
- ] as const;
93
-
94
- /**
95
- * A verify/review surface stops being non-mutating the moment it carries a
96
- * repair mode: `--fix all` / `--fix blockers-first` applies Edit/Write repairs
97
- * to the working tree (0280 dogfood, finding P2). `--fix none` stays
98
- * observational. Boundary-guarded so `--prefix all` / `--focus all` never match.
99
- * Exported so the Phase 1.0 gate and tests share one matcher (task 0293 R1/R4).
100
- */
101
- export function hasMutatingFixMode(step: string): boolean {
102
- return /(?<![\w-])--fix[=\s]+(all|blockers-first)(?![\w-])/i.test(step);
103
- }
104
-
105
- /**
106
- * Match a token at a word boundary. A "word boundary" here is the position
107
- * between a non-`[\w-]` char (or string start) and the token, and between the
108
- * token and a non-`[\w-]` char (or string end). Treating `-` as a word char
109
- * is what makes `--next` reject `--next-gen` and `dev-run` reject `dev-runner`.
110
- */
111
- function tokenMatches(testee: string, token: string): boolean {
112
- const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
- // (?<![\w-]) — not preceded by a word char or hyphen.
114
- // (?![\w-]) — not followed by a word char or hyphen.
115
- const re = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, 'i');
116
- return re.test(testee);
117
- }
118
-
119
- /**
120
- * Returns whether the testee string is pipeline-driving (contains any of the
121
- * pipeline-driving tokens as a distinct hyphen-word). Machine-checked
122
- * counterpart of the prose list in SKILL.md §Pipeline-driving detection and
123
- * dev-dogfood.md.
124
- */
125
- export function detectPipelineDriving(testee: string): boolean {
126
- if (typeof testee !== 'string' || testee.length === 0) {
127
- return false;
128
- }
129
- return PIPELINE_TOKENS.some((token) => tokenMatches(testee, token));
130
- }
131
-
132
- /**
133
- * True when a single step label (or the whole testee) is implement-heavy —
134
- * it chains into real implementation / wrap / idea work, not verify-only.
135
- *
136
- * Non-mutating surfaces (`dev-verify`, `dev-review`, `dev-unit`, plain
137
- * `dev-refine` without a further run) are never implement-heavy even if
138
- * pipeline-driving via `--next` — UNLESS they carry a mutating repair mode
139
- * (`--fix all` / `--fix blockers-first`), which turns the verify/review leg
140
- * into a tree-mutating fix pass (0280 dogfood, finding P2).
141
- */
142
- export function isImplementHeavyStep(step: string): boolean {
143
- if (typeof step !== 'string' || step.length === 0) return false;
144
- // Explicit non-mutating surfaces win unless a mutating token co-occurs.
145
- const nonMutatingOnly =
146
- tokenMatches(step, 'dev-verify') || tokenMatches(step, 'dev-review') || tokenMatches(step, 'dev-unit');
147
- const hasMutating = IMPLEMENT_HEAVY_TOKENS.some((token) => tokenMatches(step, token)) || hasMutatingFixMode(step);
148
- if (nonMutatingOnly && !hasMutating) return false;
149
- // refine alone is planning, not implement-heavy; refine+run/--next chain is.
150
- if (tokenMatches(step, 'dev-refine') && !hasMutating && !tokenMatches(step, '--next')) {
151
- return false;
152
- }
153
- if (tokenMatches(step, 'dev-refine') && tokenMatches(step, '--next')) {
154
- // refine --next chains into run → implement-heavy
155
- return true;
156
- }
157
- return hasMutating;
158
- }
159
-
160
- /**
161
- * True when the testee is implement-heavy: it carries a pipeline-driving token,
162
- * a mutating `--fix` mode, OR at least one derived step (or the testee itself
163
- * when no steps given) is implement-heavy. Used for the W8 Phase 1 advisory
164
- * after step derivation.
165
- *
166
- * Task 0293 R3: a mutating `--fix` mode alone is implement-heavy even with no
167
- * pipeline token — the verify/review leg itself becomes a tree-mutating fix
168
- * pass.
169
- */
170
- export function detectImplementHeavy(testee: string, derivedSteps: string[] = []): boolean {
171
- if (isImplementHeavyStep(testee)) return true;
172
- if (!detectPipelineDriving(testee)) return false;
173
- return derivedSteps.some((step) => isImplementHeavyStep(step));
174
- }
175
-
176
- export interface GateResult {
177
- pipelineDriving: boolean;
178
- mutatingFix: boolean;
179
- maxRetryPresent: boolean;
180
- implementHeavy: boolean;
181
- refuse: boolean;
182
- advisory: boolean;
183
- message: string | null;
184
- exitCode: 0 | 1 | 2;
185
- }
186
-
187
- /**
188
- * Phase 1.0 + W8 gate decision. Pure — no I/O. The CLI wrapper prints and exits.
189
- *
190
- * Refuse condition (task 0293 R1) is the union of two **independent** mutation
191
- * sources, each gated on `!maxRetryPresent`:
192
- *
193
- * 1. pipeline-driving testee (`--next`, `dev-run`, …) — refuses because the
194
- * testee chains lifecycle legs whose cumulative blast radius the driver
195
- * cannot pre-attribute.
196
- * 2. mutating `--fix` mode (`--fix all` / `--fix blockers-first`) — refuses
197
- * because the testee itself applies Edit/Write repairs even with no
198
- * pipeline token. `--max-retry 0` bounds the **driver** only; the testee
199
- * still mutates (honesty note in MUTATING_FIX_REFUSE_MESSAGE).
200
- *
201
- * Pipeline-driving is reported first when both co-occur (its refuse message is
202
- * the superset — chain + tree mutation).
203
- */
204
- export function evaluateDogfoodGate(
205
- testee: string,
206
- options: { maxRetryPresent?: boolean; steps?: string[] } = {},
207
- ): GateResult {
208
- const maxRetryPresent = options.maxRetryPresent === true;
209
- const steps = options.steps ?? [];
210
- const pipelineDriving = detectPipelineDriving(testee);
211
- const mutatingFix = hasMutatingFixMode(testee);
212
- const implementHeavy = detectImplementHeavy(testee, steps);
213
-
214
- if (pipelineDriving && !maxRetryPresent) {
215
- return {
216
- pipelineDriving,
217
- mutatingFix,
218
- maxRetryPresent,
219
- implementHeavy,
220
- refuse: true,
221
- advisory: false,
222
- message: PIPELINE_DRIVING_REFUSE_MESSAGE,
223
- exitCode: 2,
224
- };
225
- }
226
-
227
- if (mutatingFix && !maxRetryPresent) {
228
- return {
229
- pipelineDriving,
230
- mutatingFix,
231
- maxRetryPresent,
232
- implementHeavy,
233
- refuse: true,
234
- advisory: false,
235
- message: MUTATING_FIX_REFUSE_MESSAGE,
236
- exitCode: 2,
237
- };
238
- }
239
-
240
- if (implementHeavy) {
241
- return {
242
- pipelineDriving,
243
- mutatingFix,
244
- maxRetryPresent,
245
- implementHeavy,
246
- refuse: false,
247
- advisory: true,
248
- message: IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
249
- exitCode: 0,
250
- };
251
- }
252
-
253
- return {
254
- pipelineDriving,
255
- mutatingFix,
256
- maxRetryPresent,
257
- implementHeavy,
258
- refuse: false,
259
- advisory: false,
260
- message: null,
261
- exitCode: 0,
262
- };
263
- }
264
-
265
- export { PIPELINE_TOKENS };
@@ -1,278 +0,0 @@
1
- /**
2
- * dogfood report contract — the pure `validateReport(markdown)` core.
3
- *
4
- * Extracted from `plugins/sp/scripts/dogfood-testing/validate-report.ts` (task 1173, plan P7/ADR-130):
5
- * the reusable validation logic lives in lib, and the script keeps only the CLI entry that opens and
6
- * prints. Callers and tests may keep importing the script — it re-exports this module.
7
- *
8
- * Pure function over a report markdown string → { ok, errors[] } with stable error
9
- * codes, callable from tests and by agents finalizing a run. It validates the
10
- * *complete-report* shape: the six unique section headings, Issues subheads, the
11
- * mandatory summary footer with both delivery paths, frontmatter protocol string,
12
- * ledger↔declared-steps cardinality, and the Cost evidence contract (task 0913):
13
- * a complete report must carry a Cost block (Ledger estimate / Method / Meter),
14
- * percentages within 0..100, observable token totals consistent with the ledger
15
- * row sums (fresh + cached), and no fabricated share when no observable rows
16
- * exist. Unknown cells are never folded into zero; all-unknown evidence must
17
- * render `n/a`. Aborted/partial reports remain out of scope — they legitimately
18
- * lack the footer and Steps line.
19
- */
20
-
21
- export interface ReportValidation {
22
- ok: boolean;
23
- errors: string[];
24
- }
25
-
26
- const REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6] as const;
27
- const CANONICAL_PROTOCOL = 'sp:dogfood-testing@1.2';
28
-
29
- function countSectionHeadings(markdown: string, section: number): number {
30
- const re = new RegExp(`^### ${section}\\.`, 'gm');
31
- return (markdown.match(re) ?? []).length;
32
- }
33
-
34
- function countLedgerDataRows(markdown: string): number | null {
35
- const heading = markdown.match(/^### 3\. Monitor Ledger\s*$/m);
36
- if (!heading || heading.index === undefined) return null;
37
- const after = markdown.slice(heading.index + heading[0].length);
38
- const nextHeading = after.search(/^### /m);
39
- const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
40
- const rows = body
41
- .split('\n')
42
- .filter((line) => line.trim().startsWith('|'))
43
- .filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim()))
44
- // drift:external rows are documentary (task 0296) — included in the table but
45
- // subtracted from the executed-step count, per the @1.2 cardinality contract;
46
- // the prescribed code-span form (`drift:external`) matches too (task 0701 R5b).
47
- .filter((line) => !/^\|\s*`?drift:/.test(line.trim()));
48
- // Minus the header row; what remains are data rows.
49
- return Math.max(rows.length - 1, 0);
50
- }
51
-
52
- function declaredExecutedSteps(markdown: string): number | null {
53
- const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/)?.[1];
54
- return match ? Number.parseInt(match, 10) : null;
55
- }
56
-
57
- // ── Cost evidence contract (task 0913, R2) ──────────────────────────────────
58
-
59
- const UNKNOWN_CELLS = new Set(['~unknown', 'unknown', '—', '-', '?', 'n/a', '~n/a']);
60
-
61
- /** Display-rounding tolerance: a whole-percent share may differ from the exact
62
- * value by at most 1 point (e.g. 700/2100 = 33.3% may display as 33%). */
63
- const CACHE_SHARE_TOLERANCE = 1;
64
-
65
- function sectionBody(markdown: string, headingRe: RegExp): string | null {
66
- const heading = markdown.match(headingRe);
67
- if (!heading || heading.index === undefined) return null;
68
- const after = markdown.slice(heading.index + heading[0].length);
69
- const nextHeading = after.search(/^###? /m);
70
- return nextHeading === -1 ? after : after.slice(0, nextHeading);
71
- }
72
-
73
- interface CostTotals {
74
- total: number | 'n/a' | null;
75
- cached: number | 'n/a' | null;
76
- pct: number | 'n/a' | null;
77
- }
78
-
79
- function parseTokensLine(line: string): CostTotals | null {
80
- const match = line.match(
81
- /~(?<total>n\/a|\d+)\s+total\s*\|\s*~(?<cached>n\/a|\d+)\s+cached(?:\s*\(\s*~?(?<pct>n\/a|\d+(?:\.\d+)?)%\s*hit rate\))?/i,
82
- );
83
- if (!match) return null;
84
- const num = (v: string | undefined): number | 'n/a' | null =>
85
- v === undefined ? null : v === 'n/a' ? 'n/a' : Number.parseInt(v, 10);
86
- return { total: num(match.groups?.total), cached: num(match.groups?.cached), pct: num(match.groups?.pct) };
87
- }
88
-
89
- interface LedgerCostRow {
90
- step: string;
91
- fresh: number | null;
92
- cached: number | null;
93
- cachePct: number | null;
94
- unknownCell: boolean;
95
- }
96
-
97
- function parseTokenCell(cell: string): number | null {
98
- const t = cell.trim();
99
- if (UNKNOWN_CELLS.has(t)) return null;
100
- const m = t.match(/~?(\d+)/)?.[1];
101
- return m ? Number.parseInt(m, 10) : null;
102
- }
103
-
104
- function parsePctCell(cell: string): number | null {
105
- const t = cell.trim();
106
- if (UNKNOWN_CELLS.has(t)) return null;
107
- const m = t.match(/(\d+(?:\.\d+)?)\s*%/)?.[1];
108
- return m ? Number.parseFloat(m) : null;
109
- }
110
-
111
- function ledgerCostRows(markdown: string): LedgerCostRow[] {
112
- const body = sectionBody(markdown, /^### 3\. Monitor Ledger\s*$/m);
113
- if (body === null) return [];
114
- return body
115
- .split('\n')
116
- .filter((line) => line.trim().startsWith('|'))
117
- .filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim()))
118
- .filter((line) => !/^\|\s*`?drift:/.test(line.trim()))
119
- .slice(1) // drop header row
120
- .map((line) => {
121
- const cells = line
122
- .split('|')
123
- .map((c) => c.trim())
124
- .filter((c, i, a) => !(i === 0 && c === '') && !(i === a.length - 1 && c === ''));
125
- // Columns: 0 Step … 5 Fresh, 6 Cached, 7 Cache %
126
- const freshCell = cells[5] ?? '';
127
- const cachedCell = cells[6] ?? '';
128
- return {
129
- step: cells[0] ?? '',
130
- fresh: parseTokenCell(freshCell),
131
- cached: parseTokenCell(cachedCell),
132
- cachePct: parsePctCell(cells[7] ?? ''),
133
- unknownCell: UNKNOWN_CELLS.has(freshCell) || UNKNOWN_CELLS.has(cachedCell),
134
- };
135
- });
136
- }
137
-
138
- function validateCostEvidence(markdown: string, errors: string[]): void {
139
- const costBlock = sectionBody(markdown, /^#### Cost\s*$/m);
140
- if (costBlock === null) {
141
- errors.push('missing_cost_block');
142
- return;
143
- }
144
- for (const field of ['Ledger estimate', 'Method', 'Meter']) {
145
- if (!costBlock.includes(`**${field}:**`)) errors.push(`missing_cost_field:${field}`);
146
- }
147
-
148
- const ledgerLine = costBlock.split('\n').find((l) => l.includes('**Ledger estimate:**')) ?? '';
149
- const costTotals = parseTokensLine(ledgerLine);
150
- // A label present but unparseable is its own defect (task 0913 review P3-1): report it and
151
- // keep validating — an early return here disabled every downstream cost check, including
152
- // the per-row impossible-percentage scan. An absent label is already reported above as
153
- // missing_cost_field; with no line to parse there is nothing further to check for it.
154
- if (ledgerLine !== '' && costTotals === null) errors.push('malformed_cost_line');
155
- const footerLine = markdown.split('\n').find((l) => l.trim().startsWith('Tokens:'));
156
- const footerTotals = footerLine ? parseTokensLine(footerLine) : null;
157
- // Same contract for the footer: a present-but-unparseable Tokens: line must not
158
- // silently skip the footer totals comparison. An absent Tokens: line with the
159
- // summary block present is its own defect (task 0913 verify advisory).
160
- if (footerLine !== undefined && footerTotals === null) errors.push('malformed_footer');
161
- if (footerLine === undefined && markdown.includes('── Dogfood Summary ──')) {
162
- errors.push('missing_footer_tokens');
163
- }
164
-
165
- // Percentages must be within 0..100 wherever cost evidence renders one.
166
- const pctScopes = [costBlock, footerLine ?? ''];
167
- for (const row of ledgerCostRows(markdown)) {
168
- if (row.cachePct !== null) pctScopes.push(`${row.cachePct}%`);
169
- }
170
- for (const scope of pctScopes) {
171
- for (const m of scope.matchAll(/(\d+(?:\.\d+)?)\s*%/g)) {
172
- const pct = Number.parseFloat(m[1] ?? '');
173
- if (pct < 0 || pct > 100) errors.push(`impossible_percentage:${m[0].trim()}`);
174
- }
175
- }
176
-
177
- const rows = ledgerCostRows(markdown);
178
-
179
- // Unknown cells are never folded into a numeric share: a row whose basis is
180
- // unknown must carry `—`, never a computed percent.
181
- for (const row of rows) {
182
- if (row.unknownCell && row.cachePct !== null) errors.push('unknown_row_with_numeric_cache');
183
- }
184
-
185
- // Per-row arithmetic: a numeric Cache % must match the row's own fresh/cached cells
186
- // (±1pt display rounding), not merely the aggregate share (task 0913 verify advisory).
187
- for (const row of rows) {
188
- if (row.fresh === null || row.cached === null || row.cachePct === null) continue;
189
- const basis = row.fresh + row.cached;
190
- if (basis === 0) continue;
191
- const expected = Math.round((row.cached / basis) * 100);
192
- if (Math.abs(row.cachePct - expected) > CACHE_SHARE_TOLERANCE) {
193
- errors.push(`cache_pct_mismatch:${row.step}_expected_${expected}`);
194
- }
195
- }
196
-
197
- const observable = rows.filter((r) => r.fresh !== null && r.cached !== null);
198
- const sumFresh = observable.reduce((acc, r) => acc + (r.fresh ?? 0), 0);
199
- const sumCached = observable.reduce((acc, r) => acc + (r.cached ?? 0), 0);
200
-
201
- const checkTotals = (label: string, totals: CostTotals | null): void => {
202
- if (totals === null) return;
203
- if (observable.length > 0) {
204
- const expectedTotal = sumFresh + sumCached;
205
- if (typeof totals.total === 'number' && totals.total !== expectedTotal) {
206
- errors.push(`cost_total_mismatch:${label}_expected_${expectedTotal}`);
207
- }
208
- if (typeof totals.cached === 'number' && totals.cached !== sumCached) {
209
- errors.push(`cost_total_mismatch:${label}_cached_expected_${sumCached}`);
210
- }
211
- const expectedPct = expectedTotal > 0 ? Math.round((sumCached / expectedTotal) * 100) : null;
212
- if (expectedPct !== null) {
213
- if (totals.pct === 'n/a' || totals.pct === null) {
214
- errors.push(`missing_cache_share:${label}`);
215
- } else if (
216
- typeof totals.pct === 'number' &&
217
- Math.abs(totals.pct - expectedPct) > CACHE_SHARE_TOLERANCE
218
- ) {
219
- errors.push(`cache_share_mismatch:${label}_expected_${expectedPct}`);
220
- }
221
- }
222
- } else if (
223
- typeof totals.total === 'number' ||
224
- typeof totals.cached === 'number' ||
225
- typeof totals.pct === 'number'
226
- ) {
227
- // No observable denominator: numeric totals/shares fabricate evidence
228
- // from unknown rows (unknown folded into zero). `n/a` is the honest form.
229
- errors.push(`fabricated_cache_share:${label}`);
230
- }
231
- };
232
- checkTotals('cost', costTotals);
233
- checkTotals('footer', footerTotals);
234
- }
235
-
236
- export function validateReport(markdown: string): ReportValidation {
237
- const errors: string[] = [];
238
-
239
- // Footer (W2/D1) — the mirrored footer block at the report end.
240
- if (!markdown.includes('── Dogfood Summary ──')) errors.push('missing_footer');
241
- if (!markdown.includes('[Live:')) errors.push('missing_live_path');
242
- if (!markdown.includes('[Report:')) errors.push('missing_report_path');
243
-
244
- // Six unique section headings (W2/D3).
245
- for (const section of REQUIRED_SECTIONS) {
246
- const count = countSectionHeadings(markdown, section);
247
- if (count === 0) errors.push(`missing_section:${section}`);
248
- if (count > 1) errors.push(`duplicate_section:${section}`);
249
- }
250
-
251
- // Issues subheads (W2/D3).
252
- if (!markdown.includes('#### Fixed') || !markdown.includes('#### Unresolved')) {
253
- errors.push('missing_issues_subheads');
254
- }
255
-
256
- // Protocol string (W1/D5) — colon form, exact version.
257
- const protocolMatch = markdown.match(/^protocol:\s*(\S+)\s*$/m);
258
- if (!protocolMatch || protocolMatch[1] !== CANONICAL_PROTOCOL) {
259
- errors.push('protocol_string');
260
- }
261
-
262
- // Ledger cardinality (W3/D4) — data rows == declared executed steps.
263
- const executed = declaredExecutedSteps(markdown);
264
- if (executed === null) {
265
- errors.push('missing_steps_declared');
266
- } else {
267
- const rows = countLedgerDataRows(markdown);
268
- if (rows === null || rows !== executed) {
269
- errors.push('ledger_cardinality');
270
- }
271
- }
272
-
273
- // Cost evidence (task 0913, R2) — required block, arithmetic consistency,
274
- // honest unknowns.
275
- validateCostEvidence(markdown, errors);
276
-
277
- return { ok: errors.length === 0, errors };
278
- }
@@ -1,112 +0,0 @@
1
- /**
2
- * GENERATED by scripts/commands/bundle-plugin-lib.ts — do not edit.
3
- * Source of truth: packages/app/src/services/history-anatomy.ts (task 1005 R2).
4
- */
5
- export declare interface CacheIdentity {
6
- contractVersion: string;
7
- mode: 'daily' | 'ad-hoc';
8
- date: string;
9
- timezone: string;
10
- bounds: { since: string; until: string };
11
- sources: string[];
12
- }
13
- export declare interface CacheProvenance {
14
- identity: CacheIdentity;
15
- windowState: 'provisional' | 'closed';
16
- generatedAt: string;
17
- validatedAt: string;
18
- artifactDigest: string;
19
- baselineArtifactDigest: string | null;
20
- contractDigest: string;
21
- skillDigest: string;
22
- workflowDigest: string;
23
- helperDigest: string;
24
- coverage: Array<{ source: string; status: string; lastImportedAt: string | null }>;
25
- runId?: string;
26
- currentArtifactPath?: string;
27
- baselineArtifactPath?: string | null;
28
- spurVersion?: string;
29
- schemaVersion?: number;
30
- executor?: string;
31
- model?: string;
32
- cacheDisposition?: CacheDisposition;
33
- }
34
- export declare type CacheDisposition = 'hit' | 'miss' | 'forced-recompute';
35
- export declare interface CacheDecision {
36
- disposition: CacheDisposition;
37
- reasons: string[];
38
- }
39
- export declare const REPORT_SECTIONS: string[];
40
- export declare const FINDING_FIELDS: string[];
41
- export declare function semanticArtifactDigest(artifactJson: unknown): string;
42
- export declare function parseProvenance(reportMarkdown: string): CacheProvenance | null;
43
- export declare function decideCache(
44
- cached: CacheProvenance | null,
45
- current: CacheProvenance,
46
- opts: { recompute: boolean; dayClosed: boolean },
47
- ): CacheDecision;
48
- export declare function checkReportStructure(reportMarkdown: string): { ok: boolean; problems: string[] };
49
- export declare function logicDigest(path: string | undefined): string;
50
- export declare function importedSnapshotAsOf(coverage: CacheProvenance['coverage']): string;
51
- export declare function resolvePaths(opts: {
52
- helper: string;
53
- reportDir: string;
54
- date?: string;
55
- output?: string;
56
- now?: Date;
57
- tz?: string;
58
- mode?: string;
59
- since?: string;
60
- until?: string;
61
- }): string;
62
- export declare type SelectorValidation =
63
- | {
64
- ok: true;
65
- mode: 'daily' | 'ad-hoc';
66
- date: string | null;
67
- focus: string | null;
68
- since: string | null;
69
- until: string | null;
70
- }
71
- | { ok: false; errors: string[] };
72
- export declare function validateSelector(opts: {
73
- mode?: string;
74
- date?: string;
75
- since?: string;
76
- until?: string;
77
- focus?: string;
78
- recompute?: string;
79
- output?: string;
80
- }): SelectorValidation;
81
- export declare interface ProbeOptions {
82
- artifact: string;
83
- target: string;
84
- baseline?: string;
85
- mode: 'daily' | 'ad-hoc';
86
- date?: string;
87
- recompute: boolean;
88
- executor?: string;
89
- model?: string;
90
- skillDir?: string;
91
- contractFile?: string;
92
- workflowFile?: string;
93
- helperFile?: string;
94
- contractVersion?: string;
95
- runId?: string;
96
- spurVersion?: string;
97
- now?: Date;
98
- }
99
- export declare function buildProvenance(opts: ProbeOptions): CacheProvenance;
100
- export declare function probe(opts: ProbeOptions): { decision: CacheDecision; current: CacheProvenance };
101
- export declare function renderProvenanceFrontmatter(p: CacheProvenance): string;
102
- export declare function bannerLine(p: CacheProvenance): string;
103
- export declare function stampReport(candidateMarkdown: string, p: CacheProvenance): string;
104
- export declare function refreshReport(publishedMarkdown: string, validatedAt: string, disposition: CacheDisposition): string;
105
- export declare function publishAtomically(candidatePath: string, targetPath: string): void;
106
- export declare interface CacheCliResult {
107
- exitCode: number;
108
- stdout: string;
109
- stderr: string;
110
- }
111
- export declare function porcelainPaths(text: string): Set<string>;
112
- export declare function diffPorcelain(before: string, now: string, expects: Set<string>): string[];