Skip to content

Move to verifier-core 2.0.0-beta.1 - #31

Merged
threeqube merged 17 commits into
mainfrom
verifier-core-2.0.0-beta.1
Oct 24, 2026
Merged

threeqube merged 17 commits into
mainfrom
verifier-core-2.0.0-beta.1

Conversation

@threeqube

@threeqube threeqube commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Nate published verifier-core 2.0.0-beta.1 (built from verifier-core main e86bb79, 35 commits after our pin d217b04). This moves the plugin onto the published package and adapts to what changed. It follows verifier-core#63, #64, #65 and #67, all closed by that release.

What changes for the earner

No wording changes.

  • An expired credential now also reports its withdrawal status and its issuer. Expiry used to stop every later check (#63). A credential that is expired and deactivated now reads "deactivated", which the existing order already put first.
  • A status list someone else signed is now read like the issuer's own, following Nate's advice (see "Status lists signed by someone else" below). #64 made the library report a "deactivated" mark on such a list; it now reads "deactivated", and a clear one reads as usual instead of "couldn't check".
  • An issuer website that won't serve its key reads "couldn't reach", not "tampered", now from verifier-core itself (#65). Our namingDidWebOutages stopgap is removed. Its end-to-end tests now run the library on its own, and pass.
  • New outcome code dates_unchecked, using the existing "We couldn't check this right now" words. It applies only when the seal held but the dates check didn't report or crashed, which the library doesn't do today.

What changes in the code

  • Dates come from the new cryptographic.proof.validity-period check, not the signature check. A date counts only while the seal held, in the headline and in the Dates row. The Dates row's id is now cryptographic.proof.validity-period.
  • Registry names are read from the check's payload, not parsed out of its message, so a name containing a comma survives.
  • openBadgesSchemaSuite moves to /openbadges/schema and validates against schemas bundled with the library. The tests' copy of the schema and its plumbing are removed. Requests to purl.imsglobal.org are still refused and fail a test.
  • The verbose option and the CheckResult.check field are gone.
  • New OutcomeOptions.requireIssuerSignedStatusList (off) keeps the Move to verifier-core main, and say what its new results mean #22 rule for later. One helper, believedChecks, decides which checks the headline and the rows both act on, so they can't disagree.
  • Demo: the "Untrusted list" button is removed. Its fixture stays, and a browser test loads it directly.
  • A browser test that counted rows one tick after a re-check now counts them as the check starts, because with nothing to fetch the check finishes within a tick.

Validation (local)

  • 105,713 unit tests pass (about 10 s). The consistency suite gains the dates as a dimension, and runs every status-list combination with the setting both off and on.
  • 60 browser tests pass (1 skipped, as on main).
  • Typecheck, lint, library build and a demo-site build all pass.
  • Each new safety net was mutation-checked; the counts are in the commit messages.

Reviews

Each review was sized by what the commit touches. Every round included the tests, because outcomes.ts and the consistency suite were involved.

Commits Level Findings Outcome
ca7a544, b3bee67, abf5115 medium 9 8 fixed in 1255fc8; CheckResult.halts kept because it mirrors the library's shape
1255fc8, plus a hunt across the branch for assertions that stopped testing anything medium 10 8 fixed in f77157b. Two left unchanged: the purl unroute pattern is as on main and guarded by the preflight probe test, and halted() never branches on which check halted
f77157b medium 8 6 fixed in 14d3aac. Kept "not checked" for the Dates row behind an unheld seal rather than adding new earner text; left an old comment block where it sits on main
14d3aac medium 4 small all fixed in 4efceab
4efceab low none commit message corrected (one duplicate assertion removed, not two)
487b553 (test only) low, test hunk read none mutation: with tampering unrecognised, both tampered cases fail
46a45f7 (status lists signed by someone else) medium 7 5 fixed in 4d52dfd. Not changed: a layout test lost the longest strict-only Status value; a lookup rebuilt per helper call (speed only)
4d52dfd medium 6 led to the design change in 43cc444: an unconfirmed list signer is an unfinished check, not a stranger
43cc444 medium 5 4 fixed in 6c842d4. Not changed: "Try again" for a list read with no signer check, which the library can't produce
6c842d4 medium 3 all fixed in 1d41cea
1d41cea low none
ab12878 (consistency suite in four parts) medium, incl. tests 3 all fixed in 66ab65f: the nameless-issuer tests are split too; each part takes its number from its file name; comments that pointed at the other file
66ab65f medium, incl. tests 2 wording both fixed in the same commit
be205cd (a pause after each combination; 30 s for the nameless tests) medium, incl. tests 3 wording/number all fixed in the same commit
Final review of the whole PR (24 October 2026) medium, incl. tests, three reviewers no defects; 4 stale comments, a strict-mode limit fixed and documented in eea9e86
eea9e86 (comments only) medium, incl. tests 1 comment, which belongs to #33 fixed there; built JavaScript byte-identical

Mutation checks on the status-list commits, as measured at each commit (several nets have grown since; the final review below re-ran them): setting ignored 14,568; default dropped 14,571; rows not following the headline 14,571; strict refusing the issuer's own list 14,569; strict believing an unconfirmed list 9,716; default refusing one 9,712; the old made-up mismatch put back 2,100; a stranger's list told "Try again" 1,008; strict failing open 3.

Final review (24 October 2026)

Three independent reviewers, each with its own experiments:

  • Code: 612,360 combinations of its own, beyond the suite's: no contradiction between the headline and the rows in anything the library can produce. Every assumption about 2.0.0-beta.1 (check ids, which checks halt others, payload shapes, the list-issuer check) matches the library's code. The dependency is pinned exactly from npm; only its lockfile entry changed, and its integrity matches.
  • Tests: 35 mutations of src/outcomes.ts; all but one fail between 1 and 29,147 tests, among them a pass above an error row (10,226), a date believed behind a broken seal (15,613), a stranger's list believed in strict mode (14,574), expired-and-deactivated headlined "Expired" (562), a "couldn't check" that leans positive (21,844), and a registry named to the earner (4,204). The exception: the did:web outage's own wording isn't in the combinations, so a positive-leaning rewording of it would pass. That predates this PR; it is the first follow-up.
  • No status behind a seal that didn't hold #33 on top of this: no defects.

Follow-ups, not in this PR: the did:web and HTTP-error outcomes in the combinations; strict mode with two status lists; "couldn't load the standard to check it against", which gives the wrong reason now that the schemas are bundled (on main too); a few rare result shapes for the combinations (tried: no contradiction).

The consistency suite and Vitest's time limits

With the status-list settings the consistency suite reached 105,499 tests in one file, and CI started failing with every test passing: Vitest's "Timeout calling onTaskUpdate" (a test worker's message to the main process waits 60 s for a reply it can't read while synchronous tests run back to back). It passed at 34 s and failed twice at 70 s on #33. ab12878 and 66ab65f run the combinations from four files; be205cd adds a pause after each test, which removes that limit however long a file runs (a part slowed to 74 s passes with it and fails without), and gives the five nameless-issuer tests 30 s instead of Vitest's 5 s per test. Outside the tests only the README, one planning-doc line and one comment change: the built plugin is byte-identical. Breaking the code fails exactly as many tests as on the single file (e.g. 29,147 and 1,010).

Several seals on one credential (#67)

Tested against the real library in 487b553. The demo's verified credential with a second copy of its seal reads as tampered when either copy is broken (first or second), and holds when both are good. Not covered: seals in two different cryptosuites, where the library treats one it cannot check as a warning. That's rare for our issuers.

Status lists signed by someone else

Smoke-testing this PR showed that DCC's status service (status-list-lambda) signs every list with one ISSUER_SEED, and batch-issuer-backend signs each batch with a fresh did:key. So a batch-issued credential's list is never signed by its issuer. Since #22 the plugin refused such lists: those credentials read "We couldn't check whether this copy is still active", and with 2.0.0-beta.1 a deactivated one would have too.

Nate (22 Oct): a mismatch is a warning, not recommended practice; "display it as a warning and carry on until we get our productionized issuer capabilities set up in the next couple months". 46a45f7 and its review fixes do that:

Credential's list says Before Now
Not deactivated "We couldn't check whether this copy is still active" Reads as usual ("Verified", if nothing else is wrong)
Deactivated "We couldn't check…" "The issuer has deactivated this copy"
On hold "We couldn't check…" "The issuer has put this copy on hold"
  • No new wording for earners: the same words as an issuer-signed list. The mismatch shows in the Developer view as core's "Failed: list-issuer".
  • The strict rule is kept as OutcomeOptions.requireIssuerSignedStatusList (off), to switch back on once DCC's issuers sign their own lists. Strict mode still believes the issuer's own list, and reads an unconfirmed signer (that check errored, was skipped or is missing) as "we couldn't finish that check". One limit, documented in OutcomeOptions for whoever turns it on: the library gives one signer answer for all of a credential's lists, so where a credential has two, one the issuer's and one someone else's, strict mode reads even the issuer's own "deactivated" as "couldn't check". Cautious, not wrong.
  • What we give up meanwhile: noticing a list re-signed by someone else. For DCC credentials today that check can't tell an attack from the normal case.

Smoke tests

  • Before the status-list change: the preview (pr-31, 87aad6c = main + this PR up to 487b553) against the live demo (main, 37b6fd9), on all 13 demo credentials and 9 vc-test-fixtures links. 9 differed, all as intended: Expired, Not yet valid, Withdrawn and Suspended (and the expired fixtures) now report the issuer and "How it was built" rows instead of "not checked"; and the vc-test-fixtures revoked credentials, whose shared list is signed by did:key:z6MknNQD…, read "couldn't check", which led to the change above.
  • After it: the local dev server at 1d41cea against that preview. Exactly the 4 predicted differences: "Untrusted list" only on the preview, and didKey/noRegistry-revokedStatus-notExpired, didKey/noRegistry-revokedStatus-expired and didWeb/legacy-revokedStatus-expired now lead "The issuer has deactivated this copy".
  • Final, 24 October 2026: the live demo (main, 37b6fd9) against pr-31 (92e8165), expectations written first: exactly the predicted 10 of 22 cases differ, each in the predicted way (the four above with real issuer and "How it was built" rows; the two revoked-and-expired fixtures now leading "The issuer has deactivated this copy"; the revoked fixture's rows; two expired fixtures' Issuer rows; and "Untrusted list" only on live).

Tested in the wallet

24 October 2026. The wallet's own verification slot (src/plugins/Verifier.tsx, with its registry list), on a fresh install of lcw-front-end main from its lockfile, with this PR and #33 packaged exactly as the release workflow packages them, against today's release build:

  • The wallet's own npm run build (type check and production build) passes.
  • Its dev server and a production build give identical results, case for case, for both builds: no sign of the dev-mode verifier-core clash (the plugin bundles 2.0.0-beta.1; the wallet keeps its own 1.0.0-beta.11).
  • Every difference is one the smoke tests predicted: the demo's status cases (from the live demo's fixtures, since the built plugin rightly refuses localhost), "Untrusted list" now reading like the issuer's own list, and the vc-test-fixtures revoked credentials, whose lists someone else signed, now reading "deactivated".
  • What merging reaches: the wallet's lockfile pins today's release build (a4ca3e6), so the wallet changes when its pin is bumped, not when this merges. How the deployed wallet installs isn't in its repository, so that part is unverified.

Worth knowing

  • The bundle grows about 12% gzipped (279 → 312 KB), from the Open Badges schemas the library now bundles.
  • After merge, the wallet's verifier-plugin pin bump can take the new release build.

Open question for reviewers

The registry check now also gives the registry's own name for the issuer (entity.name), which the credential cannot alter. Should the verification box show that name instead of the credential's own when a registry matches? This PR doesn't do it, because it would change what earners read.

🤖 Generated with Claude Code

threeqube and others added 7 commits October 21, 2026 10:08
Nate's 2.0.0-beta.1 is on npm, built from verifier-core main e86bb79, so
depend on the published package rather than a commit on GitHub.

What changed upstream, and what follows here:

- Validity dates have their own check, cryptographic.proof.validity-period,
  which runs once the signature has verified. Expiry is read from it, the
  seal is just the signature passing, and the Dates row now has that id.
  Expiry and a withdrawal no longer stop the checks after them, so an
  expired credential now reports its withdrawal list and its issuer, and a
  "withdrawn" mark on a list someone else signed is caught.
- The registry check carries what it found as data. Names are read from
  it, not parsed out of the message, so a name with a comma survives.
- verifier-core#65 is fixed: an issuer whose did:web document won't load
  reads DID_WEB_UNRESOLVED. The namingDidWebOutages stopgap goes; its
  end-to-end tests now run the library unaided, and pass.
- openBadgesSchemaSuite moved to /openbadges/schema and validates against
  bundled schemas. `verbose` and CheckResult.check are gone.

No wording changes. One new outcome code, dates_unchecked, for a signature
that passed with no dates check reported: it says COULD_NOT_CHECK, so
"Verified" never sits above a Dates row reading "not checked".

The consistency suite gains the dates as a dimension (38,880 combinations).
A browser test that counted rows one tick after a re-check now counts them
as the check starts: with nothing to fetch, the check finishes in a tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verifier-core fetched the Open Badges schema from purl.imsglobal.org on
every verification until 2.0.0-beta.1, so the browser tests served a copy,
and the cross-site tests' credentials named one on the second server.
It now validates against schemas bundled with it and fetches nothing.

Removed, as support.ts said to once that happened: the copy and its
route, the second server's copy, FIXTURES_SCHEMA_URL in make-fixtures.js
and its empty override in build-site.js. The watch on purl.imsglobal.org
stays, now with no exception: any request there fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's library note, its account of the schema copy and two rows of
its file table; the inventory's note on what is on npm; and an update to
requirements §5 on why an expired credential's seal still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dates that went unchecked while the seal held now lead over the issuer
  verdicts. A dates check that throws reports CHECK_ERROR, not a date, and
  "Genuine, but we can't confirm who issued it" sat above a Dates row
  reading "not checked". The consistency suite gains the rule that would
  have caught it, and outcomes.test.ts pins the outcome.
- The halted reason names the check's id within its suite ("Not run:
  proof.signature failed"), as the library writes it; the consistency
  fixture and halted()'s note had the full id.
- The browser tests refuse requests to purl.imsglobal.org again, rather
  than leaving one to the network before failing; the cross-site tests
  drop the refusal, as before, and keep the watch.
- Three comments that described the stopgap, the schema fetch, or a
  fatal failure ending only its own suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Expiry and a start date to come count only while the seal held. Until
  2.0.0-beta.1 that came free, since the dates were judged inside the
  signature check; now an expired dates row beside an unchecked seal
  read "Expired on …" as fact.
- dates_unchecked yields to the withdrawal list's unknowns, whose words
  are more specific and say whose it is to fix, and still leads over
  who issued it.
- The seal is tested once: below that branch it held, so the repeated
  seal conditions and the unreachable signature_unchecked tail go.

Tests: the "date nothing vouched for" test now reports an expiry and
checks it isn't believed, rather than skipping the check. The
consistency suite gains a dates check that threw, a rule that a date is
reported only behind a held seal, one that an expiry the library didn't
report never appears (the end date now tested), and an allow-list for
unchecked dates. Each net was mutation-checked: 2517, 1, 226 and 9019
failures for the four regressions it guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Dates row believes the dates check only while the seal held, as the
  headline now does: behind an unheld seal it reads "not checked", "in
  date" included, as it did before 2.0.0-beta.1.
- Comments: the expired branch's seal always holds now; dates_unchecked's
  ordering note says what the withdrawal unknowns give that it doesn't.

Tests: consistency rules that the Dates row reads "not checked" behind
an unheld seal, and that no verdict but "Expired" talks of expiry (7021
and 11600 failures under mutation); unit tests for a start date and an
"in date" behind a missing seal, and for dates_unchecked yielding to a
withdrawal check that couldn't finish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expired branch's comment no longer says its reassurance can be
absent; the dates_unchecked note is re-wrapped. "No verdict but Expired
talks of expiry" now reads the action too, and matches "run out", "end
date" and "renew" as well as "expired" (caught a "renewed" action under
signature_unchecked by mutation). A duplicate assertion goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Try it: https://digitalcredentials.github.io/verifier-plugin/pr-31/

