@gobing-ai/spur 0.4.2 → 0.4.3
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/decisions/maker-liveness.yaml +39 -0
- package/config/plugin-scripts.json +0 -4
- package/config/rules/README.md +1 -0
- package/config/rules/docs/sourced-api-claims.yaml +56 -12
- package/config/rules/strict/runtime-boundaries.yaml +7 -0
- package/config/script-placement-baseline.json +1 -19
- package/config/workflows/feature-lifecycle.yaml +46 -1
- package/config/workflows/feature-verification.yaml +11 -0
- package/package.json +9 -9
- package/plugins/sp/README.md +22 -16
- package/plugins/sp/agents/super-reviewer.md +43 -0
- package/plugins/sp/bunfig.toml +8 -5
- package/plugins/sp/hooks/careful-guard.ts +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +7 -427
- package/plugins/sp/hooks/context-session-start.ts +5 -213
- package/plugins/sp/hooks/context-session-stop.ts +5 -191
- package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
- package/plugins/sp/hooks/task-write-guard.ts +1 -1
- package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
- package/plugins/sp/lib/context-post-tool.ts +432 -0
- package/plugins/sp/lib/context-session-start.ts +214 -0
- package/plugins/sp/lib/context-session-stop.ts +196 -0
- package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
- package/plugins/sp/lib/dogfood-report.ts +278 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +3 -3
- package/plugins/sp/lib/inline-run-app.ts +40 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
- package/plugins/sp/lib/inline-run.generated.mjs +1347 -90
- package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +591 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +38 -25
- package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
- package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
- package/plugins/sp/lib/run-summary-core.ts +34 -370
- package/plugins/sp/lib/transcript.ts +37 -228
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +586 -0
- package/plugins/sp/scripts/batch-preflight.mjs +23 -1
- package/plugins/sp/scripts/batch-preflight.ts +35 -295
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
- package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
- package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
- package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
- package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
- package/plugins/sp/scripts/inline-run-setup.mjs +149 -122
- package/plugins/sp/scripts/inline-run-setup.ts +1 -21
- package/plugins/sp/scripts/quality-gate.mjs +14 -2
- package/plugins/sp/scripts/residual-scan.mjs +73 -6
- package/plugins/sp/scripts/residual-scan.ts +8 -5
- package/plugins/sp/scripts/run-summary.mjs +151 -97
- package/plugins/sp/scripts/run-summary.ts +7 -5
- package/plugins/sp/scripts/session-timeline.mjs +46 -19
- package/plugins/sp/scripts/task-diffstat.mjs +11 -1
- package/plugins/sp/scripts/task-diffstat.ts +8 -39
- package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
- package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
- package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
- package/plugins/sp/skills/next-feature/SKILL.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
- package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
- package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
- package/plugins/sp/skills/spur-cli/references/features.md +46 -5
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +13 -3
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -12
- package/spur.js +80681 -79296
- package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.5IIl2O-z.js} +1 -1
- package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.Drnx-KeM.js} +150 -150
- package/web/_astro/{TaskDetail.Cxov1_N_.js → TaskDetail.Ci4LNn9S.js} +1 -1
- package/web/_astro/{arc.vaT7tPOB.js → arc.j2EVkYVd.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
- package/web/_astro/channel.DmdDxGsq.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
- package/web/_astro/index.Do2lLdgo.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
- package/web/_astro/{linear.BzGnBwNL.js → linear.CI6djNi7.js} +1 -1
- package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CuQPxS8b.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
- package/web/index.html +2 -2
- package/config/proportional-route-table.ts +0 -155
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/web/_astro/channel.D4nG9YLf.js +0 -1
- package/web/_astro/index.BhSwN8yY.css +0 -1
- /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
- /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
- /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
|
@@ -1,283 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* validate-report — @1.2 dogfood report contract checker (task 0276,
|
|
2
|
+
* validate-report — CLI entry for the @1.2 dogfood report contract checker (task 0276 W6, 0278 R6).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* mandatory summary footer with both delivery paths, frontmatter protocol string,
|
|
8
|
-
* ledger↔declared-steps cardinality, and the Cost evidence contract (task 0913):
|
|
9
|
-
* a complete report must carry a Cost block (Ledger estimate / Method / Meter),
|
|
10
|
-
* percentages within 0..100, observable token totals consistent with the ledger
|
|
11
|
-
* row sums (fresh + cached), and no fabricated share when no observable rows
|
|
12
|
-
* exist. Unknown cells are never folded into zero; all-unknown evidence must
|
|
13
|
-
* render `n/a`. Aborted/partial reports remain out of scope — they legitimately
|
|
14
|
-
* lack the footer and Steps line.
|
|
4
|
+
* The validation logic is application-owned in `plugins/sp/lib/dogfood-report.ts` (task 1173); this
|
|
5
|
+
* script opens the file, runs the check and prints — argv, usage, exit codes and every exported
|
|
6
|
+
* symbol stay as they were, so callers and tests keep importing this path.
|
|
15
7
|
*
|
|
16
8
|
* CLI (task 0278 R6): `bun …/validate-report.ts --file <report.md> [--json]`
|
|
17
9
|
*/
|
|
18
10
|
|
|
19
11
|
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { validateReport } from '../../lib/dogfood-report';
|
|
20
13
|
|
|
21
|
-
export
|
|
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
|
-
}
|
|
279
|
-
|
|
280
|
-
// ── CLI entry (Phase 4 self-validate — task 0278 R6) ─────────────────────────
|
|
14
|
+
export * from '../../lib/dogfood-report';
|
|
281
15
|
|
|
282
16
|
export interface ValidateCliArgs {
|
|
283
17
|
file: string | null;
|
|
@@ -2,20 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/feature-verification-steps.ts
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
closeSync,
|
|
8
|
-
existsSync,
|
|
9
|
-
mkdirSync,
|
|
10
|
-
openSync,
|
|
11
|
-
readdirSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
realpathSync,
|
|
14
|
-
renameSync,
|
|
15
|
-
statSync,
|
|
16
|
-
writeFileSync
|
|
17
|
-
} from "fs";
|
|
18
|
-
import { join } from "path";
|
|
5
|
+
import { existsSync } from "fs";
|
|
19
6
|
import { fileURLToPath } from "url";
|
|
20
7
|
|
|
21
8
|
// plugins/sp/lib/env.ts
|
|
@@ -25,12 +12,11 @@ function getEnvVar(name, fallback) {
|
|
|
25
12
|
}
|
|
26
13
|
|
|
27
14
|
// plugins/sp/scripts/feature-verification-steps.ts
|
|
28
|
-
var SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
29
15
|
function bundleEntry() {
|
|
30
16
|
return fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
31
17
|
}
|
|
32
18
|
function sourceModeError(spurBin) {
|
|
33
|
-
return new Error(`source mode: refusing to load packages/app/src/index.ts (spurBin=${JSON.stringify(spurBin)}). ` + "That entry is not the feature-verification contract surface \u2014 it does not export " + "
|
|
19
|
+
return new Error(`source mode: refusing to load packages/app/src/index.ts (spurBin=${JSON.stringify(spurBin)}). ` + "That entry is not the feature-verification contract surface \u2014 it does not export " + "runFeatureVerification. Fix: run in bundle mode so " + "plugins/sp/lib/inline-run.generated.mjs is loaded (omit --module-mode, or pass --module-mode bundle) " + "and regenerate that bundle if it is missing. Reinstalling the sp plugin does not add the operation to the app source entry.");
|
|
34
20
|
}
|
|
35
21
|
function resolveModuleMode(raw) {
|
|
36
22
|
if (raw === "" || raw === "bundle")
|
|
@@ -50,123 +36,21 @@ async function loadModule(spurBin, mode) {
|
|
|
50
36
|
}
|
|
51
37
|
async function requireModule(entry, mode) {
|
|
52
38
|
const mod = await import(entry);
|
|
53
|
-
const missing = [
|
|
54
|
-
"startFeatureVerificationReceipt",
|
|
55
|
-
"completeFeatureVerificationReceipt",
|
|
56
|
-
"captureFeatureReceiptDigest",
|
|
57
|
-
"resolveWorkflowDefinition",
|
|
58
|
-
"splitLaunchCommand",
|
|
59
|
-
"openInlineRunProjectDb",
|
|
60
|
-
"ArtifactDao"
|
|
61
|
-
].filter((k) => typeof mod[k] !== "function");
|
|
39
|
+
const missing = ["runFeatureVerification"].filter((k) => typeof mod[k] !== "function");
|
|
62
40
|
if (missing.length > 0) {
|
|
63
41
|
throw new Error(`feature-verification-steps: mode=${mode} \u2014 application entry ${entry} is missing feature-verification seams (${missing.join(", ")}). ` + "Fix: the generated inline bundle is stale or incomplete \u2014 run `bun run build:bundle` in a source checkout, or reinstall the sp plugin.");
|
|
64
42
|
}
|
|
65
43
|
return mod;
|
|
66
44
|
}
|
|
67
|
-
function assertSafeId(kind, id) {
|
|
68
|
-
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
69
|
-
throw new Error(`refusing unsafe ${kind}: ${id}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function findFeatureFile(featureDir, featureId) {
|
|
73
|
-
if (!existsSync(featureDir))
|
|
74
|
-
return;
|
|
75
|
-
const name = readdirSync(featureDir).find((n) => n.startsWith(`${featureId}_`) && n.endsWith(".md"));
|
|
76
|
-
return name === undefined ? undefined : join(featureDir, name);
|
|
77
|
-
}
|
|
78
|
-
var nodeFsShim = {
|
|
79
|
-
realPath: realpathSync,
|
|
80
|
-
readDir: readdirSync,
|
|
81
|
-
stat: (path) => {
|
|
82
|
-
try {
|
|
83
|
-
return statSync(path);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (error.code === "ENOENT")
|
|
86
|
-
return null;
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
ensureDir: (dir) => {
|
|
91
|
-
mkdirSync(dir, { recursive: true });
|
|
92
|
-
},
|
|
93
|
-
writeFile: (path, body) => {
|
|
94
|
-
writeFileSync(path, body);
|
|
95
|
-
},
|
|
96
|
-
rename: (src, dest) => {
|
|
97
|
-
renameSync(src, dest);
|
|
98
|
-
},
|
|
99
|
-
readFile: (path) => {
|
|
100
|
-
return readFileSync(path, "utf8");
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
45
|
async function verify(featureId, runId, cmdOverride, spurBin, mode) {
|
|
104
|
-
assertSafeId("feature id", featureId);
|
|
105
|
-
assertSafeId("run id", runId);
|
|
106
|
-
const cwd = process.cwd();
|
|
107
46
|
const mod = await loadModule(spurBin, mode);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const selected = await mod.resolveWorkflowDefinition(cwd, "feature-verification");
|
|
111
|
-
const vars = selected.workflow.vars;
|
|
112
|
-
const configuredCmd = typeof vars?.verificationCmd === "string" && vars.verificationCmd.length > 0 ? vars.verificationCmd : "bun run spur-check-feature";
|
|
113
|
-
const effectiveCmd = cmdOverride !== "" ? cmdOverride : configuredCmd;
|
|
114
|
-
const featureFile = findFeatureFile(join(cwd, "docs", "features"), featureId);
|
|
115
|
-
if (featureFile === undefined) {
|
|
116
|
-
throw new Error(`feature ${featureId} not found under ${join(cwd, "docs", "features")}`);
|
|
117
|
-
}
|
|
118
|
-
const featureContent = readFileSync(featureFile, "utf8");
|
|
119
|
-
const learningsPath = join(cwd, ".spur", "context", "learnings.md");
|
|
120
|
-
const learningsContent = existsSync(learningsPath) ? readFileSync(learningsPath, "utf8") : undefined;
|
|
121
|
-
const beforeDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
|
|
122
|
-
const receipt = await mod.startFeatureVerificationReceipt(nodeFsShim, runDir, {
|
|
47
|
+
const result = await mod.runFeatureVerification({
|
|
48
|
+
workdir: process.cwd(),
|
|
123
49
|
featureId,
|
|
124
50
|
runId,
|
|
125
|
-
|
|
126
|
-
verifier: {
|
|
127
|
-
name: "feature-verification",
|
|
128
|
-
sourcePath: selected.path,
|
|
129
|
-
layer: selected.layer,
|
|
130
|
-
definitionDigest: selected.digest
|
|
131
|
-
},
|
|
132
|
-
verificationCmd: effectiveCmd,
|
|
133
|
-
inputDigest: beforeDigest
|
|
51
|
+
cmdOverride
|
|
134
52
|
});
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
await new mod.ArtifactDao(db.adapter).record({
|
|
138
|
-
runId,
|
|
139
|
-
path: mod.featureReceiptPaths(runDir, featureId, runId).runScoped,
|
|
140
|
-
kind: "feature-verification"
|
|
141
|
-
});
|
|
142
|
-
} finally {
|
|
143
|
-
db.close();
|
|
144
|
-
}
|
|
145
|
-
const logPath = join(runDir, `${runId}-feature-verification.log`);
|
|
146
|
-
const launch = mod.splitLaunchCommand(effectiveCmd, "feature-verification verificationCmd");
|
|
147
|
-
if ("error" in launch) {
|
|
148
|
-
await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, {
|
|
149
|
-
status: "FAIL",
|
|
150
|
-
inputDigest: beforeDigest
|
|
151
|
-
});
|
|
152
|
-
console.log(`feature-verification-steps: ${featureId} verification FAIL (${launch.error})`);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const logFd = openSync(logPath, "a");
|
|
156
|
-
try {
|
|
157
|
-
const result = spawnSync(launch.command, launch.leadingArgs, {
|
|
158
|
-
cwd,
|
|
159
|
-
stdio: ["ignore", logFd, logFd],
|
|
160
|
-
timeout: 14400000
|
|
161
|
-
});
|
|
162
|
-
const exit = result.status ?? 1;
|
|
163
|
-
const afterDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
|
|
164
|
-
const status = exit === 0 && afterDigest === beforeDigest ? "PASS" : "FAIL";
|
|
165
|
-
await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, { status, inputDigest: afterDigest });
|
|
166
|
-
console.log(`feature-verification-steps: ${featureId} verification ${status} (run ${runId}, log ${logPath})`);
|
|
167
|
-
} finally {
|
|
168
|
-
closeSync(logFd);
|
|
169
|
-
}
|
|
53
|
+
console.log(`feature-verification-steps: ${featureId} ${result.message}`);
|
|
170
54
|
}
|
|
171
55
|
function main() {
|
|
172
56
|
const args = process.argv.slice(2);
|