From d6d428825f313d01f63cf42ec0087bef0010b68c Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 15:47:12 -0600 Subject: [PATCH 1/7] A latest-exports predicate, so the section 02 swap is mechanical /verify section 02 says one thing while npm's latest sits inside the resolutionPayload withdrawal band and the opposite once it moves past. The marker on those sentences is latest-does-not-export. There was no predicate for the other side, so the swap on the day would have been a hand edit to a claim nobody could check until after it was made. Safe to add before the move: it is unused until a marker names it, and adding it changes no verdict. NOT DEFINED AS THE NEGATION OF THE OTHER, deliberately. latest-does-not-export returns false when the version is unmeasured, because an unevaluable claim is not true. So does this one. A predicate written as `!other` would turn that refusal into a pass in one direction, which is the failure mode the pair exists to prevent. --- scripts/check-measured-figures.mjs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/check-measured-figures.mjs b/scripts/check-measured-figures.mjs index e024284..76d29ae 100644 --- a/scripts/check-measured-figures.mjs +++ b/scripts/check-measured-figures.mjs @@ -511,6 +511,32 @@ if (engine) { // which one they are looking at. const PREDICATES = { // True when the version npm's `latest` tag serves does NOT export the named symbol. + // The affirmative of the same question. Both exist because BOTH SIDES OF A TAG MOVE NEED A + // PREDICATE: the page says one thing before `latest` moves past a withdrawal band and the + // opposite after, and a swap between two markers is mechanical where a hand edit on the day + // is not. Adding this one is safe BEFORE the move — it is unused until a marker names it. + // + // NOT THE NEGATION OF THE OTHER, deliberately. `latest-does-not-export` returns false when + // the version is unmeasured, and so does this: an unevaluable claim is not true, whichever + // direction it points, and a predicate defined as `!other` would turn one of those refusals + // into a pass. + 'latest-exports': (res, arg, fresh) => { + const version = fresh ?? res.npmLatest; + const entry = (res.versions ?? []).find((v) => v.version === version); + if (!entry) { + return { + ok: false, + why: `npm serves ${version} at latest, and results/ carries no measurement for it. ` + + `The claim cannot be evaluated, which is not the same as it being true. ` + + `Re-run: node scripts/measure-engine-payload-exports.mjs`, + }; + } + const exports_ = entry.exports ?? []; + return exports_.includes(arg) + ? { ok: true, why: `latest (${version}) exports \`${arg}\`.` } + : { ok: false, why: `the version at latest (${version}) does NOT export \`${arg}\`, so this sentence is false.` }; + }, + 'latest-does-not-export': (res, arg, fresh) => { const version = fresh ?? res.npmLatest; const entry = (res.versions ?? []).find((v) => v.version === version); From 49c48507f2bfe06b85e3ce6ef5c130251120ce98 Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 16:38:33 -0600 Subject: [PATCH 2/7] Re-measure after the dist-tag move: latest is now 1.0.0-rc.21 Committed alone, ahead of the copy change, because npmLatest is compared by several markers and mixing the two makes the copy commit unreviewable. npmLatest 1.0.0-rc.12 -> 1.0.0-rc.21 npmLatestExports now includes resolutionPayload The withdrawal band is unchanged: resolutionPayload is still absent from rc.9 through rc.12. What changed is that latest no longer points inside it. --- results/engine-payload-exports.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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": [ { From 81a4ff2663e91d302da64cc7244bf2309d94b1eb Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 16:39:55 -0600 Subject: [PATCH 3/7] Section 02 swapped: a resolution record now checks with the version the page tells you to install All six parts in one commit, because no ordering buys anything and part 4 without parts 1-3 is green and wrong. No check reads the words, so writing them together is the only thing that makes the set atomic. 1 heading "You cannot check a resolution record with the version this page tells you to install" becomes "A resolution record now checks ... It did not, for four releases." 2 consequence sentence 1 "The version npm install serves does not export it" becomes "serves exports it. For four releases it did not." 3 consequence sentence 2 the reader-cannot-check sentence becomes the record of who could and who could not: the 122 signed resolution records were always rebuildable by someone who installed @rc, and were not by someone who followed the instruction on this page. 4 marker, both sites latest-does-not-export -> latest-exports 5 "What to do about it" it told a reader to accept the gap and said the gap closes when latest moves past the band. It has. The paragraph now says nothing needs doing and why the page stays pinned to latest regardless. 6 npmLatest spans 1.0.0-rc.12 -> 1.0.0-rc.21, both THE FOUR-RELEASE GAP IS RECORDED, NOT DELETED. The section stays because a section that vanishes when a defect clears leaves a reader unable to tell a page that was always right from a page that was corrected. resolutionPayload was still absent from rc.9 through rc.12 and the withdrawal still appears in no changelog; what changed is which release a reader receives. Verified in the INDEX before committing, per CONVENTIONS 18: all six present in the staged bytes, the old heading gone, and zero occurrences of the old marker or the old span value. --- verify.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/verify.html b/verify.html index c9718ee..6454ac7 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 sits 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 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.21 has no route to.

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.