Updates on every push to this PR, and goes away when it closes. The browser tests run against this copy too.

verifier-core#67, fixed in 2.0.0-beta.1: each seal in a proof set is
checked and all must hold. The demo's verified credential, given a
second copy of its seal with one character changed, reads as tampered
whichever of the two is broken; with two good copies it holds. Making
isTampered return false fails both tampered cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
threeqube and others added 5 commits October 22, 2026 10:53
DCC's own status service (status-list-lambda) signs every list with its
own key, and the batch issuer signs each batch with a fresh did:key, so
their lists never match their credentials' issuer. Since #22 we refused
such a list, so every batch-issued credential read "We couldn't check
whether this copy is still active", and with 2.0.0-beta.1 a deactivated
one did too, instead of "deactivated".

Nate Otto, 22 October 2026: a mismatch is a warning, not recommended
practice; show it as one "until we get our productionized issuer
capabilities set up in the next couple months". So by default the list
is read like any other, with the same words, and the mismatch is the
library's warning in the Developer view. The strict rule stays behind
OutcomeOptions.requireIssuerSignedStatusList, for when DCC's issuers
sign their own lists. Meanwhile we can't notice a list re-signed by
someone else, which for these credentials the check couldn't tell from
the normal case anyway.

One switch, believedChecks: summarise and listChecks both act on the
checks less the list-issuer one unless asked, so they can't disagree.

