diff --git a/index.html b/index.html index a5f0c5b..eafc6cf 100644 --- a/index.html +++ b/index.html @@ -1147,7 +1147,7 @@

The register, stated plainly.

Signed records an outside party can rebuild
-
Of 11,432 signed records across 13 record stores we hold, written between 2026-08-05 and 2026-08-15, 3,089 of them (27.0%) cannot be checked by anyone. They are instructed, report records, and no version of the engine ever published rebuilds their signed bytes, so there is nothing to verify a signature against. This is not a version pin and it is not a gap that a reader can work around by installing something else: those records carry a signature that no counterparty, and no one here, can check. Principle 04 below says the evidence is portable or it isn't evidence. For better than a quarter of what we sign, it isn't. A further 122 resolution records are rebuildable at some published version but not at the one npm install serves; section 04 of /verify carries that. Every figure in this row is read from results/ and fails the build if the copy and the measurement disagree.
+
Of 11,432 signed records across 13 record stores we hold, written between 2026-08-05 and 2026-08-15, 3,089 of them (27.0%) cannot be checked by anyone. They are instructed, report records, and no version of the engine ever published rebuilds their signed bytes, so there is nothing to verify a signature against. This is not a version pin and it is not a gap that a reader can work around by installing something else: those records carry a signature that no counterparty, and no one here, can check. Principle 04 below says the evidence is portable or it isn't evidence. For better than a quarter of what we sign, it isn't. A second class, resolution records, was rebuildable at some published version but not at the one npm install served, for four releases. That count is now 0; section 02 of /verify carries what happened. Every figure in this row is read from results/ and fails the build if the copy and the measurement disagree.
27.0% unverifiable
diff --git a/results/engine-payload-exports.json b/results/engine-payload-exports.json index 1ee14f3..18a8cf3 100644 --- a/results/engine-payload-exports.json +++ b/results/engine-payload-exports.json @@ -19,7 +19,7 @@ "versionCount" ], "computedAgainstRegistryState": { - "npmLatest": "1.0.0-rc.12", + "npmLatest": "1.0.0-rc.21", "npmRc": "1.0.0-rc.21", "versionCount": 26 }, @@ -33,7 +33,7 @@ } }, "package": "@observer-protocol/policy-engine", - "npmLatest": "1.0.0-rc.12", + "npmLatest": "1.0.0-rc.21", "npmRc": "1.0.0-rc.21", "versionCount": 26, "withdrawals": { @@ -56,7 +56,8 @@ "npmLatestExports": [ "evaluationVerdictPayload", "lapsePayload", - "refusalPayload" + "refusalPayload", + "resolutionPayload" ], "versions": [ { diff --git a/results/signed-record-coverage.json b/results/signed-record-coverage.json index 98d6857..d05ad1f 100644 --- a/results/signed-record-coverage.json +++ b/results/signed-record-coverage.json @@ -14,7 +14,7 @@ "no reader sees. A figure that announces it is unchecked where nobody reads the", "announcement is a marker doing no work, so the announcement is now in the artifact." ], - "measuredOn": "2026-08-16", + "measuredOn": "2026-08-23", "provenance": { "corpusHalf": { "fields": [ @@ -25,7 +25,7 @@ "signedRange", "headline.neverRebuildable" ], - "measuredOn": "2026-08-16", + "measuredOn": "2026-08-23", "over": "13 store file(s) under ~/op-artifacts", "reDerivable": false, "why": "The stores are working artifacts outside this repository. CI cannot reach them, so these figures are a DATED MEASUREMENT and nothing re-confirms them. The per-file sha256 list below exists so a later run can prove it measured the same population." @@ -36,12 +36,12 @@ "headline.notRebuildableAtNpmLatest" ], "computedAgainstRegistryState": { - "npmLatest": "1.0.0-rc.12", + "npmLatest": "1.0.0-rc.21", "source": "results/engine-payload-exports.json", "thatFileMeasuredOn": "2026-08-23" }, "reDerivable": true, - "why": "These depend on which symbols npm's `latest` exports, which is public and is re-read by check-measured-figures.mjs on every run. THEY GO FALSE WHEN THE `latest` DIST-TAG MOVES, without this file being touched. If npm latest is not 1.0.0-rc.12, treat every field listed here as stale." + "why": "These depend on which symbols npm's `latest` exports, which is public and is re-read by check-measured-figures.mjs on every run. THEY GO FALSE WHEN THE `latest` DIST-TAG MOVES, without this file being touched. If npm latest is not 1.0.0-rc.21, treat every field listed here as stale." } }, "storesRoot": "~/op-artifacts", @@ -102,7 +102,7 @@ "total": 122, "rebuildRoute": "resolutionPayload", "rebuildableEver": true, - "rebuildableAtNpmLatest": false + "rebuildableAtNpmLatest": true } ], "headline": { @@ -116,12 +116,10 @@ "note": "No published version of the package rebuilds these. Not fragile, not version-pinned: unverifiable by anyone, permanently, on the evidence of every version published to date." }, "notRebuildableAtNpmLatest": { - "kinds": [ - "resolution" - ], - "count": 122, - "percentOfSigned": "1.1%", - "note": "Rebuildable at some published version but NOT at 1.0.0-rc.12, which is what npm install serves a reader today." + "kinds": [], + "count": 0, + "percentOfSigned": "0.0%", + "note": "Rebuildable at some published version but NOT at 1.0.0-rc.21, which is what npm install serves a reader today." } }, "files": [ diff --git a/scripts/measured-figures.json b/scripts/measured-figures.json index e0c3cac..fa92744 100644 --- a/scripts/measured-figures.json +++ b/scripts/measured-figures.json @@ -12,7 +12,12 @@ " - a key marked on a page and absent from here -> FAIL, an undeclared figure", "", "Adding a row is a deliberate act. Deleting one is too, and it should be argued for", - "in the commit that does it rather than happening as a side effect of an edit." + "in the commit that does it rather than happening as a side effect of an edit.", + "REMOVED 2026-08-23: signed-record-coverage:headline.notRebuildableAtNpmLatest.kinds.", + "The kinds list is now empty because the count is 0, so there is no class name to show and", + "the declaration's own reason -- 'a count with no class name cannot be acted on' -- no longer", + "has a subject. Removed in the same commit as the copy change, per the check's instruction that", + "dropping a measured claim must not be something an edit does quietly." ], "required": [ { @@ -49,7 +54,7 @@ "index.html", "verify.html" ], - "why": "The resolution records a reader on npm latest cannot check. On both pages because the register states the scale and /verify states the cause." + "why": "The resolution records a reader on npm latest cannot check. Now 0, and KEPT AT ZERO rather than dropped: a page that stops citing the figure when it clears cannot show a reader that it ever was not zero. On both pages because the register states the scale and /verify states the cause." }, { "key": "signed-record-coverage:storeFileCount", @@ -100,13 +105,6 @@ ], "why": "The claim is that latest sits INSIDE the withdrawn band. Naming the band without naming latest leaves the reader to make the comparison the page is asserting." }, - { - "key": "signed-record-coverage:headline.notRebuildableAtNpmLatest.kinds", - "mustAppearIn": [ - "index.html" - ], - "why": "Which class is affected by the withdrawn constructor. A count with no class name cannot be acted on." - }, { "key": "engine-payload-exports:withdrawals.resolutionPayload.0.lastPresentBefore", "mustAppearIn": [ diff --git a/scripts/sentences-at-risk.mjs b/scripts/sentences-at-risk.mjs new file mode 100644 index 0000000..11b3ca1 --- /dev/null +++ b/scripts/sentences-at-risk.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/** + * WHICH SENTENCES DOES A PENDING VALUE CHANGE PUT AT RISK? + * + * A `data-measured` span updates itself. The sentence around it does not. When a measured + * value changes, every span holding it moves and every sentence built on it stays — and a + * self-updating value inside a static sentence reads as FRESHLY CHECKED while being false. + * That is worse than stale, which is visible, and worse than false, which the checks catch. + * + * Measured 2026-08-23: the section 02 swap was planned as six parts and named two dependents. + * It missed two sentences. One of them — "We hold [122] signed resolution records that a + * reader on [1.0.0-rc.21] has no route to" — was INSIDE the section being swapped, and both + * of its spans updated correctly while the sentence inverted in meaning. + * + * THE POPULATION THAT WOULD HAVE CAUGHT THEM is not a list anyone maintains. It is derivable: + * every sentence containing a span whose value the change moves. This derives it. + * + * node scripts/sentences-at-risk.mjs + * + * BEFORE a change: stage or write the new results/, then run with HEAD as the baseline. The + * new values are on disk, the old ones are in the ref, and every affected sentence prints + * before anything is committed. + * + * AFTER a change: pass the ref from before it, to audit what was missed. + * + * IT DOES NOT DECIDE. Whether a sentence survives its span changing is a judgement about + * meaning, and no check makes it. This prints the sentence and the old and new values beside + * each other so the judgement is made deliberately rather than skipped. A sentence that is + * invariant is common and fine; the point is that it was looked at. + * + * Exit 0 always when it runs: it is an enumerator, not a gate. A gate that guessed at meaning + * would be the fourth way to be wrong about this. + */ +import { readFileSync, readdirSync, existsSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const baseRef = process.argv[2]; +if (!baseRef) { + console.error('usage: node scripts/sentences-at-risk.mjs '); + console.error(' e.g. HEAD, or the commit before the change you are auditing.'); + process.exit(2); +} + +const at = (ref, path) => { + try { return execFileSync('git', ['show', `${ref}:${path}`], { cwd: root, encoding: 'utf8' }); } + catch { return null; } +}; +const dig = (obj, path) => path.split('.').reduce((o, k) => { + if (o == null) return undefined; + const m = /^(.*)\[(\d+)\]$/.exec(k); + return m ? o[m[1]]?.[Number(m[2])] : o[k]; +}, obj); + +// ─── which measured values moved ──────────────────────────────────────────────────────────── +const resultsDir = join(root, 'results'); +const moved = new Map(); // "file:path" -> {before, after} +for (const f of readdirSync(resultsDir).filter((n) => n.endsWith('.json'))) { + const name = f.replace(/\.json$/, ''); + const now = JSON.parse(readFileSync(join(resultsDir, f), 'utf8')); + const oldRaw = at(baseRef, `results/${f}`); + if (oldRaw === null) { console.log(` results/${f} did not exist at ${baseRef}; every span on it is new.`); continue; } + const old = JSON.parse(oldRaw); + const walk = (a, b, path) => { + const keys = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]); + for (const k of keys) { + const pa = path ? `${path}.${k}` : k; + const va = a?.[k], vb = b?.[k]; + if (va && typeof va === 'object' && !Array.isArray(va)) { walk(va, vb, pa); continue; } + if (JSON.stringify(va) !== JSON.stringify(vb)) moved.set(`${name}:${pa}`, { before: va, after: vb }); + } + }; + walk(old, now, ''); +} + +// ─── every span on every served page, and the sentence it sits in ─────────────────────────── +const pages = []; +(function walkDir(d) { + for (const e of readdirSync(d)) { + if (e === 'dist' || e === 'node_modules' || e.startsWith('.')) continue; + const p = join(d, e); + try { if (readdirSync(p).length >= 0) { walkDir(p); continue; } } catch { /* a file */ } + if (e.endsWith('.html')) pages.push(p); + } +})(root); + +const SPAN = /]*data-measured="([^"]+)"[^>]*>([^<]*)<\/span>/g; +const prose = (h) => h.replace(/<[^>]+>/g, '').replace(/&[a-z]+;|&#\d+;/g, ' ').replace(/\s+/g, ' '); + +let found = 0; +console.log(`\nBaseline ${baseRef}. ${moved.size} measured value(s) moved.\n`); +for (const [key, d] of moved) { + const hits = []; + for (const page of pages) { + const html = readFileSync(page, 'utf8'); + SPAN.lastIndex = 0; + let m; + while ((m = SPAN.exec(html)) !== null) { + if (m[1] !== key) continue; + // the sentence: back to the previous ". " or block tag, forward to the next + const a = Math.max(0, html.lastIndexOf('. ', m.index) + 2, html.lastIndexOf('>', html.lastIndexOf(' ${JSON.stringify(d.after)}`); + for (const h of hits) console.log(` [${h.rel}] ${h.text.slice(0, 260)}`); + console.log(''); +} + +console.log(found + ? `${found} sentence(s) contain a span whose value moved. EACH NEEDS A JUDGEMENT: is it still\ntrue with the new value? This does not decide that, and nothing else does either.` + : 'No sentence on any page contains a span whose value moved.'); diff --git a/verify.html b/verify.html index c9718ee..fa779a5 100644 --- a/verify.html +++ b/verify.html @@ -352,12 +352,12 @@

There is no verifier for a PolicyEvaluationCredent
-

You cannot check a resolution record with the version this page tells you to install.

-

An approval resolution is the record of a human decision, signed at both ends: the routing and the outcome. Rebuilding its signed bytes needs resolutionPayload. The version npm install serves does not export it.

-

Read out of every published tarball rather than out of a release note: resolutionPayload was exported at 1.0.0-rc.8, withdrawn across 4 consecutive releases, 1.0.0-rc.9 through 1.0.0-rc.12, and restored at 1.0.0-rc.13. npm's latest tag sits inside that band, at 1.0.0-rc.12, which is the version this page documents and the version the install line above gives you.

-

So a reader following this page's own instructions cannot check a resolution, and until now the page did not say so. That is worse than a missing feature: the instruction was complete, it ran, and it produced a package silently missing one constructor. We hold 122 signed resolution records that a reader on 1.0.0-rc.12 has no route to.

+

A resolution record now checks with the version this page tells you to install. It did not, for four releases.

+

An approval resolution is the record of a human decision, signed at both ends: the routing and the outcome. Rebuilding its signed bytes needs resolutionPayload. The version npm install serves exports it. For four releases it did not.

+

Read out of every published tarball rather than out of a release note: resolutionPayload was exported at 1.0.0-rc.8, withdrawn across 4 consecutive releases, 1.0.0-rc.9 through 1.0.0-rc.12, and restored at 1.0.0-rc.13. npm's latest tag no longer points inside that band, at 1.0.0-rc.21, which is the version this page documents and the version the install line above gives you.

+

The 122 signed resolution records this estate holds were always rebuildable by someone who installed @rc; what they were not was rebuildable by someone who followed the instruction on this page. That was worse than a missing feature: the instruction was complete, it ran, and it produced a package silently missing one constructor. That number is now 0: every signed resolution record this estate holds is rebuildable at the version npm install serves. It was 122 for as long as latest pointed inside the band.

The withdrawal appears in no changelog. The package's own release notes record neither the removal nor the restoration, so nothing but the tarballs could have told you. That is why the figures in this paragraph are read from a measurement in results/ and compared against npm on every build, instead of being typed here where nothing could contradict them.

-

What to do about it. Installing @rc gets you a version that exports it, and that is a different package from the one this site documents, so we are not going to tell you it is the same thing. This site stays pinned to what latest serves, because a page that documents a version nobody receives is the failure this whole surface is written against. The gap closes when latest moves past the band.

+

What to do about it. Nothing. The version this page documents and the version its install line gives you both rebuild a resolution record, because npm’s latest tag now points past the withdrawal band rather than inside it. This site stays pinned to what latest serves, for the same reason it did while that was the broken version: a page that documents a version nobody receives is the failure this whole surface is written against.