From b2c5c910fad6dbfcc91db3c36aa10798be4f75de Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 16:44:37 -0600 Subject: [PATCH 4/7] The six parts were not all of them: two sentences and a figure declaration The held plan enumerated six parts and named the two dependents that must change with them. IT MISSED TWO SENTENCES, ON TWO PAGES, AND ONE OF THEM WAS INSIDE THE SECTION BEING SWAPPED. verify.html "We hold 122 signed resolution records that a reader on 1.0.0-rc.21 has no route to." That is the failure the whole mechanism was built to prevent, in the one place it was supposed to be watched: the data-measured SPAN inside the sentence updated itself to rc.21 while the SENTENCE inverted in meaning and did not. Part 6 moved the span. Nothing moved the words around it. index.html "A further 122 resolution records are rebuildable at some published version but not at the one npm install serves." Both rewritten. The count is now 0 and both pages say so in the past tense, with the four-release history kept rather than deleted. RE-MEASURED signed-record-coverage against the stores, which are reachable: resolution.rebuildableAtNpmLatest false -> true notRebuildableAtNpmLatest kinds [resolution] count 122 -> kinds [] count 0 neverRebuildable UNCHANGED, instructed and report, 3089, 27.0%. No route at ANY version, so the tag move cannot touch it. AND THE CHECK CAUGHT ME DROPPING A DECLARED FIGURE. Rewriting index.html removed the kinds span, and check-measured-figures refused: "Dropping a measured claim must not be something an edit does quietly." Correct. 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 this commit with that reason recorded, which is what the check's message asks for. The count declaration is KEPT AT ZERO rather than dropped: a page that stops citing a figure when it clears cannot show a reader that it ever was not zero. --- index.html | 2 +- results/signed-record-coverage.json | 20 +++++++++----------- scripts/measured-figures.json | 16 +++++++--------- verify.html | 2 +- 4 files changed, 18 insertions(+), 22 deletions(-) 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/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/verify.html b/verify.html index 6454ac7..3365ac6 100644 --- a/verify.html +++ b/verify.html @@ -355,7 +355,7 @@

There is no verifier for a PolicyEvaluationCredent

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 sits 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 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.21 has no route to.

+

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. 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.

From b3752fec019ad1579287b9f25cbf26aea7037304 Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 16:51:00 -0600 Subject: [PATCH 5/7] A third missed sentence, and a malformed tag that has been live since #85 BOTH FOUND BY THE NEW INSTRUMENT, not by reading. sentences-at-risk.mjs, run against the pre-move baseline, enumerated three sentences containing a span whose value the tag move changed. Two were the ones already fixed. The third was live on this branch and false: "npm's latest tag SITS INSIDE THAT BAND, at 1.0.0-rc.21" rc.21 is not inside rc.9 through rc.12. The span updated itself to rc.21 and the sentence around it did not, which is the exact class this branch already corrected twice and missed a third time. Now reads "no longer points inside that band". THE INSTRUMENT FOUND IN ONE RUN WHAT THREE READINGS OF THE SAME SECTION DID NOT. AND A MALFORMED TAG, ON MASTER, SINCE #85:

The closing angle bracket is missing. #85 added the marker by replacing '

' with '

'. The sibling