Tests: the strict tests run with the setting on; new ones show a list
someone else signed reads exactly as the issuer's own, clear, deactivated
or on hold. The consistency suite runs those combinations both ways,
with rules that by default they read exactly as the issuer's own list
and, strict, believe nothing. Mutation: ignoring the setting fails 14568
tests, dropping the default 14571, the rows not following 14571.

Demo: "Untrusted list" goes, since it would read like "Not withdrawn";
its fixture stays, and the browser test loads it directly and checks
Verified, with "Failed: list-issuer" in the Developer view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With requireIssuerSignedStatusList, a list that was read is believed
  only when the library confirmed its signer: a list-issuer check that
  errored, was skipped or is missing now counts as someone else's, rather
  than the issuer's by default. (Off by default, and so it was before.)
- The consistency suite's lists the issuer signed now carry the passing
  list-issuer check the library reports for them, and run both ways, with
  the rule that strict reads them exactly as the default does.
- Unit tests: strict still believes the issuer's own list; believes
  nothing when the signer check errored, was skipped, or is absent.
- Comments: the Developer view shows the mismatch as the library's
  "Failed: list-issuer", not a warning; the setting is ours to flip, not
  a property of the element; one ordering note qualified.

Mutation: failing open fails 3 tests (the three written for it);
refusing the issuer's own list under strict fails 14569.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nger

