faberun 0.20.0 → 0.22.0
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/package.json +2 -1
- package/skills/faberun/references/operations.md +7 -7
- package/src/campaign/brief-cli.mjs +174 -0
- package/src/campaign/brief-text.mjs +96 -0
- package/src/campaign/campaign-brief.mjs +772 -0
- package/src/campaign/chain.mjs +30 -1
- package/src/campaign/projection.mjs +34 -0
- package/src/cli/campaign.mjs +22 -2
- package/src/contract/index.mjs +5 -3
- package/src/contract/runtime.mjs +22 -1
- package/src/contract/scope-findings.mjs +12 -0
- package/src/contract/snapshot.mjs +16 -5
- package/src/engine/dispatch.mjs +6 -4
- package/src/engine/process.mjs +1 -0
- package/src/engine/result-file.mjs +13 -1
- package/src/engine/settle.mjs +1 -0
- package/src/engine/verify.mjs +37 -2
- package/src/harnesses/codex/index.mjs +1 -0
- package/src/harnesses/index.mjs +18 -1
- package/src/plan/freeze.mjs +240 -35
- package/src/plan/pipeline-shape.mjs +100 -0
- package/src/plan/pipeline.mjs +68 -116
- package/src/plan/template.mjs +88 -25
- package/src/repo/declared-paths.mjs +16 -0
- package/src/repo/workspace.mjs +42 -3
- package/src/repo/worktree.mjs +34 -2
- package/src/report/campaign-brief-estimate.mjs +450 -0
- package/src/report/campaign-brief-html.mjs +439 -0
- package/src/report/campaign-brief.mjs +409 -0
- package/src/report/final.mjs +5 -4
- package/src/report/mdhtml-release.json +30 -0
- package/src/report/render.mjs +4 -3
- package/src/run/usage.mjs +265 -0
- package/src/web/campaign-brief-server.mjs +401 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign Brief Markdown: the portable review surface rendered from the
|
|
3
|
+
* deterministic model in `src/campaign/campaign-brief.mjs`.
|
|
4
|
+
*
|
|
5
|
+
* Rendering is a pure function of the model. It adds no timestamp and reads no
|
|
6
|
+
* live value, so the same spec, plan, contract and recorded-data snapshots
|
|
7
|
+
* produce the same bytes. The opening before the coverage matrix is bounded at
|
|
8
|
+
* 250 words; the coverage matrix, work graph, gaps, estimate and provenance
|
|
9
|
+
* follow it.
|
|
10
|
+
*/
|
|
11
|
+
/** @typedef {import("../campaign/campaign-brief.mjs").BriefModel} BriefModel */
|
|
12
|
+
/** @typedef {import("../campaign/campaign-brief.mjs").BriefCoverageRow} BriefCoverageRow */
|
|
13
|
+
/** @typedef {import("../campaign/campaign-brief.mjs").BriefMeasure} BriefMeasure */
|
|
14
|
+
|
|
15
|
+
const OPENING_WORD_LIMIT = 250;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render the Campaign Brief as Markdown. The result is portable: it links to
|
|
19
|
+
* the spec and frozen plan by path and contains every review fact as text.
|
|
20
|
+
*
|
|
21
|
+
* @param {BriefModel} model
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function renderCampaignBriefMarkdown(model) {
|
|
25
|
+
const opening = fitAtoms(openingAtoms(model), OPENING_WORD_LIMIT);
|
|
26
|
+
const body = [
|
|
27
|
+
renderCoverage(model),
|
|
28
|
+
renderWorkGraph(model),
|
|
29
|
+
renderDecisions(model),
|
|
30
|
+
renderRisksAndEvals(model),
|
|
31
|
+
renderGaps(model),
|
|
32
|
+
renderEstimate(model),
|
|
33
|
+
renderProvenance(model),
|
|
34
|
+
].join("\n\n");
|
|
35
|
+
return `${opening}\n\n${body}\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {BriefModel} model
|
|
40
|
+
* @returns {{text: string, priority: number}[]}
|
|
41
|
+
*/
|
|
42
|
+
function openingAtoms(model) {
|
|
43
|
+
return [
|
|
44
|
+
{ text: `# Campaign brief — ${model.identity.campaign}`, priority: 0 },
|
|
45
|
+
{ text: renderIdentity(model), priority: 0 },
|
|
46
|
+
{ text: renderDecision(model), priority: 0 },
|
|
47
|
+
{ text: renderIntent(model), priority: 0 },
|
|
48
|
+
{ text: renderExpectedOutcome(model), priority: 0 },
|
|
49
|
+
{ text: renderSuccessCriteria(model), priority: 1 },
|
|
50
|
+
{ text: renderHumanFacts(model), priority: 2 },
|
|
51
|
+
{ text: renderCalculatedFacts(model), priority: 3 },
|
|
52
|
+
{ text: renderOpenGapSummary(model), priority: 4 },
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Keep whole atoms while the opening stays within the word limit, dropping the
|
|
58
|
+
* lowest-priority atoms from the end first. Identity, decision, intent and
|
|
59
|
+
* expected outcome are never dropped.
|
|
60
|
+
*
|
|
61
|
+
* @param {{text: string, priority: number}[]} atoms
|
|
62
|
+
* @param {number} limit
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function fitAtoms(atoms, limit) {
|
|
66
|
+
const current = atoms.filter((atom) => atom.text.length > 0);
|
|
67
|
+
while (wordCount(current.map((atom) => atom.text).join("\n\n")) > limit) {
|
|
68
|
+
const maxPriority = Math.max(...current.map((atom) => atom.priority));
|
|
69
|
+
if (maxPriority === 0) break;
|
|
70
|
+
const index = current.map((atom) => atom.priority).lastIndexOf(maxPriority);
|
|
71
|
+
if (index < 0) break;
|
|
72
|
+
current.splice(index, 1);
|
|
73
|
+
}
|
|
74
|
+
return current.map((atom) => atom.text).join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {BriefModel} model
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function renderIdentity(model) {
|
|
82
|
+
const { identity } = model;
|
|
83
|
+
return [
|
|
84
|
+
"## Identity",
|
|
85
|
+
`- Campaign: \`${identity.campaign}\``,
|
|
86
|
+
`- Spec baseline: \`${identity.specBaseline ?? "not recorded"}\``,
|
|
87
|
+
`- Spec digest: \`${identity.specDigest}\``,
|
|
88
|
+
`- Spec: ${link(identity.specPath)}`,
|
|
89
|
+
`- Target git head: \`${identity.targetGitHead ?? "not recorded"}\``,
|
|
90
|
+
`- Frozen plan: ${link(identity.planPath)}`,
|
|
91
|
+
`- Plan digest: \`${identity.planDigest}\``,
|
|
92
|
+
`- Contract digest: \`${identity.contractDigest}\``,
|
|
93
|
+
`- Journal cursor: \`${identity.journalCursor}\``,
|
|
94
|
+
`- Usage sample cutoff: \`${identity.usageSampleCutoff ?? "not recorded"}\``,
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {BriefModel} model
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function renderDecision(model) {
|
|
103
|
+
return [
|
|
104
|
+
"## Decision",
|
|
105
|
+
`- State: **${model.decisionState}**`,
|
|
106
|
+
`- Technical plan: ${link(model.identity.planPath)}`,
|
|
107
|
+
`- Note: ${decisionNote(model)}`,
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {BriefModel} model
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function decisionNote(model) {
|
|
116
|
+
const gaps = allGaps(model);
|
|
117
|
+
if (gaps.length === 0) {
|
|
118
|
+
return "No coverage, work-graph or estimate gap is open; this is a review state, not approval to execute.";
|
|
119
|
+
}
|
|
120
|
+
return `${gaps.length} gap${gaps.length === 1 ? "" : "s"} must be resolved before this brief is ready; this is not approval to execute.`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {BriefModel} model
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function renderIntent(model) {
|
|
128
|
+
return `## Intent\n${model.opening.intent ?? "not recorded"}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {BriefModel} model
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
function renderExpectedOutcome(model) {
|
|
136
|
+
return `## Expected outcome\n${model.opening.expectedOutcome ?? "not recorded"}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {BriefModel} model
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function renderSuccessCriteria(model) {
|
|
144
|
+
const lines = (model.opening.successCriteria.length > 0 ? model.opening.successCriteria : [])
|
|
145
|
+
.map((criterion) => `- ${criterion.measure}: ${criterion.target}${criterion.evidence ? ` (evidence: ${criterion.evidence})` : ""}`);
|
|
146
|
+
return ["## Success criteria", ...(lines.length > 0 ? lines : ["- none recorded"])].join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {BriefModel} model
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
function renderHumanFacts(model) {
|
|
154
|
+
const facts = model.opening.humanFacts;
|
|
155
|
+
return ["## Human-authored facts", ...(facts.length > 0 ? facts.map((fact) => `- ${fact}`) : ["- none recorded"])].join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {BriefModel} model
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
function renderCalculatedFacts(model) {
|
|
163
|
+
const facts = model.opening.calculatedFacts;
|
|
164
|
+
return ["## Calculated facts", ...(facts.length > 0 ? facts.map((fact) => `- ${fact}`) : ["- none recorded"])].join("\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {BriefModel} model
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
function renderOpenGapSummary(model) {
|
|
172
|
+
const gaps = allGaps(model);
|
|
173
|
+
const line = gaps.length === 0
|
|
174
|
+
? "- none"
|
|
175
|
+
: `- ${gaps.length} open gap${gaps.length === 1 ? "" : "s"}; the full list follows the coverage matrix.`;
|
|
176
|
+
return ["## Open gaps", line].join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {BriefModel} model
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
function renderCoverage(model) {
|
|
184
|
+
const { coverage } = model;
|
|
185
|
+
const header = "| Requirement | Frozen nodes | Declared proof or verification | State |";
|
|
186
|
+
const separator = "| --- | --- | --- | --- |";
|
|
187
|
+
const rows = coverage.rows.map((row) => renderCoverageRow(row));
|
|
188
|
+
const unknown = coverage.unknownIds.length > 0
|
|
189
|
+
? `Unknown requirement ids: ${coverage.unknownIds.map((id) => `\`${id}\``).join(", ")}.`
|
|
190
|
+
: "Unknown requirement ids: none.";
|
|
191
|
+
return [
|
|
192
|
+
"## Coverage matrix",
|
|
193
|
+
`Spec: ${link(coverage.specPath)} · Plan: ${link(coverage.planPath)}`,
|
|
194
|
+
"Each spec requirement is cross-checked against the frozen declarations' nodeIds and each contract node's stamped requirementIds.",
|
|
195
|
+
"",
|
|
196
|
+
header,
|
|
197
|
+
separator,
|
|
198
|
+
...(rows.length > 0 ? rows : [`| _no spec requirements_ | — | — | outside this plan |`]),
|
|
199
|
+
"",
|
|
200
|
+
unknown,
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {BriefCoverageRow} row
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function renderCoverageRow(row) {
|
|
209
|
+
const nodeIds = row.nodes.length > 0
|
|
210
|
+
? row.nodes.map((node) => `\`${node.id}\``).join(", ")
|
|
211
|
+
: row.declaredNodeIds.length > 0 ? `none (declared: ${row.declaredNodeIds.map((id) => `\`${id}\``).join(", ")})` : "—";
|
|
212
|
+
const proofs = row.nodes.length > 0
|
|
213
|
+
? row.nodes.map((node) => `${node.id}: ${cell(node.proof)}`).join("; ")
|
|
214
|
+
: row.reasons.length > 0 ? cell(row.reasons.join("; ")) : "—";
|
|
215
|
+
return `| \`${row.requirementId}\` | ${nodeIds} | ${proofs} | ${row.state} |`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {BriefModel} model
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function renderWorkGraph(model) {
|
|
223
|
+
const { graph } = model;
|
|
224
|
+
const edgeLines = graph.edges.length > 0
|
|
225
|
+
? graph.edges.map((edge) => `- \`${edge.from}\` → \`${edge.to}\``)
|
|
226
|
+
: ["- Dependency edges: none"];
|
|
227
|
+
const independent = graph.independent.length > 0 ? graph.independent.map((id) => `\`${id}\``).join(", ") : "none";
|
|
228
|
+
const blocking = graph.blocking.length > 0
|
|
229
|
+
? graph.blocking.map((entry) => `\`${entry.node}\` depends on ${entry.prerequisites.map((id) => `\`${id}\``).join(", ")}`).join("; ")
|
|
230
|
+
: "none";
|
|
231
|
+
const concurrent = Object.entries(graph.maxConcurrent)
|
|
232
|
+
.map(([runtimeId, limit]) => `\`${runtimeId}\` ${limit}`)
|
|
233
|
+
.join(", ");
|
|
234
|
+
return [
|
|
235
|
+
"## Work graph",
|
|
236
|
+
...edgeLines,
|
|
237
|
+
`- Dependency-independent nodes: ${independent}`,
|
|
238
|
+
`- Blocking prerequisites: ${blocking}`,
|
|
239
|
+
`- maxParallel: ${graph.maxParallel}`,
|
|
240
|
+
`- maxConcurrent: ${concurrent || "none"}`,
|
|
241
|
+
`- Effective concurrency: ${graph.effectiveConcurrency}`,
|
|
242
|
+
`- ${renderDispatchable(graph)}`,
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The capacity reading, kept distinct from the graph reading: a node with no
|
|
248
|
+
* prerequisites is dependency-independent, but that never means it can run at
|
|
249
|
+
* the same time as a sibling. With `maxParallel` 1 the line says plainly that
|
|
250
|
+
* independent nodes are not simultaneously dispatchable.
|
|
251
|
+
*
|
|
252
|
+
* @param {BriefModel["graph"]} graph
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
function renderDispatchable(graph) {
|
|
256
|
+
if (graph.dispatchableTogether.length >= 2) {
|
|
257
|
+
return `Workers that can run at the same time: ${graph.dispatchableTogether.map((id) => `\`${id}\``).join(", ")}.`;
|
|
258
|
+
}
|
|
259
|
+
return `Workers that can run at the same time: none — ${graph.dispatchNote}.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Human and delegated decisions from this campaign's spec sections and the
|
|
264
|
+
* active campaign journal projection, and nothing else. The section repeats the
|
|
265
|
+
* facts the opening carries so they survive the 250-word opening limit.
|
|
266
|
+
*
|
|
267
|
+
* @param {BriefModel} model
|
|
268
|
+
* @returns {string}
|
|
269
|
+
*/
|
|
270
|
+
function renderDecisions(model) {
|
|
271
|
+
const { decisions } = model;
|
|
272
|
+
const lines = [
|
|
273
|
+
"## Decisions",
|
|
274
|
+
"(sources: this campaign's spec sections and the active campaign journal projection; nothing inferred from the graph)",
|
|
275
|
+
...decisions.human.map((text) => `- Human decision (spec): ${text}`),
|
|
276
|
+
...decisions.delegated.map((text) => `- Delegated decision (spec): ${text}`),
|
|
277
|
+
...decisions.journal.map((decision) => `- Journal decision [${decision.id}]: ${decision.text}${decision.at ? ` · ${decision.at}` : ""}`),
|
|
278
|
+
];
|
|
279
|
+
if (lines.length === 2) lines.push("- none recorded");
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Risks and planned evals, from this campaign's spec only.
|
|
285
|
+
*
|
|
286
|
+
* @param {BriefModel} model
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
function renderRisksAndEvals(model) {
|
|
290
|
+
const { decisions } = model;
|
|
291
|
+
const lines = [
|
|
292
|
+
"## Risks and planned evals",
|
|
293
|
+
...decisions.risks.map((risk) => `- Risk: ${risk.risk} — impact: ${risk.impact}; mitigation: ${risk.mitigation}`),
|
|
294
|
+
...decisions.evals.map((text) => `- Planned eval: ${text}`),
|
|
295
|
+
];
|
|
296
|
+
if (lines.length === 1) lines.push("- none recorded");
|
|
297
|
+
return lines.join("\n");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param {BriefModel} model
|
|
302
|
+
* @returns {string}
|
|
303
|
+
*/
|
|
304
|
+
function renderGaps(model) {
|
|
305
|
+
const gaps = allGaps(model);
|
|
306
|
+
const lines = gaps.length > 0 ? gaps.map((gap) => `- ${gap}`) : ["- none"];
|
|
307
|
+
return ["## Gaps", ...lines].join("\n");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {BriefModel} model
|
|
312
|
+
* @returns {string}
|
|
313
|
+
*/
|
|
314
|
+
function renderEstimate(model) {
|
|
315
|
+
const { estimate } = model;
|
|
316
|
+
const method = estimate.method.length > 0 ? estimate.method.join("; ") : "not recorded";
|
|
317
|
+
const assumptions = estimate.assumptions.length > 0 ? estimate.assumptions.join("; ") : "not recorded";
|
|
318
|
+
return [
|
|
319
|
+
"## Estimate",
|
|
320
|
+
`- Nodes: ${estimate.nodeCount}`,
|
|
321
|
+
`- Workers: ${estimate.workerCount}`,
|
|
322
|
+
`- Cost: ${renderMeasure(estimate.cost, "usd")}`,
|
|
323
|
+
`- Cost provenance: ${estimate.cost.provenance ?? "not recorded"}`,
|
|
324
|
+
`- Duration: ${renderMeasure(estimate.duration, "minutes")}`,
|
|
325
|
+
`- Duration provenance: ${estimate.duration.provenance ?? "not recorded"}`,
|
|
326
|
+
`- Runtimes: ${estimate.runtimes.length > 0 ? estimate.runtimes.map((id) => `\`${id}\``).join(", ") : "none"}`,
|
|
327
|
+
`- Models: ${estimate.models.length > 0 ? estimate.models.map((model_) => `\`${model_}\``).join(", ") : "none"}`,
|
|
328
|
+
`- Effective worker concurrency: ${estimate.effectiveConcurrency}`,
|
|
329
|
+
`- Method: ${method}`,
|
|
330
|
+
`- Assumptions: ${assumptions}`,
|
|
331
|
+
`- Sample cutoff: ${estimate.sampleCutoff ?? "not recorded"}`,
|
|
332
|
+
`- Advisory only: ranges are not spend or time ceilings.`,
|
|
333
|
+
].join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {BriefMeasure} measure
|
|
338
|
+
* @param {"usd"|"minutes"} unit
|
|
339
|
+
* @returns {string}
|
|
340
|
+
*/
|
|
341
|
+
function renderMeasure(measure, unit) {
|
|
342
|
+
if (measure.status !== "range" || measure.min === null || measure.max === null) {
|
|
343
|
+
const samples = typeof measure.samples === "number" ? ` (${measure.samples} comparable sample${measure.samples === 1 ? "" : "s"})` : "";
|
|
344
|
+
return `insufficient data — ${measure.reason ?? "no reason recorded"}${samples}`;
|
|
345
|
+
}
|
|
346
|
+
const range = unit === "usd" ? `$${measure.min}–$${measure.max}` : `${measure.min}–${measure.max} minutes`;
|
|
347
|
+
const provenance = measure.sourceRuns.length > 0 ? ` (source runs: ${measure.sourceRuns.join(", ")})` : "";
|
|
348
|
+
return `${range} from ${measure.samples} comparable samples${provenance}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {BriefModel} model
|
|
353
|
+
* @returns {string}
|
|
354
|
+
*/
|
|
355
|
+
function renderProvenance(model) {
|
|
356
|
+
return [
|
|
357
|
+
"## Provenance",
|
|
358
|
+
`- Journal cursor: \`${model.identity.journalCursor}\``,
|
|
359
|
+
`- Usage sample cutoff: \`${model.identity.usageSampleCutoff ?? "not recorded"}\``,
|
|
360
|
+
"- Deterministic: the same spec, plan, contract and recorded-data snapshots produce these bytes.",
|
|
361
|
+
].join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Every gap the decision state considers, in the model's own order: coverage,
|
|
366
|
+
* work graph, decisions, then the estimate measures.
|
|
367
|
+
*
|
|
368
|
+
* @param {BriefModel} model
|
|
369
|
+
* @returns {string[]}
|
|
370
|
+
*/
|
|
371
|
+
function allGaps(model) {
|
|
372
|
+
return [
|
|
373
|
+
...model.coverage.gaps,
|
|
374
|
+
...model.graph.gaps,
|
|
375
|
+
...model.decisions.gaps,
|
|
376
|
+
...model.opening.gaps,
|
|
377
|
+
...model.estimate.gaps,
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A path rendered as a Markdown link; the link text is the path itself so the
|
|
383
|
+
* brief stays readable without following it.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} path
|
|
386
|
+
* @returns {string}
|
|
387
|
+
*/
|
|
388
|
+
function link(path) {
|
|
389
|
+
return `[${cell(path)}](${path})`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Escape a table cell and collapse it to one line.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} value
|
|
396
|
+
* @returns {string}
|
|
397
|
+
*/
|
|
398
|
+
function cell(value) {
|
|
399
|
+
return value.replace(/\|/gu, "\\|").replace(/\s+/gu, " ").trim();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {string} text
|
|
404
|
+
* @returns {number}
|
|
405
|
+
*/
|
|
406
|
+
function wordCount(text) {
|
|
407
|
+
const trimmed = text.trim();
|
|
408
|
+
return trimmed ? trimmed.split(/\s+/u).length : 0;
|
|
409
|
+
}
|
package/src/report/final.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { compactCost, compactTokens, errorCode } from "../util.mjs";
|
|
10
10
|
import { basename, join } from "node:path";
|
|
11
11
|
import { readJson, writeJsonAtomic, writeTextAtomic } from "../run/store.mjs";
|
|
12
|
-
import { scopeFindingsNote } from "../contract/scope-findings.mjs";
|
|
12
|
+
import { scopeFindingsNote, verificationArtifactsNote } from "../contract/scope-findings.mjs";
|
|
13
13
|
import { MARK, advisoryFindingCount, fit, judgeRuntimeLabel, roleCosts, roleUsage, statusNote, workerRuntimeLabel, writeStatusArtifacts } from "./render.mjs";
|
|
14
14
|
import { packetRepetitionByNode, packetRepetitionNote } from "./packet-repetition.mjs";
|
|
15
15
|
import { unlinkSync } from "node:fs";
|
|
@@ -131,9 +131,10 @@ export function renderFinalReport(runDir, contract, states) {
|
|
|
131
131
|
const judge = judgeRuntimeLabel(node) ?? "-";
|
|
132
132
|
const planNode = contract.nodes.find((candidate) => candidate.id === node.id);
|
|
133
133
|
const detail = node.gate?.summary ?? node.error?.message ?? (node.blockedBy?.length ? node.blockedBy.join(", ") : null) ?? (typeof node.result === "string" && node.result.trim() ? node.result.trim() : node.phase ?? "-");
|
|
134
|
-
// The advisory
|
|
135
|
-
const
|
|
136
|
-
|
|
134
|
+
// The advisory findings lead the note, as they do in STATUS.md.
|
|
135
|
+
const findings = [scopeFindingsNote(node.scopeFindings), verificationArtifactsNote(node.verificationArtifacts)].filter(Boolean).join(" · ");
|
|
136
|
+
const note = findings
|
|
137
|
+
? `${findings} · ${detail}`
|
|
137
138
|
: `${detail} · phase ${planNode?.phase ?? "-"} · ${node.invocations?.at(-1)?.continuationMode ?? "fresh"}`;
|
|
138
139
|
lines.push(row([
|
|
139
140
|
MARK[node.status] ?? "[?]",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "faberun/mdhtml-release/1",
|
|
3
|
+
"repo": "feliperun/md.html",
|
|
4
|
+
"tag": "v1.1.3",
|
|
5
|
+
"version": "1.1.3",
|
|
6
|
+
"major": 1,
|
|
7
|
+
"minimumVersion": "1.1.3",
|
|
8
|
+
"assets": {
|
|
9
|
+
"darwin-arm64": {
|
|
10
|
+
"file": "mdhtml-1.1.3-darwin-arm64.tar.gz",
|
|
11
|
+
"sha256": "14cb7921ea75d6732974a32450f3f2abb06a9f69b464aab486de7312b94593a2"
|
|
12
|
+
},
|
|
13
|
+
"darwin-x64": {
|
|
14
|
+
"file": "mdhtml-1.1.3-darwin-x64.tar.gz",
|
|
15
|
+
"sha256": "dfb348c02f562bd721bc6a64ebd2b5d5b5ac56f2b19dfa7fd9705e7fdaf22a0d"
|
|
16
|
+
},
|
|
17
|
+
"linux-x64-gnu": {
|
|
18
|
+
"file": "mdhtml-1.1.3-linux-x64-gnu.tar.gz",
|
|
19
|
+
"sha256": "29d6a6f719f683d4aaebe4112d1e6b7c984a0125826fa12e3c3e7675046fdf15"
|
|
20
|
+
},
|
|
21
|
+
"linux-x64-musl": {
|
|
22
|
+
"file": "mdhtml-1.1.3-linux-x64-musl.tar.gz",
|
|
23
|
+
"sha256": "eba8b398a6eb6ec17fee5a8fbebdc6cab9511d66dc80ae50351df982c6b7dbf3"
|
|
24
|
+
},
|
|
25
|
+
"windows-x64": {
|
|
26
|
+
"file": "mdhtml-1.1.3-windows-x64.tar.gz",
|
|
27
|
+
"sha256": "2f68f85ea48bb7797a4c2c910e005fb02332d3f2aa548da1484d08c817a8d0fc"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/report/render.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { basename, join } from "node:path";
|
|
|
3
3
|
import { validateContract } from "../contract/index.mjs";
|
|
4
4
|
import { readJson, writeJsonAtomic } from "../run/store.mjs";
|
|
5
5
|
import { lockStale, pidAlive, readLock } from "../run/lock.mjs";
|
|
6
|
-
import { scopeFindingsNote } from "../contract/scope-findings.mjs";
|
|
6
|
+
import { scopeFindingsNote, verificationArtifactsNote } from "../contract/scope-findings.mjs";
|
|
7
7
|
import { reviewNote } from "../contract/review-modes.mjs";
|
|
8
8
|
import { validateNodeSnapshot, validateRunMetadata } from "../contract/snapshot.mjs";
|
|
9
9
|
import { compactCost, compactTokens, truncateChars } from "../util.mjs";
|
|
@@ -32,7 +32,7 @@ const POINTER_ATTENTION_CHARS = 80;
|
|
|
32
32
|
/** @typedef {{inputTokens: number|null, outputTokens: number|null, cacheReadInputTokens: number|null}} StatusPayloadUsage */
|
|
33
33
|
/** @typedef {{index: number, total: number, argv: string}} VerificationProgress */
|
|
34
34
|
/** @typedef {{verdict: string, maxSeverity: string, findingCount: number, summary: string|null}} StatusPayloadGate */
|
|
35
|
-
/** @typedef {{id: string, status: NodeStatus, phase: string|null, executionPhase: string|null, runtime: string|null, workerRuntime: string|null, judgeRuntime: string|null, continuation: string, attempt: number, revisions: number, startedAt: string|null, updatedAt: string|null, usage: StatusPayloadUsage|null, costUsd: number|null, roleCostUsd: {worker: number|null, judge: number|null}, costProvenance: {worker: string, judge: string}, verdict: string|null, gate: StatusPayloadGate|null, gateOutcome: "passed"|"rejected"|null, pendingHandoff: {runtime: string, reason: string}|null, note: string|null, scopeFindings: string[]|null, errorCode: string|null, blockedBy: string[], verificationProgress: VerificationProgress|null, declaredReadBytes: number|null}} StatusPayloadNode */
|
|
35
|
+
/** @typedef {{id: string, status: NodeStatus, phase: string|null, executionPhase: string|null, runtime: string|null, workerRuntime: string|null, judgeRuntime: string|null, continuation: string, attempt: number, revisions: number, startedAt: string|null, updatedAt: string|null, usage: StatusPayloadUsage|null, costUsd: number|null, roleCostUsd: {worker: number|null, judge: number|null}, costProvenance: {worker: string, judge: string}, verdict: string|null, gate: StatusPayloadGate|null, gateOutcome: "passed"|"rejected"|null, pendingHandoff: {runtime: string, reason: string}|null, note: string|null, scopeFindings: string[]|null, verificationArtifacts: string[]|null, errorCode: string|null, blockedBy: string[], verificationProgress: VerificationProgress|null, declaredReadBytes: number|null}} StatusPayloadNode */
|
|
36
36
|
/** @typedef {{schemaVersion: 1, run: string, contractId: string, campaignId: string, goal: string, usage: {inputTokens: number, outputTokens: number, cacheReadInputTokens: number, costUsd: number|null}, roles: {worker: RoleUsage, judge: RoleUsage}, controller: JsonObject, identityWarnings: string[], summary: string, nodes: StatusPayloadNode[]}} StatusPayload */
|
|
37
37
|
|
|
38
38
|
/** The glyph each terminal state prints in a status table. */
|
|
@@ -296,6 +296,7 @@ function buildStatusPayload(runDir, contract, nodes, identityWarnings, usage) {
|
|
|
296
296
|
pendingHandoff: pendingHandoff(node),
|
|
297
297
|
note: statusNote(node),
|
|
298
298
|
scopeFindings: node.scopeFindings?.unexpectedPaths ?? null,
|
|
299
|
+
verificationArtifacts: node.verificationArtifacts ?? null,
|
|
299
300
|
errorCode: node.error?.code ?? null,
|
|
300
301
|
blockedBy: node.blockedBy ?? [],
|
|
301
302
|
verificationProgress: progress,
|
|
@@ -723,7 +724,7 @@ function boundedNote(segments, maxLength = MAX_NOTE_LENGTH) {
|
|
|
723
724
|
* @returns {string|null}
|
|
724
725
|
*/
|
|
725
726
|
export function statusNote(node) {
|
|
726
|
-
const scope = scopeFindingsNote(node.scopeFindings);
|
|
727
|
+
const scope = boundedNote([scopeFindingsNote(node.scopeFindings), verificationArtifactsNote(node.verificationArtifacts)]);
|
|
727
728
|
const review = reviewNote(node);
|
|
728
729
|
const detail = node.gate?.summary ?? node.error?.message ?? node.blockedBy?.join(", ") ?? (candidateVerificationActive(node) ? "candidate" : node.phase);
|
|
729
730
|
const note = boundedNote([review, detail]);
|