@gobing-ai/spur 0.3.90 → 0.3.92
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/pipeline-budgets.json +9 -2
- package/config/plugin-scripts.json +17 -1
- package/config/workflows/decision-routing-example.yaml +134 -0
- package/config/workflows/feature-lifecycle.yaml +14 -5
- package/config/workflows/feature-verification.yaml +35 -27
- package/config/workflows/history-anatomy.yaml +14 -25
- package/config/workflows/idea-pipeline.yaml +17 -5
- package/config/workflows/task-pipeline.yaml +187 -4
- package/config/workflows/wayfinder-resolution.yaml +2 -0
- package/config/workflows/wrapup-pipeline.yaml +49 -5
- package/package.json +9 -9
- package/plugins/sp/README.md +1 -0
- package/plugins/sp/agents/expert-spur.md +2 -2
- package/plugins/sp/agents/super-planner.md +14 -5
- package/plugins/sp/commands/dev-dogfood.md +4 -4
- package/plugins/sp/commands/dev-fixall.md +8 -5
- package/plugins/sp/commands/dev-run.md +6 -0
- package/plugins/sp/commands/dev-runall.md +12 -6
- package/plugins/sp/commands/dev-verify.md +9 -0
- package/plugins/sp/commands/dev-verifyall.md +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +301 -300
- package/plugins/sp/lib/inline-run.generated.d.mts +17 -0
- package/plugins/sp/lib/inline-run.generated.mjs +1460 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/environment-lens.md +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
- package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
- package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
- package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +349 -0
- package/plugins/sp/scripts/inline-run-setup.ts +192 -75
- package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
- package/plugins/sp/scripts/record-feature-sync.ts +84 -0
- package/plugins/sp/scripts/residual-scan.mjs +476 -0
- package/plugins/sp/scripts/residual-scan.ts +614 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +71 -6
- package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
- package/plugins/sp/scripts/task-size-precheck.ts +8 -3
- package/plugins/sp/scripts/validate-flag-contracts.ts +3 -3
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
- package/plugins/sp/skills/code-verification/SKILL.md +21 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
- package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
- package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +12 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +11 -2
- package/plugins/sp/skills/spur-cli/references/features.md +1 -1
- package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
- package/plugins/sp/skills/spur-cli/references/self.md +5 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +42 -20
- package/plugins/sp/skills/spur-dev/SKILL.md +11 -4
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +25 -3
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +285 -57
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -5
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +19 -4
- package/plugins/sp/skills/spur-dev/references/glossary.md +9 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +51 -16
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
- package/spur.js +23422 -19364
- package/web/_astro/BoardApp.CerSBgis.js +192 -0
- package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
- package/web/_astro/{TaskDetail.DwTmbQp5.js → TaskDetail.DCqiC-OZ.js} +1 -1
- package/web/_astro/arc.CPwg6Rw0.js +1 -0
- package/web/_astro/{architectureDiagram-3BPJPVTR.jvdDahWM.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.zSg4AmFD.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BkUIUQWH.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
- package/web/_astro/channel.MYZLKNwy.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.DvfQ_f50.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DuI4gQqX.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.D3BWBOpF.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.3QSi0a9M.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.xazCQrAF.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.B2g6u4rA.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.wWwWs99t.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BD5g3qa9.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.C7CzCdsX.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.C7CzCdsX.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.Xyiau0gw.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BeC5MWas.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.yZbMN9vc.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.CR6k3Y3G.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.x7mwu8jz.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.D8aTTvUr.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BoBqcKXQ.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.D3mTQdrU.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
- package/web/_astro/{ganttDiagram-6RSMTGT7.H-cqgIh-.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.B6s9zbfC.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
- package/web/_astro/index.Hjbr15fG.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BzgCoV6P.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BZzVhy1-.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.BV3195Py.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BjRd2DWz.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
- package/web/_astro/{linear.BILTgS5N.js → linear.C2_IkbZT.js} +1 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.BiEjaI4-.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.i_8V5pIn.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BWaW3MHn.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CzddBbtg.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.X2ww0e-D.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DSA4kTcc.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.D0DtFSpR.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BfQq0zQv.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.Dmlrgi1m.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.D5mpl00Z.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.Df4BdzO4.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.DiTRreKN.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.CDUcHlTJ.js +0 -188
- package/web/_astro/BoardApp.CaCGU_uX.js +0 -1
- package/web/_astro/arc.BzF71EFI.js +0 -1
- package/web/_astro/channel.SSVY0JPQ.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.Cmo2zQM-.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.D033eSVi.js +0 -10
- package/web/_astro/index.CcU5weKX.css +0 -1
- package/web/_astro/mermaid.core.DBy_WKeW.js +0 -301
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.92",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -24,7 +24,7 @@ field named by each column — never as a history-anatomy `category` (that vocab
|
|
|
24
24
|
| automated checks | `reliability` | `automated-checks` | `environment`; action is a gate, not prose |
|
|
25
25
|
| coding standards | `workflow` | `coding-standards` | `environment`; owner surface is review, never implementer |
|
|
26
26
|
| AGENTS.md placement | `workflow` | `agents-md-placement` | `environment`; action = move to skill/reference/check |
|
|
27
|
-
| tool economy | `performance` | `tool-economy` | existing
|
|
27
|
+
| tool economy | `performance` | `tool-economy` | existing evidence-based reuse-observation P3 (task 0913); `environment` when the tool itself is the waste |
|
|
28
28
|
| no-ops | `workflow` | `no-ops` | `environment`; `file:line` of the dead instruction |
|
|
29
29
|
| information access | `telemetry` | `information-access` | existing chained-step `~unknown` P3; `environment` when access is missing |
|
|
30
30
|
|
|
@@ -25,6 +25,135 @@ function declaredExecutedSteps(markdown) {
|
|
|
25
25
|
const match = markdown.match(/\*\*Steps:\*\*\s*\d+\s+derived,\s*(\d+)\s+executed/);
|
|
26
26
|
return match ? Number.parseInt(match[1], 10) : null;
|
|
27
27
|
}
|
|
28
|
+
var UNKNOWN_CELLS = new Set(["~unknown", "unknown", "—", "-", "?", "n/a", "~n/a"]);
|
|
29
|
+
var CACHE_SHARE_TOLERANCE = 1;
|
|
30
|
+
function sectionBody(markdown, headingRe) {
|
|
31
|
+
const heading = markdown.match(headingRe);
|
|
32
|
+
if (!heading || heading.index === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const after = markdown.slice(heading.index + heading[0].length);
|
|
35
|
+
const nextHeading = after.search(/^###? /m);
|
|
36
|
+
return nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
37
|
+
}
|
|
38
|
+
function parseTokensLine(line) {
|
|
39
|
+
const match = line.match(/~(?<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);
|
|
40
|
+
if (!match)
|
|
41
|
+
return null;
|
|
42
|
+
const num = (v) => v === undefined ? null : v === "n/a" ? "n/a" : Number.parseInt(v, 10);
|
|
43
|
+
return { total: num(match.groups?.total), cached: num(match.groups?.cached), pct: num(match.groups?.pct) };
|
|
44
|
+
}
|
|
45
|
+
function parseTokenCell(cell) {
|
|
46
|
+
const t = cell.trim();
|
|
47
|
+
if (UNKNOWN_CELLS.has(t))
|
|
48
|
+
return null;
|
|
49
|
+
const m = t.match(/~?(\d+)/);
|
|
50
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
51
|
+
}
|
|
52
|
+
function parsePctCell(cell) {
|
|
53
|
+
const t = cell.trim();
|
|
54
|
+
if (UNKNOWN_CELLS.has(t))
|
|
55
|
+
return null;
|
|
56
|
+
const m = t.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
57
|
+
return m ? Number.parseFloat(m[1]) : null;
|
|
58
|
+
}
|
|
59
|
+
function ledgerCostRows(markdown) {
|
|
60
|
+
const body = sectionBody(markdown, /^### 3\. Monitor Ledger\s*$/m);
|
|
61
|
+
if (body === null)
|
|
62
|
+
return [];
|
|
63
|
+
return body.split(`
|
|
64
|
+
`).filter((line) => line.trim().startsWith("|")).filter((line) => !/^\|[\s:|-]+\|?\s*$/.test(line.trim())).filter((line) => !/^\|\s*`?drift:/.test(line.trim())).slice(1).map((line) => {
|
|
65
|
+
const cells = line.split("|").map((c) => c.trim()).filter((c, i, a) => !(i === 0 && c === "") && !(i === a.length - 1 && c === ""));
|
|
66
|
+
const freshCell = cells[5] ?? "";
|
|
67
|
+
const cachedCell = cells[6] ?? "";
|
|
68
|
+
return {
|
|
69
|
+
step: cells[0] ?? "",
|
|
70
|
+
fresh: parseTokenCell(freshCell),
|
|
71
|
+
cached: parseTokenCell(cachedCell),
|
|
72
|
+
cachePct: parsePctCell(cells[7] ?? ""),
|
|
73
|
+
unknownCell: UNKNOWN_CELLS.has(freshCell) || UNKNOWN_CELLS.has(cachedCell)
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function validateCostEvidence(markdown, errors) {
|
|
78
|
+
const costBlock = sectionBody(markdown, /^#### Cost\s*$/m);
|
|
79
|
+
if (costBlock === null) {
|
|
80
|
+
errors.push("missing_cost_block");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const field of ["Ledger estimate", "Method", "Meter"]) {
|
|
84
|
+
if (!costBlock.includes(`**${field}:**`))
|
|
85
|
+
errors.push(`missing_cost_field:${field}`);
|
|
86
|
+
}
|
|
87
|
+
const ledgerLine = costBlock.split(`
|
|
88
|
+
`).find((l) => l.includes("**Ledger estimate:**")) ?? "";
|
|
89
|
+
const costTotals = parseTokensLine(ledgerLine);
|
|
90
|
+
if (ledgerLine !== "" && costTotals === null)
|
|
91
|
+
errors.push("malformed_cost_line");
|
|
92
|
+
const footerLine = markdown.split(`
|
|
93
|
+
`).find((l) => l.trim().startsWith("Tokens:"));
|
|
94
|
+
const footerTotals = footerLine ? parseTokensLine(footerLine) : null;
|
|
95
|
+
if (footerLine !== undefined && footerTotals === null)
|
|
96
|
+
errors.push("malformed_footer");
|
|
97
|
+
if (footerLine === undefined && markdown.includes("── Dogfood Summary ──")) {
|
|
98
|
+
errors.push("missing_footer_tokens");
|
|
99
|
+
}
|
|
100
|
+
const pctScopes = [costBlock, footerLine ?? ""];
|
|
101
|
+
for (const row of ledgerCostRows(markdown)) {
|
|
102
|
+
if (row.cachePct !== null)
|
|
103
|
+
pctScopes.push(`${row.cachePct}%`);
|
|
104
|
+
}
|
|
105
|
+
for (const scope of pctScopes) {
|
|
106
|
+
for (const m of scope.matchAll(/(\d+(?:\.\d+)?)\s*%/g)) {
|
|
107
|
+
const pct = Number.parseFloat(m[1]);
|
|
108
|
+
if (pct < 0 || pct > 100)
|
|
109
|
+
errors.push(`impossible_percentage:${m[0].trim()}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const rows = ledgerCostRows(markdown);
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
if (row.unknownCell && row.cachePct !== null)
|
|
115
|
+
errors.push("unknown_row_with_numeric_cache");
|
|
116
|
+
}
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
if (row.fresh === null || row.cached === null || row.cachePct === null)
|
|
119
|
+
continue;
|
|
120
|
+
const basis = row.fresh + row.cached;
|
|
121
|
+
if (basis === 0)
|
|
122
|
+
continue;
|
|
123
|
+
const expected = Math.round(row.cached / basis * 100);
|
|
124
|
+
if (Math.abs(row.cachePct - expected) > CACHE_SHARE_TOLERANCE) {
|
|
125
|
+
errors.push(`cache_pct_mismatch:${row.step}_expected_${expected}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const observable = rows.filter((r) => r.fresh !== null && r.cached !== null);
|
|
129
|
+
const sumFresh = observable.reduce((acc, r) => acc + (r.fresh ?? 0), 0);
|
|
130
|
+
const sumCached = observable.reduce((acc, r) => acc + (r.cached ?? 0), 0);
|
|
131
|
+
const checkTotals = (label, totals) => {
|
|
132
|
+
if (totals === null)
|
|
133
|
+
return;
|
|
134
|
+
if (observable.length > 0) {
|
|
135
|
+
const expectedTotal = sumFresh + sumCached;
|
|
136
|
+
if (typeof totals.total === "number" && totals.total !== expectedTotal) {
|
|
137
|
+
errors.push(`cost_total_mismatch:${label}_expected_${expectedTotal}`);
|
|
138
|
+
}
|
|
139
|
+
if (typeof totals.cached === "number" && totals.cached !== sumCached) {
|
|
140
|
+
errors.push(`cost_total_mismatch:${label}_cached_expected_${sumCached}`);
|
|
141
|
+
}
|
|
142
|
+
const expectedPct = expectedTotal > 0 ? Math.round(sumCached / expectedTotal * 100) : null;
|
|
143
|
+
if (expectedPct !== null) {
|
|
144
|
+
if (totals.pct === "n/a" || totals.pct === null) {
|
|
145
|
+
errors.push(`missing_cache_share:${label}`);
|
|
146
|
+
} else if (typeof totals.pct === "number" && Math.abs(totals.pct - expectedPct) > CACHE_SHARE_TOLERANCE) {
|
|
147
|
+
errors.push(`cache_share_mismatch:${label}_expected_${expectedPct}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else if (typeof totals.total === "number" || typeof totals.cached === "number" || typeof totals.pct === "number") {
|
|
151
|
+
errors.push(`fabricated_cache_share:${label}`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
checkTotals("cost", costTotals);
|
|
155
|
+
checkTotals("footer", footerTotals);
|
|
156
|
+
}
|
|
28
157
|
function validateReport(markdown) {
|
|
29
158
|
const errors = [];
|
|
30
159
|
if (!markdown.includes("── Dogfood Summary ──"))
|
|
@@ -56,6 +185,7 @@ function validateReport(markdown) {
|
|
|
56
185
|
errors.push("ledger_cardinality");
|
|
57
186
|
}
|
|
58
187
|
}
|
|
188
|
+
validateCostEvidence(markdown, errors);
|
|
59
189
|
return { ok: errors.length === 0, errors };
|
|
60
190
|
}
|
|
61
191
|
function parseValidateCliArgs(argv) {
|
|
@@ -83,6 +213,12 @@ Exit codes:
|
|
|
83
213
|
2 validation failed (errors on stdout / --json)
|
|
84
214
|
1 usage error
|
|
85
215
|
|
|
216
|
+
Cost evidence (task 0913): a complete report must carry the Cost block
|
|
217
|
+
(Ledger estimate / Method / Meter), percentages within 0..100, totals equal to
|
|
218
|
+
the ledger sums of observable rows (fresh + cached), and 'n/a' (never a
|
|
219
|
+
fabricated number) when no observable rows exist. Display-rounding tolerance
|
|
220
|
+
for the cache share is ±1 percentage point.
|
|
221
|
+
|
|
86
222
|
Phase 4 finalize MUST run this before status: complete (task 0278 R6).
|
|
87
223
|
On exit 2: set status: aborted and list error codes under #### Unresolved.`;
|
|
88
224
|
function runValidateCli(argv, readFile) {
|
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
* codes, callable from tests and by agents finalizing a run. It validates the
|
|
6
6
|
* *complete-report* shape: the six unique section headings, Issues subheads, the
|
|
7
7
|
* mandatory summary footer with both delivery paths, frontmatter protocol string,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
10
15
|
*
|
|
11
16
|
* CLI (task 0278 R6): `bun …/validate-report.ts --file <report.md> [--json]`
|
|
12
17
|
*/
|
|
@@ -49,6 +54,185 @@ function declaredExecutedSteps(markdown: string): number | null {
|
|
|
49
54
|
return match ? Number.parseInt(match[1], 10) : null;
|
|
50
55
|
}
|
|
51
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+)/);
|
|
101
|
+
return m ? Number.parseInt(m[1], 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*%/);
|
|
108
|
+
return m ? Number.parseFloat(m[1]) : 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
|
+
|
|
52
236
|
export function validateReport(markdown: string): ReportValidation {
|
|
53
237
|
const errors: string[] = [];
|
|
54
238
|
|
|
@@ -86,6 +270,10 @@ export function validateReport(markdown: string): ReportValidation {
|
|
|
86
270
|
}
|
|
87
271
|
}
|
|
88
272
|
|
|
273
|
+
// Cost evidence (task 0913, R2) — required block, arithmetic consistency,
|
|
274
|
+
// honest unknowns.
|
|
275
|
+
validateCostEvidence(markdown, errors);
|
|
276
|
+
|
|
89
277
|
return { ok: errors.length === 0, errors };
|
|
90
278
|
}
|
|
91
279
|
|
|
@@ -119,6 +307,12 @@ Exit codes:
|
|
|
119
307
|
2 validation failed (errors on stdout / --json)
|
|
120
308
|
1 usage error
|
|
121
309
|
|
|
310
|
+
Cost evidence (task 0913): a complete report must carry the Cost block
|
|
311
|
+
(Ledger estimate / Method / Meter), percentages within 0..100, totals equal to
|
|
312
|
+
the ledger sums of observable rows (fresh + cached), and 'n/a' (never a
|
|
313
|
+
fabricated number) when no observable rows exist. Display-rounding tolerance
|
|
314
|
+
for the cache share is ±1 percentage point.
|
|
315
|
+
|
|
122
316
|
Phase 4 finalize MUST run this before status: complete (task 0278 R6).
|
|
123
317
|
On exit 2: set status: aborted and list error codes under #### Unresolved.`;
|
|
124
318
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/feature-verification-steps.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import {
|
|
7
|
+
closeSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
writeFileSync
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
// plugins/sp/lib/env.ts
|
|
20
|
+
function getEnvVar(name, fallback) {
|
|
21
|
+
const raw = process.env[name];
|
|
22
|
+
return raw === undefined ? fallback : raw;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// plugins/sp/scripts/feature-verification-steps.ts
|
|
26
|
+
var SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27
|
+
function bundleEntry() {
|
|
28
|
+
return fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
|
|
29
|
+
}
|
|
30
|
+
function sourceModeError(spurBin) {
|
|
31
|
+
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 " + "splitLaunchCommand / ArtifactDao. 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 these seams to the app source entry.");
|
|
32
|
+
}
|
|
33
|
+
function resolveModuleMode(raw) {
|
|
34
|
+
if (raw === "" || raw === "bundle")
|
|
35
|
+
return "bundle";
|
|
36
|
+
if (raw === "source")
|
|
37
|
+
return "source";
|
|
38
|
+
throw new Error(`unknown --module-mode ${JSON.stringify(raw)}; expected "source" or "bundle"`);
|
|
39
|
+
}
|
|
40
|
+
async function loadModule(spurBin, mode) {
|
|
41
|
+
if (mode === "source")
|
|
42
|
+
throw sourceModeError(spurBin);
|
|
43
|
+
const bundle = bundleEntry();
|
|
44
|
+
if (!existsSync(bundle)) {
|
|
45
|
+
throw new Error(`feature-verification-steps: mode=bundle \u2014 the generated inline bundle is missing at ${bundle}` + `${spurBin === "" ? "" : ` (spurBin=${spurBin})`}. ` + "Fix: in a source checkout run `bun run build:bundle`; for an installed plugin reinstall it.");
|
|
46
|
+
}
|
|
47
|
+
return requireModule(bundle, mode);
|
|
48
|
+
}
|
|
49
|
+
async function requireModule(entry, mode) {
|
|
50
|
+
const mod = await import(entry);
|
|
51
|
+
const missing = [
|
|
52
|
+
"startFeatureVerificationReceipt",
|
|
53
|
+
"completeFeatureVerificationReceipt",
|
|
54
|
+
"captureFeatureReceiptDigest",
|
|
55
|
+
"resolveWorkflowDefinition",
|
|
56
|
+
"splitLaunchCommand",
|
|
57
|
+
"openInlineRunProjectDb",
|
|
58
|
+
"ArtifactDao"
|
|
59
|
+
].filter((k) => typeof mod[k] !== "function");
|
|
60
|
+
if (missing.length > 0) {
|
|
61
|
+
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.");
|
|
62
|
+
}
|
|
63
|
+
return mod;
|
|
64
|
+
}
|
|
65
|
+
function assertSafeId(kind, id) {
|
|
66
|
+
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
67
|
+
throw new Error(`refusing unsafe ${kind}: ${id}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function findFeatureFile(featureDir, featureId) {
|
|
71
|
+
if (!existsSync(featureDir))
|
|
72
|
+
return;
|
|
73
|
+
const name = readdirSync(featureDir).find((n) => n.startsWith(`${featureId}_`) && n.endsWith(".md"));
|
|
74
|
+
return name === undefined ? undefined : join(featureDir, name);
|
|
75
|
+
}
|
|
76
|
+
var nodeFsShim = {
|
|
77
|
+
ensureDir: (dir) => {
|
|
78
|
+
mkdirSync(dir, { recursive: true });
|
|
79
|
+
},
|
|
80
|
+
writeFile: (path, body) => {
|
|
81
|
+
writeFileSync(path, body);
|
|
82
|
+
},
|
|
83
|
+
rename: (src, dest) => {
|
|
84
|
+
renameSync(src, dest);
|
|
85
|
+
},
|
|
86
|
+
readFile: (path) => {
|
|
87
|
+
return readFileSync(path, "utf8");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
async function verify(featureId, runId, cmdOverride, spurBin, mode) {
|
|
91
|
+
assertSafeId("feature id", featureId);
|
|
92
|
+
assertSafeId("run id", runId);
|
|
93
|
+
const cwd = process.cwd();
|
|
94
|
+
const mod = await loadModule(spurBin, mode);
|
|
95
|
+
const runDir = join(cwd, ".spur", "run");
|
|
96
|
+
mkdirSync(runDir, { recursive: true });
|
|
97
|
+
const selected = await mod.resolveWorkflowDefinition(cwd, "feature-verification");
|
|
98
|
+
const vars = selected.workflow.vars;
|
|
99
|
+
const configuredCmd = typeof vars?.verificationCmd === "string" && vars.verificationCmd.length > 0 ? vars.verificationCmd : "bun run spur-check-feature";
|
|
100
|
+
const effectiveCmd = cmdOverride !== "" ? cmdOverride : configuredCmd;
|
|
101
|
+
const featureFile = findFeatureFile(join(cwd, "docs", "features"), featureId);
|
|
102
|
+
if (featureFile === undefined) {
|
|
103
|
+
throw new Error(`feature ${featureId} not found under ${join(cwd, "docs", "features")}`);
|
|
104
|
+
}
|
|
105
|
+
const featureContent = readFileSync(featureFile, "utf8");
|
|
106
|
+
const learningsPath = join(cwd, ".spur", "context", "learnings.md");
|
|
107
|
+
const learningsContent = existsSync(learningsPath) ? readFileSync(learningsPath, "utf8") : undefined;
|
|
108
|
+
const beforeDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
|
|
109
|
+
const receipt = await mod.startFeatureVerificationReceipt(nodeFsShim, runDir, {
|
|
110
|
+
featureId,
|
|
111
|
+
runId,
|
|
112
|
+
workdir: cwd,
|
|
113
|
+
verifier: {
|
|
114
|
+
name: "feature-verification",
|
|
115
|
+
sourcePath: selected.path,
|
|
116
|
+
layer: selected.layer,
|
|
117
|
+
definitionDigest: selected.digest
|
|
118
|
+
},
|
|
119
|
+
verificationCmd: effectiveCmd,
|
|
120
|
+
inputDigest: beforeDigest
|
|
121
|
+
});
|
|
122
|
+
const db = await mod.openInlineRunProjectDb(cwd);
|
|
123
|
+
try {
|
|
124
|
+
await new mod.ArtifactDao(db.adapter).record({
|
|
125
|
+
runId,
|
|
126
|
+
path: join(runDir, `${runId}-feature-verification.json`),
|
|
127
|
+
kind: "feature-verification"
|
|
128
|
+
});
|
|
129
|
+
} finally {
|
|
130
|
+
db.close();
|
|
131
|
+
}
|
|
132
|
+
const logPath = join(runDir, `${runId}-feature-verification.log`);
|
|
133
|
+
const launch = mod.splitLaunchCommand(effectiveCmd, "feature-verification verificationCmd");
|
|
134
|
+
if ("error" in launch) {
|
|
135
|
+
await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, {
|
|
136
|
+
status: "FAIL",
|
|
137
|
+
inputDigest: beforeDigest
|
|
138
|
+
});
|
|
139
|
+
console.log(`feature-verification-steps: ${featureId} verification FAIL (${launch.error})`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const logFd = openSync(logPath, "a");
|
|
143
|
+
try {
|
|
144
|
+
const result = spawnSync(launch.command, launch.leadingArgs, {
|
|
145
|
+
cwd,
|
|
146
|
+
stdio: ["ignore", logFd, logFd],
|
|
147
|
+
timeout: 14400000
|
|
148
|
+
});
|
|
149
|
+
const exit = result.status ?? 1;
|
|
150
|
+
const afterDigest = await mod.captureFeatureReceiptDigest(cwd, featureContent, learningsContent);
|
|
151
|
+
const status = exit === 0 && afterDigest === beforeDigest ? "PASS" : "FAIL";
|
|
152
|
+
await mod.completeFeatureVerificationReceipt(nodeFsShim, runDir, receipt, { status, inputDigest: afterDigest });
|
|
153
|
+
console.log(`feature-verification-steps: ${featureId} verification ${status} (run ${runId}, log ${logPath})`);
|
|
154
|
+
} finally {
|
|
155
|
+
closeSync(logFd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function main() {
|
|
159
|
+
const args = process.argv.slice(2);
|
|
160
|
+
const command = args[0];
|
|
161
|
+
const flag = (name) => {
|
|
162
|
+
const i = args.indexOf(name);
|
|
163
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] ?? "" : "";
|
|
164
|
+
};
|
|
165
|
+
if (command !== "verify") {
|
|
166
|
+
console.error("Usage: feature-verification-steps.ts verify --feature-id <id> --run-id <id> [--cmd <command>]");
|
|
167
|
+
process.exit(2);
|
|
168
|
+
}
|
|
169
|
+
verify(flag("--feature-id") || getEnvVar("featureId") || "", flag("--run-id") || getEnvVar("__runId") || "", flag("--cmd") || getEnvVar("verificationCmd") || "", flag("--spur-bin") || getEnvVar("spurBin") || "", resolveModuleMode(flag("--module-mode"))).then(() => process.exit(0)).catch((err) => {
|
|
170
|
+
console.error(`feature-verification-steps: ${String(err)}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
main();
|