Under requireIssuerSignedStatusList, a list whose signer check errored,
was skipped or is missing had a mismatch made up for it, so it read as
someone else's list: "may not be from the issuer", "let them know",
though nothing was wrong at the issuer's end and a retry may well work.
Now its withdrawal answer is set aside, as if that check hadn't
finished, which reads "we couldn't finish that check" with a Status row
of "not checked". A mismatch the library reports keeps the stranger's
wording. Nothing is added to the checks: only what the library reported.

The consistency suite gains lists whose signer check threw
(passed+errored, revoked+errored), run both ways: by default they read
as the issuer's own; strict, they settle nothing. The lists the issuer
signed are one constant. Unit tests cover a clear, deactivated and
on-hold list with an errored signer check; the "skipped" case, which the
library can't produce for a list it read, goes.

Mutation: strict believing an unconfirmed list fails 9716 tests; the
default refusing one fails 9712. Unit suite: 95,984 tests, 25s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one

The consistency suite now says why, not only that, a list is refused
under requireIssuerSignedStatusList: a stranger's list reads "may not be
from the issuer" with its Status row; a signer nobody could confirm reads
"we couldn't finish that check" with "not checked". Putting back the old
made-up mismatch for an unconfirmed signer now fails 2100 combinations.
It also gains suspended+errored, and the Rev doc says what +errored is.
The missing-signer unit test pins its wording and row, not just its code.

