@renzrollon/interlock 1.0.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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +58 -0
- package/CHANGELOG.md +380 -0
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/agents/ping.md +8 -0
- package/agents/worker.md +8 -0
- package/bin/interlock +2655 -0
- package/bin/interlock-graph +6 -0
- package/bin/interlock-run +508 -0
- package/bin/interlock-ship-acp +39 -0
- package/hooks/_shared.mjs +162 -0
- package/hooks/guard-commit.mjs +77 -0
- package/hooks/guard-tasks.mjs +107 -0
- package/hooks/guard-tests.mjs +63 -0
- package/hooks/preflight.mjs +107 -0
- package/lib/artifacts.mjs +316 -0
- package/lib/autonomy.mjs +149 -0
- package/lib/conformance.mjs +132 -0
- package/lib/doctor.mjs +1176 -0
- package/lib/drift.mjs +409 -0
- package/lib/eval-history.mjs +304 -0
- package/lib/evals-calibrate.mjs +317 -0
- package/lib/evals-capture.mjs +437 -0
- package/lib/evals-promote.mjs +257 -0
- package/lib/evals-triage.mjs +216 -0
- package/lib/findings.mjs +340 -0
- package/lib/graph/cli.mjs +266 -0
- package/lib/graph/extract/config-json.mjs +74 -0
- package/lib/graph/extract/create-extractor.mjs +35 -0
- package/lib/graph/extract/memory.mjs +99 -0
- package/lib/graph/extract/openspec.mjs +80 -0
- package/lib/graph/extract/python.mjs +137 -0
- package/lib/graph/extract/regex-fallback.mjs +187 -0
- package/lib/graph/extract/shell.mjs +71 -0
- package/lib/graph/extract/typescript.mjs +217 -0
- package/lib/graph/extract/walk.mjs +272 -0
- package/lib/graph/format.mjs +51 -0
- package/lib/graph/graph/build.mjs +374 -0
- package/lib/graph/graph/query.mjs +249 -0
- package/lib/graph/graph/report.mjs +122 -0
- package/lib/graph/graph/update.mjs +47 -0
- package/lib/graph/paths.mjs +66 -0
- package/lib/graph/retrieve/context.mjs +86 -0
- package/lib/graph/retrieve/docs-index.mjs +116 -0
- package/lib/graph/retrieve/markdown.mjs +246 -0
- package/lib/host/acp.mjs +551 -0
- package/lib/host/claude-cli.mjs +285 -0
- package/lib/host/codex.mjs +231 -0
- package/lib/host/model-map.mjs +129 -0
- package/lib/host/qwen.mjs +176 -0
- package/lib/host/registry.mjs +213 -0
- package/lib/host.mjs +393 -0
- package/lib/ledger.mjs +444 -0
- package/lib/limits.mjs +525 -0
- package/lib/locators.mjs +99 -0
- package/lib/merge-lanes.mjs +148 -0
- package/lib/metrics.mjs +99 -0
- package/lib/notify.mjs +229 -0
- package/lib/outcomes.mjs +560 -0
- package/lib/plan-fingerprint.mjs +570 -0
- package/lib/project-slug.mjs +18 -0
- package/lib/prompts/commit.mjs +26 -0
- package/lib/prompts/dimensions.mjs +70 -0
- package/lib/prompts/handoff.mjs +86 -0
- package/lib/prompts/implementer.mjs +150 -0
- package/lib/prompts/index.mjs +250 -0
- package/lib/prompts/planner.mjs +70 -0
- package/lib/prompts/remediate.mjs +137 -0
- package/lib/prompts/replan.mjs +23 -0
- package/lib/prompts/review.mjs +107 -0
- package/lib/prompts/schemas.mjs +206 -0
- package/lib/prompts/stage.mjs +48 -0
- package/lib/prompts/verify.mjs +88 -0
- package/lib/ready.mjs +812 -0
- package/lib/receipt.mjs +434 -0
- package/lib/remediate.mjs +275 -0
- package/lib/report-html.mjs +623 -0
- package/lib/report.mjs +1124 -0
- package/lib/review-core.mjs +713 -0
- package/lib/risk.mjs +370 -0
- package/lib/run-log.mjs +1004 -0
- package/lib/run-paths.mjs +239 -0
- package/lib/run.mjs +2921 -0
- package/lib/ship-stage.mjs +223 -0
- package/lib/spill.mjs +150 -0
- package/lib/surface.mjs +195 -0
- package/lib/verify.mjs +1074 -0
- package/lib/waves.mjs +2661 -0
- package/package.json +40 -0
- package/shared/CONTEXT-HYGIENE.md +84 -0
- package/shared/DECISION-LEDGER.md +141 -0
- package/shared/EXPLORE-BRIEF.md +96 -0
- package/shared/INVARIANT-SWEEP.md +57 -0
- package/shared/SESSION-SHAPES.md +261 -0
- package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
- package/shared/TEST-PROFILE.md +204 -0
- package/shared/TOOL-ECONOMY.md +155 -0
- package/skills/bootstrap/SKILL.md +226 -0
- package/skills/commit/SKILL.md +99 -0
- package/skills/dispatch/SKILL.md +78 -0
- package/skills/docs-digest/SKILL.md +109 -0
- package/skills/evals/SKILL.md +160 -0
- package/skills/explain-code/SKILL.md +42 -0
- package/skills/explain-code/commit-teachin.md +136 -0
- package/skills/explain-code/module-walkthrough.md +358 -0
- package/skills/explore/SKILL.md +159 -0
- package/skills/fix-tests/SKILL.md +120 -0
- package/skills/graph/SKILL.md +136 -0
- package/skills/manual-test-plan/SKILL.md +121 -0
- package/skills/mr/SKILL.md +170 -0
- package/skills/report/SKILL.md +121 -0
- package/skills/review-artifacts/SKILL.md +127 -0
- package/skills/review-code/REVIEW.template.md +49 -0
- package/skills/review-code/SKILL.md +156 -0
- package/skills/review-code/dimensions/architecture.md +10 -0
- package/skills/review-code/dimensions/devops.md +12 -0
- package/skills/review-code/dimensions/language.md +12 -0
- package/skills/review-code/dimensions/qa.md +12 -0
- package/skills/review-code/dimensions/security.md +12 -0
- package/skills/review-code/dimensions/technical-lead.md +19 -0
- package/skills/ship/SKILL.md +67 -0
- package/skills/spec/SKILL.md +264 -0
- package/skills/spec/continuity.md +66 -0
- package/workflows/ship.js +565 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
// The third renderer.
|
|
2
|
+
//
|
|
3
|
+
// `formatReport` renders the report object to text and `--json` emits it
|
|
4
|
+
// directly; this renders the same object to one self-contained HTML document.
|
|
5
|
+
// It is a pure function of that object — no filesystem, no corpus read, no
|
|
6
|
+
// indicator computed, nothing re-derived. That is not tidiness: it is what
|
|
7
|
+
// makes "the three surfaces cannot disagree" a test rather than an aspiration,
|
|
8
|
+
// since one object can be rendered to all three and the values compared.
|
|
9
|
+
//
|
|
10
|
+
// Three constraints run through every line below, and each one is a
|
|
11
|
+
// requirement rather than a preference:
|
|
12
|
+
//
|
|
13
|
+
// 1. **Absence renders at full weight.** An unobserved indicator gets the same
|
|
14
|
+
// type, size and position as one carrying a number, and its reason is the
|
|
15
|
+
// cell's content. Every dashboard idiom — the grey cell, the dash, the zero,
|
|
16
|
+
// the collapsed row — translates "we never measured this" into "this
|
|
17
|
+
// measured zero". On these corpora that mistranslation would be near-total.
|
|
18
|
+
// 2. **The document issues no verdict.** No threshold, target, goal line, trend
|
|
19
|
+
// arrow, pass/fail label, or colour that encodes health. Colour and weight
|
|
20
|
+
// carry structure and reading order only, so two reports with wildly
|
|
21
|
+
// different figures are indistinguishable in palette.
|
|
22
|
+
// 3. **Everything is inline.** No stylesheet, script, font, image or data
|
|
23
|
+
// source is fetched, so the document renders with no network and can be
|
|
24
|
+
// attached to an issue or handed to someone without the repo.
|
|
25
|
+
//
|
|
26
|
+
// Corpus content is untrusted input: a metrics file name and an unreadable
|
|
27
|
+
// file's reason both come off disk. Every interpolated value therefore passes
|
|
28
|
+
// through the `html` tagged template below, which escapes unless a fragment is
|
|
29
|
+
// explicitly marked `raw`. There is no second interpolation path.
|
|
30
|
+
|
|
31
|
+
// The one thing this renderer imports: the wording of an absence it shares with
|
|
32
|
+
// the text surface. Two surfaces describing the same absence in different words
|
|
33
|
+
// is how a reader comes to believe they are looking at two different facts.
|
|
34
|
+
import { NO_EVAL_RESULT } from './report.mjs'
|
|
35
|
+
|
|
36
|
+
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
37
|
+
|
|
38
|
+
const RAW = Symbol('raw html')
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mark an already-rendered fragment so the template does not escape it again.
|
|
42
|
+
*
|
|
43
|
+
* It carries a `toString` so that concatenating a marked fragment with `+`
|
|
44
|
+
* yields its markup rather than `[object Object]`. Without it the two ways of
|
|
45
|
+
* assembling a fragment — interpolation and concatenation — would disagree, and
|
|
46
|
+
* the failure mode is a silently mangled document rather than an error.
|
|
47
|
+
*/
|
|
48
|
+
function raw(value) {
|
|
49
|
+
const markup = String(value)
|
|
50
|
+
return { [RAW]: markup, toString: () => markup }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The one escape. Everything interpolated reaches the document through here. */
|
|
54
|
+
function escape(value) {
|
|
55
|
+
if (value === null || value === undefined) return ''
|
|
56
|
+
if (Array.isArray(value)) return value.map(escape).join('')
|
|
57
|
+
if (typeof value === 'object' && RAW in value) return value[RAW]
|
|
58
|
+
return String(value).replace(/[&<>"']/g, c => ESCAPES[c])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Tagged template that escapes every interpolation. `raw()` opts a fragment out. */
|
|
62
|
+
function html(strings, ...values) {
|
|
63
|
+
let out = strings[0]
|
|
64
|
+
for (let i = 0; i < values.length; i += 1) out += escape(values[i]) + strings[i + 1]
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Readings
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
// A reading is a headline and a basis. The headline is a numeral or one of two
|
|
73
|
+
// words; the basis is either the denominator or the reason there is none. The
|
|
74
|
+
// two absence words are typed exactly like a numeral — same face, same size,
|
|
75
|
+
// same weight — which is requirement 1 above expressed in markup.
|
|
76
|
+
|
|
77
|
+
const UNOBSERVED = 'UNOBSERVED'
|
|
78
|
+
const NOT_COMPUTABLE = 'NOT COMPUTABLE'
|
|
79
|
+
|
|
80
|
+
function basisOf(ind) {
|
|
81
|
+
return `of ${ind.observedOf} observed` + (ind.notObserved ? `, ${ind.notObserved} not observed` : '')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A share, formatted exactly as the text surface formats it. */
|
|
85
|
+
function pctReading(ind) {
|
|
86
|
+
if (!ind || ind.value === null) {
|
|
87
|
+
return { headline: UNOBSERVED, basis: ind ? ind.reason : 'no indicator' }
|
|
88
|
+
}
|
|
89
|
+
return { headline: `${(ind.value * 100).toFixed(1)}%`, basis: basisOf(ind) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A bare number, formatted exactly as the text surface formats it. */
|
|
93
|
+
function plainReading(ind) {
|
|
94
|
+
if (!ind || ind.value === null) {
|
|
95
|
+
return { headline: UNOBSERVED, basis: ind ? ind.reason : 'no indicator' }
|
|
96
|
+
}
|
|
97
|
+
return { headline: String(ind.value), basis: basisOf(ind) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Marks
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
const MARK_WIDTH = 260
|
|
105
|
+
const MARK_HEIGHT = 9
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A part-of-whole mark for a coverage proportion. The denominator is printed
|
|
109
|
+
* beside it in every case, so the bar is read against a stated whole rather
|
|
110
|
+
* than against an implied target — and the track is drawn at the same weight
|
|
111
|
+
* whatever the fill, so a low proportion is not styled as a deficiency.
|
|
112
|
+
*/
|
|
113
|
+
function proportionMark(numerator, denominator) {
|
|
114
|
+
const n = Number(numerator) || 0
|
|
115
|
+
const d = Number(denominator) || 0
|
|
116
|
+
const width = d > 0 ? Math.max(0, Math.min(1, n / d)) * MARK_WIDTH : 0
|
|
117
|
+
return raw(
|
|
118
|
+
html`<svg class="mark" width="${MARK_WIDTH}" height="${MARK_HEIGHT}" viewBox="0 0 ${MARK_WIDTH} ${MARK_HEIGHT}" role="presentation" aria-hidden="true">` +
|
|
119
|
+
html`<rect x="0" y="0" width="${MARK_WIDTH}" height="${MARK_HEIGHT}" class="track"/>` +
|
|
120
|
+
(width > 0 ? html`<rect x="0" y="0" width="${width}" height="${MARK_HEIGHT}" class="fill"/>` : '') +
|
|
121
|
+
'</svg>'
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The gate-exit distribution: one row per command, two bars on ONE shared count
|
|
127
|
+
* scale from a common origin, so a bar's length reads as a count and never as
|
|
128
|
+
* progress toward a full width. The scale is the largest count present; no
|
|
129
|
+
* axis, no threshold line, no marker.
|
|
130
|
+
*/
|
|
131
|
+
function exitDistribution(commands) {
|
|
132
|
+
const rows = commands.filter(c => c && Number(c.exits) >= 0)
|
|
133
|
+
if (!rows.length) return ''
|
|
134
|
+
const scale = rows.reduce((max, c) => Math.max(max, Number(c.exits) || 0), 0)
|
|
135
|
+
if (!scale) return ''
|
|
136
|
+
|
|
137
|
+
const barWidth = count => (Math.max(0, Number(count) || 0) / scale) * MARK_WIDTH
|
|
138
|
+
const body = rows
|
|
139
|
+
.map(
|
|
140
|
+
c =>
|
|
141
|
+
html`<div class="dist-row"><div class="dist-name">${c.command}</div>` +
|
|
142
|
+
html`<div class="dist-marks">` +
|
|
143
|
+
html`<svg class="mark" width="${MARK_WIDTH}" height="20" viewBox="0 0 ${MARK_WIDTH} 20" role="presentation" aria-hidden="true">` +
|
|
144
|
+
html`<rect x="0" y="0" width="${barWidth(c.exits)}" height="8" class="fill-soft"/>` +
|
|
145
|
+
html`<rect x="0" y="11" width="${barWidth(c.nonZero)}" height="8" class="fill"/>` +
|
|
146
|
+
html`</svg></div>` +
|
|
147
|
+
html`<div class="dist-figures"><span>${c.nonZero}</span> of <span>${c.exits}</span> non-zero</div></div>`
|
|
148
|
+
)
|
|
149
|
+
.join('')
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
html`<div class="dist"><div class="dist-legend">bars share one count scale, common origin — upper: exits recorded, lower: exits non-zero. Largest count drawn is ${scale}.</div>` +
|
|
153
|
+
body +
|
|
154
|
+
'</div>'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Sections
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The fourth corpus's coverage block.
|
|
164
|
+
*
|
|
165
|
+
* Rendered even when it is excluded or absent, and it says which: a corpus that
|
|
166
|
+
* silently vanished from a filtered page would be indistinguishable from one
|
|
167
|
+
* holding nothing.
|
|
168
|
+
*/
|
|
169
|
+
function evalHistoryCorpus(e) {
|
|
170
|
+
if (!e) return ''
|
|
171
|
+
const count = e.excluded
|
|
172
|
+
? raw('<span class="word">EXCLUDED</span>')
|
|
173
|
+
: e.exists
|
|
174
|
+
? raw(html`<span class="num">${e.records}</span> result(s)`)
|
|
175
|
+
: raw('<span class="word">ABSENT</span>')
|
|
176
|
+
|
|
177
|
+
const notes = []
|
|
178
|
+
if (e.excluded && e.reason) notes.push(e.reason)
|
|
179
|
+
if (!e.excluded && !e.exists) notes.push(`${e.path} does not exist — no outcome eval has recorded a result`)
|
|
180
|
+
if (!e.excluded && e.reason) notes.push(`unreadable — ${e.reason}`)
|
|
181
|
+
if (e.filteredOut) notes.push(`${e.filteredOut} outside the filter`)
|
|
182
|
+
if (e.skippedLines) notes.push(`${e.skippedLines} unreadable line(s) skipped`)
|
|
183
|
+
for (const u of e.unrecognized || []) {
|
|
184
|
+
notes.push(`line ${u.line} not counted${u.schema ? ` (schema ${u.schema})` : ' (no schema key)'}`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
html`<div class="corpus"><div class="corpus-head"><h3>Outcome-eval results</h3>
|
|
189
|
+
<div class="corpus-count">${count}</div></div>
|
|
190
|
+
<p class="reason">Fixture runs, not real work. Nothing here is counted in any figure above.</p>` +
|
|
191
|
+
(notes.length
|
|
192
|
+
? html`<ul class="named">${raw(notes.map(n => html`<li>${n}</li>`).join(''))}</ul>`
|
|
193
|
+
: '') +
|
|
194
|
+
'</div>'
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function coverageSection(coverage) {
|
|
199
|
+
const { trajectories: t, outcomes: o, metrics: m } = coverage
|
|
200
|
+
|
|
201
|
+
const trajectoryNotes = []
|
|
202
|
+
if (t.truncated) trajectoryNotes.push(`scan capped — ${t.notScanned} not read`)
|
|
203
|
+
if (t.filteredOut) trajectoryNotes.push(`${t.filteredOut} outside the filter`)
|
|
204
|
+
if (t.skippedLines) trajectoryNotes.push(`${t.skippedLines} torn line(s) skipped`)
|
|
205
|
+
if (t.reason) trajectoryNotes.push(t.reason)
|
|
206
|
+
|
|
207
|
+
const outcomeModes = Object.keys(o.byMode || {}).sort()
|
|
208
|
+
|
|
209
|
+
return html`<section class="block"><h2>Coverage — what the corpora can and cannot answer</h2>
|
|
210
|
+
<p class="lede">Every figure in the next section rests on what is scanned here. A corpus that holds
|
|
211
|
+
nothing is reported as holding nothing; it is never reported as a zero.</p>` +
|
|
212
|
+
// --- trajectories
|
|
213
|
+
html`<div class="corpus"><div class="corpus-head"><h3>Run trajectories</h3>
|
|
214
|
+
<div class="corpus-count"><span class="num">${t.scanned}</span> scanned of <span class="num">${t.total}</span> on disk</div></div>
|
|
215
|
+
${proportionMark(t.scanned, t.total)}
|
|
216
|
+
<dl class="facets">
|
|
217
|
+
<div><dt>with run-start</dt><dd class="num">${t.withRunStart}</dd></div>
|
|
218
|
+
<div><dt>with terminal</dt><dd class="num">${t.withTerminal}</dd><dd class="aside">run-complete or run-halt</dd></div>
|
|
219
|
+
<div><dt>with receipt</dt><dd class="num">${t.withReceipt}</dd><dd class="aside">every receipt-derived indicator rests on this</dd></div>
|
|
220
|
+
</dl>` +
|
|
221
|
+
(trajectoryNotes.length
|
|
222
|
+
? html`<ul class="named">${raw(trajectoryNotes.map(n => html`<li>${n}</li>`).join(''))}</ul>`
|
|
223
|
+
: '') +
|
|
224
|
+
(t.unreadable && t.unreadable.length
|
|
225
|
+
? html`<div class="named-head">Unreadable, named rather than counted</div><ul class="named">` +
|
|
226
|
+
raw(t.unreadable.map(u => html`<li><span class="file">${u.runId}</span> — ${u.reason}</li>`).join('')) +
|
|
227
|
+
'</ul>'
|
|
228
|
+
: '') +
|
|
229
|
+
'</div>' +
|
|
230
|
+
// --- outcomes
|
|
231
|
+
html`<div class="corpus"><div class="corpus-head"><h3>Outcome records</h3>
|
|
232
|
+
<div class="corpus-count">${o.exists ? raw(html`<span class="num">${o.records}</span> record(s)`) : raw('<span class="word">ABSENT</span>')}</div></div>` +
|
|
233
|
+
(o.exists
|
|
234
|
+
? html`<dl class="facets">${raw(
|
|
235
|
+
outcomeModes.map(k => html`<div><dt>${k}</dt><dd class="num">${o.byMode[k]}</dd></div>`).join('')
|
|
236
|
+
)}</dl>`
|
|
237
|
+
: html`<p class="reason">${o.path} does not exist — no ship run has recorded an outcome</p>`) +
|
|
238
|
+
(o.filteredOut ? html`<ul class="named"><li>${o.filteredOut} outside the filter</li></ul>` : '') +
|
|
239
|
+
(o.skippedLines ? html`<ul class="named"><li>${o.skippedLines} unreadable line(s) skipped</li></ul>` : '') +
|
|
240
|
+
'</div>' +
|
|
241
|
+
// --- review metrics
|
|
242
|
+
html`<div class="corpus"><div class="corpus-head"><h3>Review metrics</h3>
|
|
243
|
+
<div class="corpus-count"><span class="num">${m.recognized}</span> recognized of <span class="num">${m.total}</span> file(s)</div></div>
|
|
244
|
+
${proportionMark(m.recognized, m.total)}` +
|
|
245
|
+
(m.filteredOut ? html`<ul class="named"><li>${m.filteredOut} outside the filter</li></ul>` : '') +
|
|
246
|
+
(m.unrecognized && m.unrecognized.length
|
|
247
|
+
? html`<div class="named-head">Not counted, named rather than summarised</div><ul class="named">` +
|
|
248
|
+
raw(
|
|
249
|
+
m.unrecognized
|
|
250
|
+
.map(
|
|
251
|
+
u =>
|
|
252
|
+
html`<li><span class="file">${u.file}</span> — ${
|
|
253
|
+
u.schema ? `schema ${u.schema}` : 'no schema key'
|
|
254
|
+
}</li>`
|
|
255
|
+
)
|
|
256
|
+
.join('')
|
|
257
|
+
) +
|
|
258
|
+
'</ul>'
|
|
259
|
+
: '') +
|
|
260
|
+
(m.unreadable && m.unreadable.length
|
|
261
|
+
? html`<div class="named-head">Unreadable, named rather than counted</div><ul class="named">` +
|
|
262
|
+
raw(m.unreadable.map(u => html`<li><span class="file">${u.file}</span> — ${u.reason}</li>`).join('')) +
|
|
263
|
+
'</ul>'
|
|
264
|
+
: '') +
|
|
265
|
+
'</div>' +
|
|
266
|
+
evalHistoryCorpus(coverage.evalHistory) +
|
|
267
|
+
'</section>'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** One indicator row. `detail` is pre-rendered markup or ''. */
|
|
271
|
+
function row(no, name, definition, reading, detail) {
|
|
272
|
+
return html`<div class="ind"><div class="ind-no">${no}</div>
|
|
273
|
+
<div class="ind-name"><div class="ind-title">${name}</div><div class="ind-def">${definition}</div></div>
|
|
274
|
+
<div class="ind-reading"><div class="ind-headline">${reading.headline}</div><div class="ind-basis">${reading.basis}</div></div>
|
|
275
|
+
<div class="ind-detail">${raw(detail || '')}</div></div>`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function breakdown(entries) {
|
|
279
|
+
if (!entries.length) return ''
|
|
280
|
+
return html`<div class="breakdown">${raw(
|
|
281
|
+
entries.map(e => html`<div><span class="bk-label">${e.label}</span><span class="bk-value num">${e.value}</span></div>`).join('')
|
|
282
|
+
)}</div>`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function indicatorsSection(indicators) {
|
|
286
|
+
const i = indicators
|
|
287
|
+
const rows = []
|
|
288
|
+
|
|
289
|
+
rows.push(
|
|
290
|
+
row(
|
|
291
|
+
'01',
|
|
292
|
+
'First-pass ship',
|
|
293
|
+
'Runs that reached a receipt with no halt and no remediation round, over the runs whose receipt observed both.',
|
|
294
|
+
pctReading(i.firstPassShip.rate),
|
|
295
|
+
html`<p class="finding">${i.firstPassShip.note}</p>`
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const dist = Object.keys(i.rework.remediationRounds.distribution || {}).sort()
|
|
300
|
+
rows.push(
|
|
301
|
+
row(
|
|
302
|
+
'02',
|
|
303
|
+
'Remediation rounds per run',
|
|
304
|
+
'The mean number of remediation rounds a run recorded on its receipt.',
|
|
305
|
+
plainReading(i.rework.remediationRounds),
|
|
306
|
+
dist.length
|
|
307
|
+
? breakdown(dist.map(k => ({ label: `${k} round(s)`, value: i.rework.remediationRounds.distribution[k] })))
|
|
308
|
+
: ''
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
const changes = i.rework.attemptsPerChange.changes || []
|
|
313
|
+
rows.push(
|
|
314
|
+
row(
|
|
315
|
+
'03',
|
|
316
|
+
'Runs per change',
|
|
317
|
+
'How many runs each named change took. Runs the corpus could not attribute are excluded and reported separately.',
|
|
318
|
+
plainReading(i.rework.attemptsPerChange),
|
|
319
|
+
(i.rework.attemptsPerChange.unattributed
|
|
320
|
+
? html`<p class="finding">${i.rework.attemptsPerChange.unattributed} run(s) unattributed and excluded from this figure.</p>`
|
|
321
|
+
: '') + breakdown(changes.map(c => ({ label: c.change, value: `${c.runs} run(s)` })))
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
const statuses = Object.keys(i.planFidelity.planStatus.counts || {}).sort()
|
|
326
|
+
rows.push(
|
|
327
|
+
row(
|
|
328
|
+
'04',
|
|
329
|
+
'Plan-reuse status',
|
|
330
|
+
'Whether a run reused a stored wave plan, over the runs that recorded a status.',
|
|
331
|
+
statuses.length
|
|
332
|
+
? { headline: String(statuses.reduce((n, s) => n + i.planFidelity.planStatus.counts[s], 0)), basis: 'run(s) recorded a status' }
|
|
333
|
+
: { headline: UNOBSERVED, basis: i.planFidelity.planStatus.reason },
|
|
334
|
+
statuses.length
|
|
335
|
+
? breakdown(statuses.map(s => ({ label: s, value: i.planFidelity.planStatus.counts[s] })))
|
|
336
|
+
: ''
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
rows.push(
|
|
341
|
+
row(
|
|
342
|
+
'05',
|
|
343
|
+
'Plan revised mid-run',
|
|
344
|
+
'Runs that recorded a wave action changing the plan after the run began.',
|
|
345
|
+
pctReading(i.planFidelity.midRunRevision),
|
|
346
|
+
''
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
rows.push(
|
|
351
|
+
row(
|
|
352
|
+
'06',
|
|
353
|
+
'Committed diff matches plan',
|
|
354
|
+
'The share of paths a run committed that its executed plan predicted. The touched set is the denominator.',
|
|
355
|
+
pctReading(i.planFidelity.diffMatchesPlan),
|
|
356
|
+
breakdown([
|
|
357
|
+
{ label: 'qualifying run(s)', value: i.planFidelity.diffMatchesPlan.runs },
|
|
358
|
+
{ label: 'touched path(s) predicted', value: i.planFidelity.diffMatchesPlan.paths.matched },
|
|
359
|
+
{ label: 'touched path(s)', value: i.planFidelity.diffMatchesPlan.paths.touched },
|
|
360
|
+
// Every exclusion is shown, zero included: a reason absent from the list
|
|
361
|
+
// and a reason that excluded nobody would otherwise read the same.
|
|
362
|
+
...Object.keys(i.planFidelity.diffMatchesPlan.excluded).map(key => ({
|
|
363
|
+
label: `excluded — ${i.planFidelity.diffMatchesPlan.exclusionReasons[key]}`,
|
|
364
|
+
value: i.planFidelity.diffMatchesPlan.excluded[key]
|
|
365
|
+
}))
|
|
366
|
+
])
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
const fm = i.reviewFindings.fromMetrics
|
|
371
|
+
rows.push(
|
|
372
|
+
row(
|
|
373
|
+
'07',
|
|
374
|
+
'Findings dismissed at review',
|
|
375
|
+
'Findings the skeptics verified away, over the findings raised — from the recognized review-metrics files.',
|
|
376
|
+
pctReading(fm.dismissalShare),
|
|
377
|
+
breakdown([
|
|
378
|
+
{ label: 'metrics file(s) read', value: fm.files },
|
|
379
|
+
{ label: 'raised', value: fm.counts.raised },
|
|
380
|
+
{ label: 'dismissed', value: fm.counts.dismissed },
|
|
381
|
+
{ label: 'dropped by quality', value: fm.counts.droppedByQuality },
|
|
382
|
+
{ label: 'surviving', value: fm.counts.surviving }
|
|
383
|
+
])
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
const fr = i.reviewFindings.fromReceipts
|
|
388
|
+
rows.push(
|
|
389
|
+
row(
|
|
390
|
+
'08',
|
|
391
|
+
'Findings surviving to the verdict',
|
|
392
|
+
'Findings that survived review, over the findings raised — from run receipts.',
|
|
393
|
+
pctReading(fr.survivalShare),
|
|
394
|
+
breakdown([
|
|
395
|
+
{ label: 'receipt(s) read', value: fr.runs },
|
|
396
|
+
{ label: 'raised', value: fr.counts.raised },
|
|
397
|
+
{ label: 'surviving', value: fr.counts.surviving },
|
|
398
|
+
{ label: 'blocker(s)', value: fr.counts.blockers },
|
|
399
|
+
{ label: 'warning(s)', value: fr.counts.warnings }
|
|
400
|
+
]) + html`<p class="finding">The two series above are never summed — different writers, different runs.</p>`
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
const g = i.gateExitHealth
|
|
405
|
+
rows.push(
|
|
406
|
+
row(
|
|
407
|
+
'09',
|
|
408
|
+
'Gate exits non-zero',
|
|
409
|
+
'Command exits the trajectory recorded as non-zero, over the exits it recorded.',
|
|
410
|
+
pctReading(g.nonZeroShare),
|
|
411
|
+
exitDistribution(g.commands || [])
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
// The outcome-eval rows come last and are numbered on from the real-run
|
|
416
|
+
// indicators rather than interleaved with them, so a reader cannot pick up a
|
|
417
|
+
// fixture figure believing it says something about real work. One row per arm,
|
|
418
|
+
// each with its own denominator.
|
|
419
|
+
const s = i.shipOutcomeEval
|
|
420
|
+
if (s) {
|
|
421
|
+
if (s.excluded) {
|
|
422
|
+
rows.push(
|
|
423
|
+
row(
|
|
424
|
+
'10',
|
|
425
|
+
'Outcome evals (fixtures)',
|
|
426
|
+
'Criteria met over criteria applicable, from the committed outcome-eval history.',
|
|
427
|
+
{ headline: UNOBSERVED, basis: s.reason },
|
|
428
|
+
''
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
} else {
|
|
432
|
+
const arms = Object.keys(s.byArm).sort()
|
|
433
|
+
if (!arms.length) {
|
|
434
|
+
rows.push(
|
|
435
|
+
row(
|
|
436
|
+
'10',
|
|
437
|
+
'Outcome evals (fixtures)',
|
|
438
|
+
'Criteria met over criteria applicable, from the committed outcome-eval history.',
|
|
439
|
+
{ headline: UNOBSERVED, basis: NO_EVAL_RESULT },
|
|
440
|
+
''
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
arms.forEach((arm, index) => {
|
|
445
|
+
const a = s.byArm[arm]
|
|
446
|
+
rows.push(
|
|
447
|
+
row(
|
|
448
|
+
String(10 + index).padStart(2, '0'),
|
|
449
|
+
`Outcome evals — ${arm} arm`,
|
|
450
|
+
'Criteria met over criteria applicable, from the committed outcome-eval history. Fixture runs, never real runs.',
|
|
451
|
+
pctReading(a.criteriaMet),
|
|
452
|
+
breakdown([
|
|
453
|
+
{ label: 'result(s) recorded', value: a.rows },
|
|
454
|
+
{ label: 'not applicable to this arm', value: a.notApplicable },
|
|
455
|
+
{ label: 'unobserved, counted as not met', value: a.unobserved }
|
|
456
|
+
]) + html`<p class="finding">${s.note}</p>`
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return html`<section class="block"><h2>Indicators</h2>
|
|
464
|
+
<div class="ind-head"><div></div><div>Indicator</div><div>Reading</div><div>Detail</div></div>
|
|
465
|
+
${raw(rows.join(''))}</section>`
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Document
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
const STYLE = `
|
|
473
|
+
:root {
|
|
474
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
475
|
+
--sans: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
476
|
+
--ground: #e6eeea; --panel: #f4f9f6; --ink: #0e1c17; --ink2: #4d6a5d;
|
|
477
|
+
--rule: rgba(14,28,23,0.22); --hair: rgba(14,28,23,0.11); --mark: #0e7a56;
|
|
478
|
+
}
|
|
479
|
+
@media (prefers-color-scheme: dark) {
|
|
480
|
+
:root {
|
|
481
|
+
--ground: #08110e; --panel: #0c1613; --ink: #e4efe9; --ink2: #8aa89a;
|
|
482
|
+
--rule: rgba(228,239,233,0.20); --hair: rgba(228,239,233,0.10); --mark: #34d399;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
* { box-sizing: border-box; }
|
|
486
|
+
body { margin: 0; background: var(--ground); color: var(--ink); font-family: var(--sans);
|
|
487
|
+
font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; }
|
|
488
|
+
.sheet { max-width: 1180px; margin: 0 auto; background: var(--panel); padding: 44px 52px 38px; }
|
|
489
|
+
.num, .word { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
490
|
+
h1 { font-size: 34px; font-weight: 500; letter-spacing: -0.02em; margin: 8px 0 0; }
|
|
491
|
+
h2 { font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
|
|
492
|
+
color: var(--ink2); font-weight: 400; margin: 0 0 12px; }
|
|
493
|
+
h3 { font-size: 15px; font-weight: 500; margin: 0; }
|
|
494
|
+
.kicker { font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em;
|
|
495
|
+
text-transform: uppercase; color: var(--ink2); }
|
|
496
|
+
.head { display: flex; justify-content: space-between; align-items: flex-start; gap: 48px;
|
|
497
|
+
flex-wrap: wrap; }
|
|
498
|
+
.meta { display: grid; grid-template-columns: auto auto; gap: 3px 18px; font-family: var(--mono);
|
|
499
|
+
font-size: 11px; line-height: 1.5; padding-top: 6px; }
|
|
500
|
+
.meta .k { color: var(--ink2); }
|
|
501
|
+
.rule { height: 1px; background: var(--rule); margin: 24px 0 20px; border: 0; }
|
|
502
|
+
.intro { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; }
|
|
503
|
+
@media (max-width: 820px) { .intro { grid-template-columns: 1fr; } }
|
|
504
|
+
.intro p { margin: 0; max-width: 54ch; }
|
|
505
|
+
.legend { display: grid; gap: 6px; font-size: 12.5px; }
|
|
506
|
+
.legend > div { display: grid; grid-template-columns: 132px 1fr; gap: 14px; }
|
|
507
|
+
.legend .word { font-size: 11px; }
|
|
508
|
+
.legend span:last-child { color: var(--ink2); }
|
|
509
|
+
.block { margin-top: 34px; }
|
|
510
|
+
.lede { color: var(--ink2); max-width: 74ch; margin: 0 0 20px; }
|
|
511
|
+
.corpus { padding: 14px 0; border-top: 1px solid var(--hair); }
|
|
512
|
+
.corpus-head { display: flex; justify-content: space-between; align-items: baseline; gap: 24px; }
|
|
513
|
+
.corpus-count { font-family: var(--mono); font-size: 12px; color: var(--ink2);
|
|
514
|
+
font-variant-numeric: tabular-nums; }
|
|
515
|
+
.corpus-count .num { font-size: 15px; color: var(--ink); }
|
|
516
|
+
.facets { display: flex; flex-wrap: wrap; gap: 8px 34px; margin: 10px 0 0; }
|
|
517
|
+
.facets > div { display: flex; align-items: baseline; gap: 8px; }
|
|
518
|
+
.facets dt { font-size: 12.5px; color: var(--ink2); }
|
|
519
|
+
.facets dd { margin: 0; font-size: 14px; }
|
|
520
|
+
.facets .aside { font-size: 12px; color: var(--ink2); font-family: var(--sans); }
|
|
521
|
+
.named-head { font-family: var(--mono); font-size: 10px; letter-spacing: 0.13em;
|
|
522
|
+
text-transform: uppercase; color: var(--ink2); margin-top: 12px; }
|
|
523
|
+
.named { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12.5px; color: var(--ink2); }
|
|
524
|
+
.named li { padding: 2px 0; overflow-wrap: anywhere; }
|
|
525
|
+
.named .file { font-family: var(--mono); font-size: 11.5px; color: var(--ink); }
|
|
526
|
+
.reason { margin: 8px 0 0; font-size: 13px; color: var(--ink2); max-width: 78ch; }
|
|
527
|
+
.mark { display: block; margin: 12px 0 2px; max-width: 100%; }
|
|
528
|
+
.track { fill: var(--hair); }
|
|
529
|
+
.fill { fill: var(--mark); }
|
|
530
|
+
.fill-soft { fill: var(--rule); }
|
|
531
|
+
.ind-head, .ind { display: grid; grid-template-columns: 30px 264px 214px 1fr; gap: 24px; }
|
|
532
|
+
@media (max-width: 940px) { .ind-head { display: none; } .ind { grid-template-columns: 1fr; } }
|
|
533
|
+
.ind-head { padding: 9px 0; font-family: var(--mono); font-size: 10px; letter-spacing: 0.13em;
|
|
534
|
+
text-transform: uppercase; color: var(--ink2); border-bottom: 1px solid var(--rule); }
|
|
535
|
+
.ind { padding: 15px 0; border-bottom: 1px solid var(--hair); }
|
|
536
|
+
.ind-no { font-family: var(--mono); font-size: 11px; color: var(--ink2); padding-top: 6px; }
|
|
537
|
+
.ind-title { font-size: 14.5px; font-weight: 500; }
|
|
538
|
+
.ind-def { font-size: 12.5px; color: var(--ink2); margin-top: 4px; }
|
|
539
|
+
.ind-headline { font-family: var(--mono); font-size: 20px; font-weight: 500; line-height: 1.2;
|
|
540
|
+
font-variant-numeric: tabular-nums; }
|
|
541
|
+
.ind-basis { font-family: var(--mono); font-size: 10.5px; color: var(--ink2); margin-top: 6px;
|
|
542
|
+
line-height: 1.5; overflow-wrap: anywhere; }
|
|
543
|
+
.finding { font-size: 13px; line-height: 1.55; max-width: 64ch; margin: 0 0 8px; }
|
|
544
|
+
.breakdown { display: grid; gap: 2px; }
|
|
545
|
+
.breakdown > div { display: grid; grid-template-columns: 1fr 96px; gap: 14px; }
|
|
546
|
+
.bk-label { font-size: 13px; }
|
|
547
|
+
.bk-value { font-size: 13px; text-align: right; }
|
|
548
|
+
.requires { display: grid; grid-template-columns: 104px 1fr; gap: 12px; margin-top: 9px;
|
|
549
|
+
font-size: 12.5px; max-width: 72ch; }
|
|
550
|
+
.requires-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
|
|
551
|
+
text-transform: uppercase; color: var(--ink2); }
|
|
552
|
+
.dist { display: grid; gap: 8px; }
|
|
553
|
+
.dist-legend { font-size: 11.5px; color: var(--ink2); max-width: 62ch; }
|
|
554
|
+
.dist-row { display: grid; grid-template-columns: 1fr; gap: 2px; }
|
|
555
|
+
.dist-name { font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
|
|
556
|
+
.dist-figures { font-family: var(--mono); font-size: 11px; color: var(--ink2);
|
|
557
|
+
font-variant-numeric: tabular-nums; }
|
|
558
|
+
.dist-figures span { color: var(--ink); }
|
|
559
|
+
.foot { display: flex; justify-content: space-between; gap: 32px; flex-wrap: wrap; margin-top: 24px;
|
|
560
|
+
font-family: var(--mono); font-size: 11px; color: var(--ink2); }
|
|
561
|
+
`
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Render a `buildReport()` result to one self-contained HTML document.
|
|
565
|
+
*
|
|
566
|
+
* Pure: no I/O, no corpus read, no indicator computed or re-derived. Every
|
|
567
|
+
* figure comes off the object exactly as the text and machine-readable surfaces
|
|
568
|
+
* take it.
|
|
569
|
+
*
|
|
570
|
+
* @param {object} report a `buildReport()` result
|
|
571
|
+
* @returns {string} a complete HTML document
|
|
572
|
+
*/
|
|
573
|
+
export function renderReportHtml(report) {
|
|
574
|
+
const r = report && typeof report === 'object' ? report : {}
|
|
575
|
+
const filter = r.filter || {}
|
|
576
|
+
const filterBits = []
|
|
577
|
+
if (filter.since) filterBits.push(`since ${filter.since}`)
|
|
578
|
+
if (filter.change) filterBits.push(`change ${filter.change}`)
|
|
579
|
+
|
|
580
|
+
const body = r.coverage
|
|
581
|
+
? coverageSection(r.coverage) + (r.indicators ? indicatorsSection(r.indicators) : '')
|
|
582
|
+
: html`<section class="block"><h2>The report could not be computed</h2>
|
|
583
|
+
<p class="reason">${r.reason || 'unknown'}</p>
|
|
584
|
+
<p class="lede">Nothing is asserted about the corpora. No indicator below is rendered as a
|
|
585
|
+
zero, because none was measured.</p></section>`
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
'<!doctype html>\n' +
|
|
589
|
+
html`<html lang="en"><head><meta charset="utf-8">
|
|
590
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
591
|
+
<title>Interlock report</title>
|
|
592
|
+
<style>${raw(STYLE)}</style>
|
|
593
|
+
</head>
|
|
594
|
+
<body><main class="sheet">
|
|
595
|
+
<div class="head">
|
|
596
|
+
<div><div class="kicker">Interlock · instrument readout</div><h1>Interlock Report</h1></div>
|
|
597
|
+
<div class="meta">
|
|
598
|
+
<span class="k">ROOT</span><span>${r.root || '.'}</span>
|
|
599
|
+
<span class="k">SCHEMA</span><span>${r.schema || 'unknown'}</span>
|
|
600
|
+
<span class="k">SCAN CAP</span><span>${filter.maxRuns === undefined ? 'unstated' : filter.maxRuns} run(s)</span>
|
|
601
|
+
<span class="k">FILTER</span><span>${filterBits.length ? filterBits.join(', ') : 'none'}</span>
|
|
602
|
+
<span class="k">GATES</span><span>off — reports only</span>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
<hr class="rule">
|
|
606
|
+
<div class="intro">
|
|
607
|
+
<p>This page reports what the scanned corpora record about one development loop. It does not
|
|
608
|
+
evaluate the loop, compare it to a target, or issue a verdict. No threshold is applied to any
|
|
609
|
+
figure below.</p>
|
|
610
|
+
<div class="legend">
|
|
611
|
+
<div><span class="word">${UNOBSERVED}</span><span>The corpus was read and holds no events of this kind.</span></div>
|
|
612
|
+
<div><span class="word">${NOT_COMPUTABLE}</span><span>No scanned corpus records the quantity; it cannot be derived from what exists.</span></div>
|
|
613
|
+
<div><span></span><span>Both are findings, recorded at the same standing as a measured value.</span></div>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
${raw(body)}
|
|
617
|
+
<div class="foot"><span>${r.note || 'reports only'}</span><span>Exit status is always 0.</span></div>
|
|
618
|
+
</main></body></html>
|
|
619
|
+
`
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export default renderReportHtml
|