marker was added by a different substitution and is well formed, which is why one is broken and one is not. It has been on the served page since #85 merged. NOTHING CAUGHT IT: no check in this repository parses HTML, so a broken tag is invisible to all fourteen. A browser recovers from it, which is why it is invisible to a reader too. Found only because the sentence beside it was being read for a different reason. That is not a method. --- scripts/sentences-at-risk.mjs | 119 ++++++++++++++++++++++++++++++++++ verify.html | 4 +- 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 scripts/sentences-at-risk.mjs 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 3365ac6..fa779a5 100644 --- a/verify.html +++ b/verify.html @@ -354,8 +354,8 @@

There is no verifier for a PolicyEvaluationCredent

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 sits 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.

+

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. 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.

From c4dafb22c8af4e75bc2a25a6426b418aaf1dcecc Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 17:52:40 -0600 Subject: [PATCH 6/7] The pin moves to rc.21, and the four Group 2 expectations move with it ONE COMMIT, because they cannot land separately. Measured, not assumed: with the expectations changed and the lockfile still at rc.12, verify-published-credentials returns 1, because rc.12 cannot verify the four. An expectation is a statement about the version being run, so it moves when the version does. THE FOUR verify-samples/ppp-verdict-released.json verify-samples/ppp-verdict-denied-ceiling.json verify-samples/ppp-probe-a-verdict-release-above-escalation.json verify-samples/ppp-probe-b-verdict-release-above-ceiling.json before allow: false reasonContains "(op.evaluation.verdict.v3); the record states construction op.evaluation.verdict.v4" knownIssue VERDICT-PAYLOAD-VERSION-PIN after allow: true reasonContains "signature verifies over the payload this build rebuilds (op.evaluation.verdict.v4)" knownIssue removed THE EXPECTATIONS CHANGED BECAUSE THE VERSION AT latest CHANGED, NOT BECAUSE THE ENGINE CHANGED. The records are signed under op.evaluation.verdict.v4 and were always sound. rc.12 rebuilds v3 and cannot verify them; rc.21 rebuilds v4 and does. Nothing about the artifacts moved. The reasonContains strings named both version tokens deliberately so the pin could not move in either direction without turning this check red. It moved, the check went red, and this is the answer to it: the control working, not a finding about the engine. POPULATION SEARCHED: every tracked file with dist/ excluded, grepped for the package name adjacent to a semver, plus every data-engine-version="current" span in tracked HTML. scripts/package.json 1.0.0-rc.12 -> 1.0.0-rc.21 scripts/package-lock.json 1.0.0-rc.12 -> 1.0.0-rc.21 eight "current" spans verify.html 3, index.html 2, docs.html 2, check.html 1 EIGHT, NOT THE FIVE THE EARLIER SURVEY FOUND. That survey excluded verify.html's three as parked; they are on master now, so the population grew by exactly those three. The difference is not a correction to the earlier count, it is the same method over a population that changed underneath it. A NINTH THING, WHICH NO INSTRUMENT FOUND. verify-samples/README.md said "What the pin does, and what it does not: scripts/package.json still pins 1.0.0-rc.12 ... and credential-expectations.json still records the four allow:false outcomes." This commit falsifies that paragraph. Rewritten here. sentences-at-risk.mjs was run before committing and found NOTHING, correctly and uselessly: it keys off results/ values and this change moves a pin and eight spans, touching no results/ file. The population it derives is not the population this change touches, and the README paragraph is in Markdown, which it does not read at all. Its scope is one class of change, and this is a different one. --- check.html | 2 +- docs.html | 4 ++-- index.html | 4 ++-- scripts/credential-expectations.json | 22 +++++++++------------- scripts/package-lock.json | 8 ++++---- scripts/package.json | 2 +- verify-samples/README.md | 11 ++++++----- verify.html | 6 +++--- 8 files changed, 28 insertions(+), 31 deletions(-) diff --git a/check.html b/check.html index 339da96..02af63e 100644 --- a/check.html +++ b/check.html @@ -214,7 +214,7 @@

What this runs

signed payload is rebuilt from the record's own fields, under the construction the record itself states rather than whichever version this page ships, and the signature is checked against the key the record names.