Not changed: "Try again" for a read list with no signer check. The
library's status suite always runs that check after a list it read, so
the case can't arise with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strict-lists rule now checks the action as well as the reason: a
stranger's list tells the earner to let the issuer know, an unconfirmed
signer to try again (giving the stranger's case "Try again" fails 1008
combinations). Its Status-row half is limited to a held seal on
purpose, and now says so: behind a broken seal that row is about the
seal (#32). A unit test covers a skipped signer check, which the docs
already said confirms nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@threeqube

Copy link
Copy Markdown
Collaborator Author

Following Nate's recommendation: a status list signed by someone other than the issuer is now read like the issuer's own, and flagged for developers

Smoke-testing this PR showed that DCC's status service (status-list-lambda) signs every list with its own key, and batch issuer signs each batch with a fresh did:key — so batch-issued credentials' lists are never signed by their issuer. Since #22 the plugin refused such lists, so those credentials read "We couldn't check whether this copy is still active", and with this PR a deactivated one would have too.

Per Nate (22 Oct: a mismatch is a warning, not recommended practice; "display it as a warning and carry on until we get our productionized issuer capabilities set up"), commit 46a45f7 and its review fixes change that:

Credential's list says Before Now
Not deactivated "We couldn't check whether this copy is still active" Reads as usual ("Verified", if nothing else is wrong)
Deactivated "We couldn't check…" (this PR, before this change) "The issuer has deactivated this copy"
On hold "We couldn't check…" "The issuer has put this copy on hold"
  • No new wording for earners — same words as an issuer-signed list. The mismatch shows in the Developer view as core's "Failed: list-issuer".
  • The strict rule is kept as OutcomeOptions.requireIssuerSignedStatusList (off), to switch back on once DCC's issuers sign their own lists. Strict mode still believes the issuer's own list, and reads an unconfirmed signer as "we couldn't finish that check".
  • Demo: "Untrusted list" is removed — it would look exactly like "Not withdrawn". Its fixture stays in the tests.
  • What we give up meanwhile: noticing a list re-signed by someone else. For DCC credentials today that check can't tell an attack from the normal case.

Checked: 105,697 unit tests (the consistency suite runs both settings), browser tests, medium reviews with tests on each commit, and a smoke comparison against the previous preview: exactly the four expected cases changed (the deactivated vc-test-fixtures credentials now read "deactivated", and the removed demo button).

@ottonomy — does this match what you meant? @jchartrand — FYI: no earner-facing wording changed, but this reverses the #22 rule.

threeqube added a commit that referenced this pull request Oct 22, 2026
Whether a credential has a status list, and which entry of it is its
own, are the credential's claims. Behind a seal that didn't hold they
vouch for nothing, yet the Status row said "the issuer set up no way to
deactivate this" for a tampered credential with no status entry, which
removing that entry would buy. It now reads "not checked", as the Dates
row does since #31.

The same holds for the headline: "deactivated" and "on hold" are
believed only behind a held seal. The library halts after a failed seal,
so it can't produce either today, but the rule must not depend on that.
notWithdrawn needs no guard: everything that reads it already stands
behind a held seal.

A test that pinned "withdrawn" behind a missing seal, to check the
action named nobody, now pins signature_unchecked and still checks that.
The consistency suite gains the rule that the Status row reads "not
checked" behind an unheld seal. Mutation: dropping the row guard fails
20782 tests, the withdrawn guard 1853, the on-hold guard 1852.

Closes #32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
threeqube and others added 3 commits October 23, 2026 10:34
Vitest fails any test file that runs longer than 60 seconds, whatever its
results ("Timeout calling onTaskUpdate"). The consistency suite grew to
105,499 tests in one file, and CI machines vary about threefold in speed:
it passed in 34 s on #31 and failed twice at 70 s on #33, every test
passing. Reproduced locally by slowing it past 60 s; two 40 s files run
back to back on one worker pass, so the limit is per file.

The combinations, their builders and the agreement rules move unchanged
to consistency-combinations.ts. consistency-1 to -4 each run every fourth
combination (26,358 each, 105,432 in all). consistency.test.ts keeps the
claim patterns, the nameless-issuer check (whole, about 10 s) and the
stopped-early cases, and gains one test that every part has its file, so
a lost file can't drop a quarter of the combinations unnoticed.

Checked: 105,698 tests (105,697 plus that one); four deliberate breaks
fail exactly as many tests as on the single file (29,147, 14,571, 1,010,
14,573); the guard fails with a part missing or one extra; the slowdown
that failed the single file at 73 s passes split, files run one at a
time (each at most 17 s). The whole run takes about 10 s, not 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Vitest also fails any one test past 5 seconds. The five nameless-issuer
  tests each looped over all 105,432 combinations, about 2 s apiece beside
  the parts; under CPU load (10 busy processes, the parts about 2.3 times
  slower) three of them timed out. They now run in each part over its
  quarter: 20 tests of about 0.45 s, all passing under that load (slowest
  0.79 s). The total goes from 105,698 to 105,713.
- Two part files could call the same part and the guard would pass, a
  quarter of the combinations silently unrun. Each part now takes its
  number from its own file name, so the files carry no number to repeat;
  a name outside consistency-1 to -4 stops its file with an error.
- Comments that pointed "below" or "above" at code now in the other file
  say which file; the README rows say where the nameless check went.

Checked: lint; 105,713 tests pass; with 10 busy processes alongside, all
pass; an extra consistency-5 fails its file and the guard; ignoring the
setting still fails 29,147 tests and the stranger told "Try again" 1,010,
as on the single file; naming a nameless issuer by its identifier fails
all 20 nameless tests, 5 in each part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In CI the four parts took 16 to 27 s each (23 October 2026), two to four
times a laptop's, as they share fewer processors: only about 2x clear of
the 60 s limit. That limit turns out not to be the file's length. A
worker fails the run, every test passing, when a message to the main
Vitest process waits 60 s for its reply to be read, and synchronous tests
never let it read one. A pause after each test (one setImmediate) lets
it, however long a file runs: one part slowed to 74 s passes with the
pause and fails without, and a 70 s probe file of 1,000 slow tests does
the same. A microtask doesn't (the reviewer checked).

The nameless-issuer tests each loop over a part's combinations and took
up to 2.0 s in CI, against Vitest's 5 s limit per test; they now have
30 s. Slowed to about 7 s each, they pass with it and fail without.

Cost: about 8 to 15% per part file locally (7.1 to 8 s, was 6.6 to 7 s),
about 1 s on the whole run. 105,713 tests pass; lint passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
threeqube added a commit that referenced this pull request Oct 24, 2026
Whether a credential has a status list, and which entry of it is its
own, are the credential's claims. Behind a seal that didn't hold they
vouch for nothing, yet the Status row said "the issuer set up no way to
deactivate this" for a tampered credential with no status entry, which
removing that entry would buy. It now reads "not checked", as the Dates
row does since #31.

The same holds for the headline: "deactivated" and "on hold" are
believed only behind a held seal. The library halts after a failed seal,
so it can't produce either today, but the rule must not depend on that.
notWithdrawn needs no guard: everything that reads it already stands
behind a held seal.

A test that pinned "withdrawn" behind a missing seal, to check the
action named nobody, now pins signature_unchecked and still checks that.
The consistency suite gains the rule that the Status row reads "not
checked" behind an unheld seal. Mutation: dropping the row guard fails
20782 tests, the withdrawn guard 1853, the on-hold guard 1852.

Closes #32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the final review of the whole branch (24 October 2026). Comments
only: the built JavaScript is byte-identical; the .d.ts files differ in
these comments.

- isWithdrawn, notWithdrawn and PROBLEM.revoked said a list someone else
  signed establishes nothing. That is now true only with
  requireIssuerSignedStatusList; by default it is read like the issuer's.
- CHECK.statusListIssuer said the check runs only for lists that loaded.
  It gives one answer for all of a credential's lists, compares only those
  whose signature it can establish, and is skipped if any couldn't be.
- OutcomeOptions gains what to know before turning the strict rule on: one
  answer for all lists means a stranger's second list sets aside the
  issuer's own "deactivated", which then reads "couldn't check". Cautious,
  not wrong; found by a probe in review.
- A consistency comment described verifier-plugin#32's fix as if it were
  already here; the README credited the split, not the pause, with the
  60-second limit.

Checked: lint, typecheck, 105,713 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
threeqube added a commit that referenced this pull request Oct 24, 2026
Whether a credential has a status list, and which entry of it is its
own, are the credential's claims. Behind a seal that didn't hold they
vouch for nothing, yet the Status row said "the issuer set up no way to
deactivate this" for a tampered credential with no status entry, which
removing that entry would buy. It now reads "not checked", as the Dates
row does since #31.

The same holds for the headline: "deactivated" and "on hold" are
believed only behind a held seal. The library halts after a failed seal,
so it can't produce either today, but the rule must not depend on that.
notWithdrawn needs no guard: everything that reads it already stands
behind a held seal.

A test that pinned "withdrawn" behind a missing seal, to check the
action named nobody, now pins signature_unchecked and still checks that.
The consistency suite gains the rule that the Status row reads "not
checked" behind an unheld seal. Mutation: dropping the row guard fails
20782 tests, the withdrawn guard 1853, the on-hold guard 1852.

Closes #32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@threeqube

Copy link
Copy Markdown
Collaborator Author
screencapture-file-private-tmp-claude-501-Users-sunnylee-repos-verifier-plugin-9874c219-e69c-481f-bd2e-2f1b488c8fdd-scratchpad-report-smoke1-index-html-2026-10-24-16_33_52

@threeqube
threeqube merged commit ea31850 into main Oct 24, 2026
6 checks passed
@threeqube
threeqube deleted the verifier-core-2.0.0-beta.1 branch October 24, 2026 23:38
github-actions Bot added a commit that referenced this pull request Oct 24, 2026
@threeqube

Copy link
Copy Markdown
Collaborator Author

Post-merge check (29 October 2026)

Checked on the live demo and the release branch at main 713b8b4, which also includes #33 and #38. The expectations were written before checking.

  • Landed: ea31850 is on main, and the live demo serves 713b8b4.
  • Release: the release branch's dist/ is byte-identical to a fresh build of 713b8b4. It and the live demo both bundle verifier-core 2.0.0-beta.1.
  • Browser tests against the live demo: 58 passed, 3 skipped (the cross-site tests, which need a local second site). That includes the test that loads the status list signed by someone else directly.
  • On the live demo:
What this PR changed Live
Expired and deactivated reads "deactivated" ✅ noRegistry-revokedStatus-expired and legacy-revokedStatus-expired: "The issuer has deactivated this copy", Dates "expired on 2 January 2010"
An issuer website that won't serve its key reads "couldn't reach", not "tampered" ✅ badDidWeb: "We couldn't check this right now. We couldn't reach the issuer's website…"
Dates behind a seal that didn't hold read "not checked" ✅ Changed, Not their seal
The schema check uses the schemas bundled with the library ✅ Built wrong: "How it was built: missing details the standard requires". Verified: "as the standard expects"
"Untrusted list" button removed ✅ 12 demo buttons
  • Compare tool: the live demo against itself shows 0 of 21 cases differing (12 demo buttons, 9 vc-test-fixtures links).

The wallet still pins an earlier build (a4ca3e6), so none of this reaches it until that pin is bumped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant