@gobing-ai/spur 0.4.2 → 0.4.4

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 (168) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/decisions/maker-liveness.yaml +39 -0
  3. package/config/plugin-scripts.json +0 -4
  4. package/config/rules/README.md +1 -0
  5. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  6. package/config/rules/docs/verdict-confidence.yaml +68 -0
  7. package/config/rules/quality/coverage-gate.yaml +13 -0
  8. package/config/rules/recommended-pre-check.yaml +6 -2
  9. package/config/rules/strict/runtime-boundaries.yaml +10 -0
  10. package/config/rules/surface/check-cli-surface.yaml +1 -3
  11. package/config/script-placement-baseline.json +1 -19
  12. package/config/templates/AGENTS.md +7 -0
  13. package/config/workflows/feature-lifecycle.yaml +46 -1
  14. package/config/workflows/feature-verification.yaml +20 -1
  15. package/config/workflows/task-lifecycle.yaml +50 -0
  16. package/config/workflows/task-pipeline.yaml +15 -9
  17. package/package.json +10 -10
  18. package/plugins/sp/README.md +22 -16
  19. package/plugins/sp/agents/super-reviewer.md +46 -0
  20. package/plugins/sp/bunfig.toml +8 -5
  21. package/plugins/sp/commands/dev-changelog.md +1 -1
  22. package/plugins/sp/commands/dev-gitmsg.md +6 -2
  23. package/plugins/sp/commands/dev-handover.md +1 -0
  24. package/plugins/sp/commands/dev-job-dump.md +2 -1
  25. package/plugins/sp/commands/dev-refine.md +14 -2
  26. package/plugins/sp/commands/dev-refineall.md +7 -1
  27. package/plugins/sp/commands/dev-review.md +1 -0
  28. package/plugins/sp/hooks/careful-guard.ts +1 -1
  29. package/plugins/sp/hooks/context-post-tool.ts +7 -427
  30. package/plugins/sp/hooks/context-session-start.ts +5 -213
  31. package/plugins/sp/hooks/context-session-stop.ts +5 -191
  32. package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
  33. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  34. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  35. package/plugins/sp/lib/context-post-tool.ts +432 -0
  36. package/plugins/sp/lib/context-session-start.ts +214 -0
  37. package/plugins/sp/lib/context-session-stop.ts +196 -0
  38. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  39. package/plugins/sp/lib/dogfood-report.ts +278 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +18 -18
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +91 -91
  42. package/plugins/sp/lib/inline-run-app.ts +40 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  44. package/plugins/sp/lib/inline-run.generated.mjs +1425 -212
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +623 -9
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +1 -0
  48. package/plugins/sp/lib/quality-gate.generated.mjs +13809 -13969
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +20 -1
  50. package/plugins/sp/lib/residual-scan.generated.mjs +120 -25
  51. package/plugins/sp/lib/run-summary-core.ts +34 -370
  52. package/plugins/sp/lib/step-profile.generated.mjs +8 -8
  53. package/plugins/sp/lib/transcript.ts +37 -228
  54. package/plugins/sp/plugin.json +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +619 -11
  56. package/plugins/sp/scripts/batch-preflight.mjs +57 -13
  57. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  58. package/plugins/sp/scripts/commit-guard.mjs +6 -6
  59. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +127 -108
  60. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  61. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +13 -12
  62. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  63. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +7 -4
  64. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  65. package/plugins/sp/scripts/feature-verification-steps.mjs +20 -125
  66. package/plugins/sp/scripts/feature-verification-steps.ts +57 -181
  67. package/plugins/sp/scripts/history-anatomy-cache.mjs +19 -19
  68. package/plugins/sp/scripts/idea-handoff.mjs +2 -2
  69. package/plugins/sp/scripts/inline-run-dispatch.mjs +25 -24
  70. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  71. package/plugins/sp/scripts/inline-run-setup.mjs +164 -137
  72. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  73. package/plugins/sp/scripts/persist-out-check.mjs +5 -5
  74. package/plugins/sp/scripts/pr-reviewing.mjs +13 -13
  75. package/plugins/sp/scripts/quality-gate.mjs +13730 -13887
  76. package/plugins/sp/scripts/residual-scan.mjs +129 -31
  77. package/plugins/sp/scripts/residual-scan.ts +16 -13
  78. package/plugins/sp/scripts/run-summary.mjs +166 -112
  79. package/plugins/sp/scripts/run-summary.ts +7 -5
  80. package/plugins/sp/scripts/script-root.mjs +6 -6
  81. package/plugins/sp/scripts/session-timeline.mjs +62 -35
  82. package/plugins/sp/scripts/task-diffstat.mjs +19 -9
  83. package/plugins/sp/scripts/task-diffstat.ts +8 -39
  84. package/plugins/sp/scripts/workflow-step-profile.mjs +12 -12
  85. package/plugins/sp/scripts/wrapup-drift-probe.mjs +9 -9
  86. package/plugins/sp/scripts/wrapup-steps.mjs +25 -25
  87. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  88. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  89. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  90. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  91. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  92. package/plugins/sp/skills/next-router/SKILL.md +3 -1
  93. package/plugins/sp/skills/next-router/references/routing-table.md +9 -3
  94. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  95. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  96. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  97. package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
  98. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  99. package/plugins/sp/skills/spur-cli/references/tasks.md +24 -4
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +8 -1
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -11
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
  103. package/plugins/sp/skills/spur-dev/references/execution-batch.md +19 -6
  104. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  105. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  106. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +4 -2
  107. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +49 -12
  108. package/schemas/section-matrix.schema.json +3 -2
  109. package/spur.js +66591 -73262
  110. package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.Niubpe6z.js} +1 -1
  111. package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.hF0RAIrg.js} +150 -150
  112. package/web/_astro/TaskDetail.DJpapt38.js +1 -0
  113. package/web/_astro/{arc.vaT7tPOB.js → arc.BgKcK0i8.js} +1 -1
  114. package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.ChP-Ig0V.js} +1 -1
  115. package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.BVaCBoeN.js} +1 -1
  116. package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.CHSDpEO5.js} +1 -1
  117. package/web/_astro/channel.DkpvN2tD.js +1 -0
  118. package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.MYRcYVRa.js} +1 -1
  119. package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.S5j22Zqa.js} +1 -1
  120. package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.gciRPvmZ.js} +1 -1
  121. package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.CdqrSQkX.js} +1 -1
  122. package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.CyokdHT3.js} +1 -1
  123. package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.Bg9KJhjW.js} +1 -1
  124. package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.1gxxI-vD.js} +1 -1
  125. package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.DDEYtCKU.js} +1 -1
  126. package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.Ch3zi37T.js} +1 -1
  127. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.Ch3zi37T.js} +1 -1
  128. package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.D2q5AINI.js} +1 -1
  129. package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.BA9kC2YT.js} +1 -1
  130. package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.CHO08nRp.js} +1 -1
  131. package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.BfFQt0LW.js} +1 -1
  132. package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.CHMwMTe4.js} +1 -1
  133. package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.BJ-7gK-w.js} +1 -1
  134. package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.C5xqcc0t.js} +1 -1
  135. package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.C2pQjfrC.js} +1 -1
  136. package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35._FoNyxgC.js} +1 -1
  137. package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.B_ENkT2I.js} +1 -1
  138. package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.B3q8h77C.js} +1 -1
  139. package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.QWZFDboZ.js} +1 -1
  140. package/web/_astro/index.BFyW13JJ.css +1 -0
  141. package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.Cs6eEc92.js} +1 -1
  142. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.BU9DJMHA.js} +1 -1
  143. package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.D35k6Jpe.js} +1 -1
  144. package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.DBqcAT4m.js} +1 -1
  145. package/web/_astro/{linear.BzGnBwNL.js → linear.D-LSNK8W.js} +1 -1
  146. package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CSruWVOa.js} +4 -4
  147. package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.BiFaUsfj.js} +1 -1
  148. package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.D1KTKqb9.js} +1 -1
  149. package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.DBHflsMG.js} +1 -1
  150. package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.hwX9CNdk.js} +1 -1
  151. package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.DkI8mJiE.js} +1 -1
  152. package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.DMz7jBDx.js} +1 -1
  153. package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.B6Ef2xJ1.js} +1 -1
  154. package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.CgoaufB9.js} +1 -1
  155. package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.B3c8h1t-.js} +1 -1
  156. package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.BdHPAxAb.js} +1 -1
  157. package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.BrJXPPAj.js} +1 -1
  158. package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.CMH5yFsV.js} +1 -1
  159. package/web/index.html +2 -2
  160. package/web/spur_logo.svg +1 -1
  161. package/config/proportional-route-table.ts +0 -155
  162. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  163. package/web/_astro/TaskDetail.Cxov1_N_.js +0 -1
  164. package/web/_astro/channel.D4nG9YLf.js +0 -1
  165. package/web/_astro/index.BhSwN8yY.css +0 -1
  166. /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
  167. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  168. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -0,0 +1,278 @@
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
+ }
@@ -669,23 +669,23 @@ function diffPorcelain(before, now, expects) {
669
669
  return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
670
670
  }
671
671
  export {
672
- validateSelector,
673
- stampReport,
674
- semanticArtifactDigest,
675
- resolvePaths,
676
- renderProvenanceFrontmatter,
677
- refreshReport,
678
- publishAtomically,
679
- probe,
680
- porcelainPaths,
681
- parseProvenance,
682
- logicDigest,
683
- importedSnapshotAsOf,
684
- diffPorcelain,
685
- decideCache,
686
- checkReportStructure,
687
- buildProvenance,
688
- bannerLine,
672
+ FINDING_FIELDS,
689
673
  REPORT_SECTIONS,
690
- FINDING_FIELDS
674
+ bannerLine,
675
+ buildProvenance,
676
+ checkReportStructure,
677
+ decideCache,
678
+ diffPorcelain,
679
+ importedSnapshotAsOf,
680
+ logicDigest,
681
+ parseProvenance,
682
+ porcelainPaths,
683
+ probe,
684
+ publishAtomically,
685
+ refreshReport,
686
+ renderProvenanceFrontmatter,
687
+ resolvePaths,
688
+ semanticArtifactDigest,
689
+ stampReport,
690
+ validateSelector
691
691
  };