Both are the constructions @observer-protocol/policy-engine exports at - 1.0.0-rc.12, the version npm's latest tag serves a reader today. + 1.0.0-rc.21, the version npm's latest tag serves a reader today. This page loads nothing, so it cannot import the package; it carries its own copy of those constructions and CI asserts, on every build, that the bytes it produces are identical to the package's own over every refusal this repository publishes. A divergence turns the build red rather than turning a verdict here wrong. diff --git a/docs.html b/docs.html index 1253795..59f8df9 100644 --- a/docs.html +++ b/docs.html @@ -218,7 +218,7 @@

Choose your path.

📦
-
NPM INSTALL · v1.0.0-rc.12
+
NPM INSTALL · v1.0.0-rc.21
Verify a credential
npm install @observer-protocol/policy-engine
Verify a delegation credential yourself — issuer, structure, validity window, eddsa-jcs-2022 proof against the issuer DID document, and revocation. No API key, no token, no Observer Protocol API in the path. It fetches the issuer's DID document and the revocation status list over ordinary HTTPS from the origins the credential names; point offline.didDocumentPath at a local copy and it makes no network call at all. The hosted verifier is a separate deployment running a different engine version; see the SDK section.
@@ -447,7 +447,7 @@

@observer-protocol/policy-engine

unauthenticated POST /v1/verify returns 200 with a signed result.

It is a different build, though. The hosted service reports - engine.running: "0.3.3"; the package above is 1.0.0-rc.12. Re-measured against + engine.running: "0.3.3"; the package above is 1.0.0-rc.21. Re-measured against rc.10 on 9 August 2026: they agree on 7 of the 8 artifacts this site publishes. The eighth is never evaluated by the hosted engine at all — it is refused at that deployment's issuer allowlist, which does not carry the testbed issuer. So the agreement is on samples rather diff --git a/index.html b/index.html index a5f0c5b..d9f62af 100644 --- a/index.html +++ b/index.html @@ -983,7 +983,7 @@

It fails closed, and it will refuse us too. onUnreachable: 'cache-then-deny' is the only accepted value: if the revocation list cannot be fetched, a cached answer is used and then the credential is denied. A status list hosted on an origin other than the pinned issuer's is refused until you allowlist it, and Observer Protocol's own clause-zero revocation demonstration is exactly such a pair, so it does not verify out of the box. That limit is published in the package.

@@ -1105,7 +1105,7 @@

The register, stated plainly.

Offline verification with nothing from us
-
@observer-protocol/policy-engine 1.0.0-rc.12 on npm, MIT. Section 05 is a transcript of it running against a credential served from this domain.
+
@observer-protocol/policy-engine 1.0.0-rc.21 on npm, MIT. Section 05 is a transcript of it running against a credential served from this domain.
Live
diff --git a/scripts/credential-expectations.json b/scripts/credential-expectations.json index a650e31..fd8a686 100644 --- a/scripts/credential-expectations.json +++ b/scripts/credential-expectations.json @@ -26,7 +26,7 @@ "engine": "@observer-protocol/policy-engine", "$reMeasured": "All 8 expectations re-confirmed against 1.0.0-rc.10 on 2026-08-09, BEFORE the lockfile bump landed rather than after. Output was byte-identical to rc.6, allow verdicts and reasonContains both. The bump happened because the site documented rc.6 while npm's latest dist-tag served rc.10: sync-engine-version compared the site to the lockfile and called internal consistency currency. Separately re-ran the hosted-service comparison at rc.10, because rewriting a 'current' marker inside a sentence that states a MEASURED agreement would have restated an untested pair: 7 of 8 agree, and the 8th is refused at the hosted deployment's issuer allowlist so its engine never sees it. Prior note, kept: all 8 were re-confirmed against rc.6 on 2026-08-08 after it superseded rc.5 mid-pass.", "$pppCorpus": "The 8 verify-samples/ppp-* artifacts were added 2026-08-16 and measured against 1.0.0-rc.12 on that date. They are store records copied verbatim from a run of op-ppp-cases against a local enforcement deployment on 2026-08-15; the JSON is re-indented and nothing else is touched, which is safe because every one of these signatures is over bytes the verifier REBUILDS rather than over the file. Their expectations were not chosen: each reason string below was copied out of a failing run of this checker, which is why four of them record a failure. See verify-samples/README.md for the case behind each.", - "$pppVerdictsFailOnTheVersionPIN": "THE FOUR op.evaluation.verdict EXPECTATIONS BELOW SAY allow:false AND THE RECORDS ARE SOUND. Measured 2026-08-16 against these exact eight files at both published versions: rc.12 (npm `latest`, what this repository pins and what a reader following this site's install instruction gets) rebuilds op.evaluation.verdict.v3 and the four signatures do not verify; rc.18 (npm `rc`) rebuilds v4 and all four verify. The records state v4 in their own `construction` field and nothing in either package consults it. NOT FIXED BY BUMPING THE PIN: sync-engine-version.mjs --check asserts the site states what npm serves a READER, and rc.18 is not on `latest`, so bumping would make this file agree with an engine the site does not tell anyone to install. The reasonContains strings name BOTH versions deliberately, so the pin cannot move in either direction without turning this check red. Contrast the two op.enforcement.refusal entries, which verify under both: signableFromRefusal rebuilds under the payloadType the RECORD carries. One design decision, applied to one signed class and not the other.", + "$pppVerdictsFailOnTheVersionPIN": "RESOLVED 2026-08-23 by the dist-tag move, not by a fix. These four op.evaluation.verdict expectations said allow:false for as long as npm's `latest` was 1.0.0-rc.12, which rebuilds op.evaluation.verdict.v3 while the records state v4. The pin is now 1.0.0-rc.21, which rebuilds v4, so all four verify. THE RECORDS DID NOT CHANGE AND WERE ALWAYS SOUND; what changed is which release this repository pins. The reasonContains strings named both version tokens deliberately so the pin could not move in either direction without turning this check red. It did, and that is the control working rather than a finding about the engine. Kept rather than deleted: a note that vanishes when a condition clears leaves a reader unable to tell a repository that was always right from one that was corrected. The contrast that was always the point is unchanged: the two op.enforcement.refusal entries verify under BOTH versions, because signableFromRefusal rebuilds under the payloadType the RECORD carries and evaluationVerdictPayload has no equivalent. One design decision, applied to one signed class and not the other.", "measuredOn": "2026-08-09", "artifacts": [ { @@ -124,20 +124,18 @@ "path": "verify-samples/ppp-verdict-released.json", "expect": { "construction": "op.evaluation.verdict", - "allow": false, - "reasonContains": "(op.evaluation.verdict.v3); the record states construction op.evaluation.verdict.v4" + "allow": true, + "reasonContains": "signature verifies over the payload this build rebuilds (op.evaluation.verdict.v4)" }, - "knownIssue": "VERDICT-PAYLOAD-VERSION-PIN", "case": "BE-0001 under 85 FR 20811, $105,922.00. Payroll exactly 3/4 of principal, so the 25% non-payroll cap is exactly at its boundary and does not bind; all three terms equal the principal. Submitted 2026-08-15T19:11:31.955Z, third attempt, 200 instructed as res-0001." }, { "path": "verify-samples/ppp-verdict-denied-ceiling.json", "expect": { "construction": "op.evaluation.verdict", - "allow": false, - "reasonContains": "(op.evaluation.verdict.v3); the record states construction op.evaluation.verdict.v4" + "allow": true, + "reasonContains": "signature verifies over the payload this build rebuilds (op.evaluation.verdict.v4)" }, - "knownIssue": "VERDICT-PAYLOAD-VERSION-PIN", "case": "ID-0077 under 85 FR 20811, $1,156,884.63, principal binds under both rules. Above the mandate's 700,000 USDC per-transaction ceiling, so the verdict is a deny naming per_transaction_ceiling. Submitted 2026-08-15T19:10:06.718Z, second attempt, 422 CEILING_EXCEEDED." }, { @@ -153,10 +151,9 @@ "path": "verify-samples/ppp-probe-a-verdict-release-above-escalation.json", "expect": { "construction": "op.evaluation.verdict", - "allow": false, - "reasonContains": "(op.evaluation.verdict.v3); the record states construction op.evaluation.verdict.v4" + "allow": true, + "reasonContains": "signature verifies over the payload this build rebuilds (op.evaluation.verdict.v4)" }, - "knownIssue": "VERDICT-PAYLOAD-VERSION-PIN", "case": "PROBE A. DV-0004 under 85 FR 20811, $500,589.98, chosen as the corpus payment nearest the midpoint of the escalation band so neither threshold can explain the result. A deliberately non-compliant verdict: decision release at an amount above the mandate's displayed 300,000 USDC escalation threshold. Pre-registered before it was sent." }, { @@ -173,10 +170,9 @@ "path": "verify-samples/ppp-probe-b-verdict-release-above-ceiling.json", "expect": { "construction": "op.evaluation.verdict", - "allow": false, - "reasonContains": "(op.evaluation.verdict.v3); the record states construction op.evaluation.verdict.v4" + "allow": true, + "reasonContains": "signature verifies over the payload this build rebuilds (op.evaluation.verdict.v4)" }, - "knownIssue": "VERDICT-PAYLOAD-VERSION-PIN", "case": "PROBE B. BO-0190 under 85 FR 36308, $2,339,635.21, 3.3x the mandate's ceiling. A deliberately non-compliant verdict: decision release at an amount the mandate cannot permit. Being a release it carries no breachedConstraint and no denialDetail, so per_transaction_ceiling occurs nowhere in the request." }, { diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 4ef64af..6778d21 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -6,13 +6,13 @@ "": { "name": "op-website-credential-check", "dependencies": { - "@observer-protocol/policy-engine": "1.0.0-rc.12" + "@observer-protocol/policy-engine": "1.0.0-rc.21" } }, "node_modules/@observer-protocol/policy-engine": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@observer-protocol/policy-engine/-/policy-engine-1.0.0-rc.12.tgz", - "integrity": "sha512-dm6lheerAZQESwmnGbfeKLoEkAsti9IZ9bzjRZLwnv2wSJpfT3Yfc9T0YRHBQngmQrIhB6JmyHmSpFe1vtUREQ==", + "version": "1.0.0-rc.21", + "resolved": "https://registry.npmjs.org/@observer-protocol/policy-engine/-/policy-engine-1.0.0-rc.21.tgz", + "integrity": "sha512-OFFRtmDMs+DWW1pyAWWwRcoXHPPMp/8y48NZ0xyzDKjy310PqFb8S7NPGoW9H7gUWGAm2tEROE3fZq2Y2gOKbA==", "license": "MIT", "engines": { "node": ">=18" diff --git a/scripts/package.json b/scripts/package.json index 56bdda3..d8770ec 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -7,6 +7,6 @@ "verify:credentials": "node ./verify-published-credentials.mjs" }, "dependencies": { - "@observer-protocol/policy-engine": "1.0.0-rc.12" + "@observer-protocol/policy-engine": "1.0.0-rc.21" } } diff --git a/verify-samples/README.md b/verify-samples/README.md index f6d6568..7c22f03 100644 --- a/verify-samples/README.md +++ b/verify-samples/README.md @@ -156,11 +156,12 @@ So the correct description of these artifacts is **recoverable by trial across a not fragile. A record whose recovery required guessing an unbounded string, or a nonce, or a timestamp, would be fragile. This one requires counting. -**What the pin does, and what it does not.** `scripts/package.json` still pins `1.0.0-rc.12`, so CI -measures this corpus against rc.12 and `scripts/credential-expectations.json` still records the four -`allow: false` outcomes. **That file and this page describe different versions on purpose until the -pin is bumped**, and the expectation strings name both version tokens so the pin cannot move in -either direction without turning the check red. Bumping it is a separate change from this one. +**What the pin does.** `scripts/package.json` now pins `1.0.0-rc.21`, the version npm's `latest` +serves, and `scripts/credential-expectations.json` records the four verdicts as `allow: true`. +Both moved in the same commit as the pin, because rc.12 cannot verify these four and an +expectation is a statement about the version being run. The expectation strings named both +version tokens deliberately, so the pin could not move in either direction without turning the +check red; it did, and that is what brought the two changes together. **Why it is not in the filenames.** A file called `must-not-verify-*` is making a permanent claim. diff --git a/verify.html b/verify.html index c9718ee..c0ac1be 100644 --- a/verify.html +++ b/verify.html @@ -120,7 +120,7 @@

Install, one call, done.

-
verify.mjs@observer-protocol/policy-engine 1.0.0-rc.12 · MIT
+
verify.mjs@observer-protocol/policy-engine 1.0.0-rc.21 · MIT
$ npm install @observer-protocol/policy-engine
 $ curl -O https://observerprotocol.org/verify-samples/verifies-delegation-mandate.json
 
@@ -501,7 +501,7 @@ 

Convenient, ours, and now the same engine two releases back.

Correction: the two verifiers converged, and this section said otherwise for a week

This section used to argue that the hosted service was a different build. It reported engine.running: "0.3.3" and concluded that any agreement between the two was "agreement observed on samples, not a shared code path". That argument is false and has been since 9 August 2026. Convergence landed that day, in commit d3278efb3cb2, built 2026-08-09, and nothing here recorded it. The page went on making an argument its own subject had stopped supporting.

-

Read from the service's own /version today: engine.running 1.0.0-rc.10, engine.builtAgainst 1.0.0-rc.10, agree: true. It is the published package. The published package is 1.0.0-rc.12, so the honest description is the same engine, two releases behind, and not a separate implementation. What remains true is that a version gap is a real gap: rc.10 and rc.12 are not the same code, and a difference between them is a difference you can meet.

+

Read from the service's own /version today: engine.running 1.0.0-rc.10, engine.builtAgainst 1.0.0-rc.10, agree: true. It is the published package. The published package is 1.0.0-rc.21, so the honest description is the same engine, two releases behind, and not a separate implementation. What remains true is that a version gap is a real gap: rc.10 and rc.12 are not the same code, and a difference between them is a difference you can meet.

The old "agree on 7 of 8" figure is withdrawn, and the denominator was the defect rather than the agreement. Three of those eight artifacts never reach the hosted engine at all, so a ratio over eight was never reproducible by a reader. One is the testbed-issuer demonstration credential, and that deployment refuses it at its issuer allowlist, so the refusal is evidence about the allowlist and not about the engine. The other two are the PolicyEvaluationCredential artifacts, for the reason in the next paragraph. A figure whose denominator counts artifacts that cannot be submitted is not a measurement of agreement; it is a measurement of how many things we tried.

A PolicyEvaluationCredential cannot be submitted here at all. Post one as the request body and the service answers HTTP 400, body must carry agentDid (string) and mandate (object). There is no agentDid to supply: a PEC's credentialSubject carries decision, evaluator, proposal and evaluatedAt, and no id. The endpoint's shape assumes a delegation credential granting authority to a subject, and an evaluation credential has no subject in that sense. This is the same root cause as the gap in section 02, met at the transport rather than in the validator: both the package and the service are built around one credential shape, and the artifact that records a determination does not have it. Wrapping a PEC in an invented agentDid does get a response, and what comes back is a structural refusal about credentialSchema, which is the offline failure repeated rather than a second opinion about it.

The service is also candid about a second limit, in its own /version response: the schema and issuer allowlists it enforces come from environment variables on the host, inRepo: false, so a change to what it accepts "leaves no reviewable record". The offline path has no such property, because the allowlist is the one you wrote.

@@ -544,7 +544,7 @@

Checking a record is not approving a vendor.

From a095b133d5148bd3d0aa2f1d733b927776759e9f Mon Sep 17 00:00:00 2001 From: Boyd Cohen Date: Sun, 23 Aug 2026 17:53:02 -0600 Subject: [PATCH 7/7] Regenerate the sitemap for the four pages the pin bump touched verify.html, index.html, docs.html and check.html all moved their git date with the version spans. make-sitemap.py is one commit behind by construction, so this follows rather than rides with them. Caught by tools/hooks/pre-push, which refused the push. First time that hook has stopped a real one rather than a probe. --- sitemap.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sitemap.xml b/sitemap.xml index c599e3a..3de80e7 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -22,7 +22,7 @@ https://observerprotocol.org/check - 2026-08-20 + 2026-08-23 https://observerprotocol.org/crossrail @@ -34,7 +34,7 @@ https://observerprotocol.org/docs.html - 2026-08-22 + 2026-08-23 https://observerprotocol.org/enforcement