Skip to content

Release 0.3.0: fail-closed evidence, verifier and CLI fixes, version gate and release workflow - #69

Merged
EauDoon merged 19 commits into
mainfrom
upgrade/v0.3.0
Oct 9, 2026
Merged

EauDoon merged 19 commits into
mainfrom
upgrade/v0.3.0

Conversation

@EauDoon

@EauDoon EauDoon commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Release 0.3.0 of the Consequence Rail reference implementation: 19 commits that fix the lifecycle, verifier, HTTP and CLI defects found in review, make the version single-sourced and checked, and add the release engineering the project never had.

  • Lifecycle fail-closed fixes. Evidence that omits a postcondition fact, or that becomes too large to sign once evaluated, used to strand the action in VERIFYING / REMEDY_VERIFYING with no receipt and recourse still reserved. It now closes disputed (EVIDENCE_FACT_MISSING, EVIDENCE_UNREPRESENTABLE) and starts no remedy. A breach under an escalate reservation now closes disputed via REVIEW_REQUIRED instead of hanging in REMEDY_DUE. A compound eq value, which strict equality can never match, is refused at both proposal boundaries.
  • Verifier. Integrity verification binds the receipt outcome and postcondition result to the terminal CLOSED event for every profile. For receipt-profile bundles it also binds closed_at to no earlier than that event and at most 1000 ms after it, so receipts from 0.2.20 and earlier, whose closed_at came from a second clock read, still verify. A re-signed receipt claiming settled over a SETTLEMENT_DISPUTED chain is now BUNDLE_TAMPERED.
  • HTTP and CLI.
    • OpenAPI declares the 422s five operations return.
    • execute accepts only fault.
    • crctl accepts dash-leading digest pins and --name=value.
    • A malformed --at is USAGE_INVALID, not an invalid artifact.
    • rail parses ports strictly, and its help no longer ends in a stray quote.
  • Version and release.
    • package.json is the single version source. --version is on both CLIs and implementation_version is in the capability document.
    • scripts/check.js gates version consistency in every CI job, and in tag builds it also checks the tag.
    • CHANGELOG now follows Keep a Changelog, with a heading for every shipped version.
    • A tag-gated release workflow publishes GitHub Releases.
    • Dependabot and CodeQL are added.
  • Tests and docs.
    • The suite passes from any working directory.
    • README examples are executed by a test.
    • Stale claims in SECURITY.md and architecture docs are corrected.

Authentic artifacts written by 0.2.20 and earlier still verify, with one deliberate exception: an audit bundle whose proposal carries a compound eq value (array or object), which strict equality could never match, is now BUNDLE_TAMPERED offline, as the CHANGELOG states. An earlier head of this PR also rejected about one in eight authentic receipt-profile exports from the 0.2.20 sidecar. Those receipts took closed_at from a second clock read after the CLOSED event and can trail it by a millisecond, and the PR head required exact equality. 7c55f3a fixes that with a bounded window and a regression test. No schema_version, signature or canonical-byte change for artifacts this repository produces. The pinned default artifact hashes (test/rail.bundle.test.js) pass unchanged, and inspect(), bundles, receipts and events gain no fields. Protocol identifiers are unchanged: schema versions, well-known protocol_version: v0.1 and the signing seed labels.

Review findings addressed

ID Severity Finding Commit
F1 high Failure evaluating or signing accepted evidence strands the action with no receipt and active recourse a07958c
F2 medium eq clauses with array/object values can never be satisfied, so a correct action is breached and remedied 7f8d03f
F3 medium crctl cannot pin a digest starting with -, and has no --flag=value form c8e7018
F4 medium Five HTTP operations return 422 that OpenAPI does not declare 2f8cd2a
F5 low HTTP execute accepts the undeclared library-only proposalOverride 2f8cd2a
F6 medium A malformed --at is blamed on the artifact; verify-many marks every drill invalid 86f1d1a
F7 low rail --help ends with a stray quote and no trailing newline c8e7018
F8 low Sidecar port parsing accepts hex, exponent, padded and fractional forms c8e7018
F9 medium Neither CLI nor the sidecar can report the implementation version 2dd4a31, d099cbd
F10 medium Version sources disagree, CHANGELOG cannot be mapped to versions, nothing checks consistency b172478, c193b1a, d099cbd
F11 medium No release engineering: no tag workflow, no GitHub Releases, no documented procedure f244a53
F12 low Pinned actions have no update path, pin comments are inaccurate, no code scanning 681980b
F13 low SECURITY.md and architecture.md contradict the code 6ebdfe2
F14 low SECURITY.md's primary reporting route (private vulnerability reporting) is disabled not a commit: settings follow-up below
F15 low README "Expected result" block is stale and no test executes README examples f890b16
F16 low Tests depend on the working directory 99dfcb3
F17 low Walkthrough output directory is neither ignored nor excluded from the integrity scan 6ebdfe2
F18 low PR template carries issue-template front matter; crctl help omits inventory faults 6ebdfe2, c8e7018
F19 medium Integrity verification accepts a receipt whose outcome and close time contradict the terminal CLOSED event 8bf3368, 7c55f3a, d81f9f6
F20 medium A breached action with an escalate-kind reservation is held in REMEDY_DUE with no event until expiry d72a627

Commits

  1. a07958c fix(rail): close disputed when evidence cannot be evaluated or signed
  2. d72a627 fix(rail): route an escalate-only breach to review instead of REMEDY_DUE
  3. 7f8d03f fix(postconditions): refuse compound eq values at both proposal boundaries
  4. 8bf3368 fix(verify): bind receipt outcome and close time to the terminal event
  5. 2f8cd2a fix(http): align sidecar statuses and execute input with OpenAPI
  6. c8e7018 fix(cli): accept dash-leading digest pins and --name=value; strict ports
  7. 86f1d1a fix(cli): report a malformed --at as usage, not an invalid artifact
  8. 99dfcb3 test: resolve repository files from the test tree, not the cwd
  9. f890b16 test(docs): execute README expected-output blocks and fix the stale one
  10. 6ebdfe2 docs: correct claims that contradict the code; ignore walkthrough output
  11. 681980b ci: add Dependabot for actions and CodeQL analysis
  12. 2dd4a31 feat: single-source the implementation version and expose it
  13. b172478 docs(changelog): adopt Keep a Changelog with recoverable version history
  14. c193b1a build: gate version consistency in the repository check
  15. f244a53 ci(release): publish GitHub Releases from version tags
  16. d099cbd chore(release): 0.3.0
  17. 7c55f3a fix(verify): accept the legacy receipt close-time lag
  18. d81f9f6 docs(changelog): describe the receipt close-time window in 0.3.0
  19. 31fde8c docs(changelog): date 0.3.0 for its release day

Each fix commit carries its own regression tests. I confirmed each new test fails against the pre-fix source.

Versioning and release plan

0.2.20 to 0.3.0, a minor bump. Under 0.y.z SemVer a minor bump is the signal for features plus observable behavior changes. This release adds --version, --name=value and implementation_version. It also tightens behavior:

  • compound eq is refused
  • unevaluable evidence closes disputed
  • escalate breaches close via review
  • receipt-profile integrity is bound to the terminal event
  • VERIFICATION_TIME_INVALID is a new library code
  • execute accepts only fault
  • ports are parsed strictly

The repository convention is a patch per fix (0.2.1 to 0.2.20) and a minor per feature release. #67 and #68 merged without a bump and are folded into 0.3.0.

Version surfaces. package.json is the single source, read at runtime by src/version.js. scripts/check.js checks these derived copies on every CI job:

  • OpenAPI info.version
  • the "experimental v0.3.0 source release" line in README and docs/release-status.md
  • the top released CHANGELOG heading
  • in tag builds, the tag

README's line 18 no longer carries a series version.

After merge:

  1. Merge with a merge commit (gh pr merge <n> --merge), not squash, so the 19 commits are kept. main has no branch protection, so confirm gh pr checks is green by hand first.
  2. git fetch origin, confirm package.json on origin/main says 0.3.0, then tag the merge commit:
    git tag -a v0.3.0 -m "Consequence Rail 0.3.0" <merge sha> && git push origin v0.3.0
  3. The tag push runs .github/workflows/release.yml from the tagged tree.
    • verify reruns scripts/check.js on the 6-job matrix. The tag gate is live there, so the tag must equal v + package version. It also reruns node --test.
    • publish refuses a commit that is not on origin/main and skips an existing release.
    • Otherwise it creates the "Consequence Rail 0.3.0" release with notes from node scripts/release-notes.js v0.3.0.
    • Fallback if publish fails: node scripts/release-notes.js v0.3.0 > notes.md && gh release create v0.3.0 --verify-tag --title "Consequence Rail 0.3.0" --notes-file notes.md.
  4. Backfill annotated tags so the CHANGELOG links resolve: v0.1.0 on 5b316b1, v0.2.0 on 7cbf28b, v0.2.20 on 418a212. Those commits contain no release.yml and CI ignores tag pushes, so no workflow runs.
  5. Expect a Dependabot PR bumping actions/setup-node to v7.1.0.
  6. The CHANGELOG dates 0.3.0 as 2026-10-09 (31fde8c), the UTC day of the merge and of the v0.3.0 tag. Merge and push the tag before 2026-10-10T00:00Z. If the UTC day rolls over first, change the heading to the merge day in a new commit before merging, so the tagged tree carries the true date.

Settings follow-ups (not commits; they change repository settings, so they need the owner's confirmation):

  • Enable private vulnerability reporting (F14). SECURITY.md names it as the primary route.
  • Optionally protect main with the six CI jobs as required checks.

Verification

Run on Windows 11, Node v24.18.0, from the final branch tip unless noted.

Command Result
node ./scripts/check.js repository_check: pass, files_checked: 118, json_valid: true, relative_links: pass, public_prose: pass, version_consistency: pass
node --test tests 286, pass 286, fail 0 (baseline on main: 254 of 254)
npm run check pass (check.js + 286/286)
From the parent directory: node --test "consequence-rail/test/*.test.*" 286 pass, 0 fail (baseline: 244 pass, 10 fail)
GITHUB_REF_TYPE=tag GITHUB_REF_NAME=v0.3.0 node ./scripts/check.js exit 0
GITHUB_REF_TYPE=tag GITHUB_REF_NAME=v9.9.9 node ./scripts/check.js exit 1, tag v9.9.9 does not match package.json version v0.3.0
node ./cmd/crctl.js --version / node ./cmd/rail.js --version consequence-rail 0.3.0
node scripts/release-notes.js v0.3.0 / v9.9.9 prints the 0.3.0 section / exit 1
git check-ignore .consequence-rail-evidence/drill.json exit 0; running the walkthrough leaves files_checked unchanged
Pinned artifact hashes (default v0.1 artifact bytes remain pinned to the public base) pass unchanged
Receipt-profile bundles exported by the 0.2.20 tree with the system clock (refund, 400 runs; plus 200 each settled, compensated, disputed) 0 verifier failures at this head; d099cbd failed every bundle whose closed_at trailed the event (51 of 400)
Verifier comparison, origin/main against this head, on refund and inventory demo bundles from v0.1.0, v0.2.0, v0.2.20 and main 0 differences
Privacy screen against origin/main with this body clean

Node 20 and 22 were not run locally; the CI matrix covers them, and no post-20.0 APIs were introduced.

On this PR (head 31fde8c):

  • CI is green on all 6 jobs: ubuntu-latest and windows-latest × Node 20.x, 22.x and 24.x.
  • CodeQL Analyze (javascript-typescript) and Analyze (actions) both pass, and the CodeQL check passes, with 0 open code-scanning alerts on the PR ref.

Deviations from the plan

  • F4/F5 test (2f8cd2a). I added a new route-specific test, "OpenAPI declares the 422 each action operation can return", instead of extending the existing boundary-status test in place. The existing test is unchanged, and the new one drives every planned 422 case live.
  • F19 close-time window (7c55f3a, d81f9f6). Review found that the exact receipt-profile closed_at check in 8bf3368 rejected authentic 0.2.20 sidecar receipts. The check now accepts a closed_at no earlier than the terminal CLOSED event and at most 1000 ms after it. A moved close time, such as the 2036 test case, is still BUNDLE_TAMPERED. The audit profile keeps its exact semantic check, which has applied since 0.2.14.
  • F19 messages (8bf3368). The configured_postcondition_result check got its own message, "Receipt postcondition result does not match its outcome." The receipt-profile close-time check says "Receipt close time does not match the terminal CLOSED event." This keeps each failure diagnosable. The outcome/reason-code check uses the planned message.
  • Ports (c8e7018). The strict pattern also refuses an empty --port "" value, which previously parsed as 0 and bound an ephemeral port, and leading-zero forms such as 08080. Both follow from the planned pattern.
  • --name=value (c8e7018). An unknown --flag=value reports only the flag name, so values are not echoed.
  • CodeQL (681980b). Workflow-level concurrency uses cancel-in-progress: true, which the plan left open. The schedule is weekly, Monday 03:17 UTC.
  • CHANGELOG (b172478).
    • Old section titles are kept verbatim, including their date suffix, as ### lines.
    • Version link references use full commit SHAs.
    • [0.2.0] and [0.1.0] point at releases/tag/v0.2.0 and releases/tag/v0.1.0, which resolve once the tags are backfilled.
  • Version gate (c193b1a). versionFindings also reports a release heading that is not valid SemVer. Without that check, the precedence comparison could not run on such a heading.
  • Release workflow (f244a53). publish sets up Node 24 with the pinned setup-node instead of relying on the runner image's Node. It passes GH_REPO explicitly and skips an existing release through a step output.
  • 0.3.0 date (d099cbd, 31fde8c). The plan said to use the merge date in UTC. d099cbd used 2026-10-08, the UTC date the PR was cut. The merge cannot happen on that day, so 31fde8c changes only the heading to 2026-10-09, the UTC day of the merge and the tag (see the release plan).

EauDoon added 16 commits October 9, 2026 01:35
Evaluation and signing of accepted evidence ran after the VERIFYING (or
REMEDY_VERIFYING) transition and outside the guarded block. Evidence that
omitted a clause's fact, or whose evaluation pushed the signed artifact past
the canonical JSON limits, threw CANONICALIZATION_FAILED and stranded the
action with no receipt and recourse still reserved.

- postconditions: export missingPostconditionPaths, sharing readPath's walk.
- rail: reject evidence missing a clause fact as EVIDENCE_FACT_MISSING, and
  evaluate and sign inside the guarded block through acceptEvidence, mapping
  a canonicalization failure to EVIDENCE_UNREPRESENTABLE. Both take the
  existing EVIDENCE_REJECTED / REMEDY_EVIDENCE_REJECTED -> disputed path and
  start no remedy. Accepted-evidence bytes and key order are unchanged.
- verify: offline semantics reject accepted evidence missing a clause fact
  with SEMANTIC_INVALID instead of CANONICALIZATION_FAILED.
- spec: document both cases as invalid evidence.
A breached action whose reservation kind is escalate threw
REMEDY_REQUIRES_CHILD_ACTION from remediate() with no transition or event,
so it sat in REMEDY_DUE until the reservation expired and then closed
disputed blaming RECOURSE_EXPIRED.

After the expiry and recourse-active guards, remediate() now takes the
existing REMEDY_DUE -> REVIEW_REQUIRED edge with reason
REMEDY_REQUIRES_CHILD_ACTION and closes disputed. No remedy starts, and the
reservation is not released, so the receipt records recourse_final_status
active for the operator's child action. The transition table is unchanged;
the Remediation section of the state-machine spec states the rule.
…aries

Strict eq compares arrays and objects by reference, and evidence is
deep-cloned, so an eq clause with a compound expected value could never be
satisfied: a correct action was recorded as breached and its reserved remedy
ran.

evaluatePostcondition now refuses an eq value that is not a string, finite
number, boolean or null with POSTCONDITION_INVALID, so admission (and HTTP
POST /v0/actions, 400) inherits the rule. The offline bundle validator
mirrors it as BUNDLE_TAMPERED. Scalar comparison semantics are unchanged.

The portable proposal vectors gain an optional live-boundary "code" field
(default SCHEMA_INVALID), documented in conformance/README.md, plus accepted
scalar and rejected array/object eq rows. spec/model.md states the rule; the
published schema bytes are unchanged.
Integrity verification checked that the chain ends in CLOSED and that the
receipt binds the chain head, but never compared the receipt's outcome with
the terminal event. A receipt-profile bundle, which offers integrity only,
accepted a trusted re-signed receipt claiming "settled" while its own chain
ended in SETTLEMENT_DISPUTED, and reviewBundle then reported "settled".

For every profile, integrity now requires the terminal CLOSED event's reason
code and details digest to match the receipt outcome, and the receipt's
configured_postcondition_result to match that outcome. Receipt-profile
bundles also bind closed_at to the terminal event's recorded time; audit
bundles keep that as the existing SEMANTIC_INVALID check. No artifact bytes
change. spec/model.md, docs/architecture.md and docs/offline-review.md state
the bindings.
The sidecar maps SCHEMA_INVALID, EVIDENCE_* and RECOVERY_* to 422, but
api/openapi.json declared no 422 for authorize, permit, verify-outcome,
remediate or reconcile-remedy, so generated clients did not expect them.
Each of those operations now declares 422 with the shared Error response.

The execute route passed the raw body to the library execute(), which also
accepts the undeclared, library-only proposalOverride used by the
action-mutation demo. The route now refuses any field other than fault with
SCHEMA_INVALID (422) before the rail is called; the library API is
unchanged.

A new route-specific test drives each 422 live and asserts it is declared
for that operation, and that a refused execute leaves the action PERMITTED
with no connector call.
About 1 artifact digest in 64 starts with "-" (a shipped refund demo bundle
does), and crctl treated any dash-leading value as a missing value, so it
refused the digest it had just printed. It also had no --name=value form.

- Move the crctl parser into src/cli-args.js so it can be unit-tested
  (crctl runs main() on import).
- Accept --name=value for every value flag; an empty value, "=" on a
  boolean flag, and repeated flags are USAGE_INVALID.
- For --expect-digest and --expect-other-digest only, accept a next token
  starting with "-" when it has the 43-character digest shape.
- crctl help lists the inventory demo faults, says --assurance applies to
  both demos, and documents the --name=value form.
- rail parses --port and CONSEQUENCE_RAIL_PORT as plain decimal only, so
  0x1F90, 1e3, " 8080", 8080.0, +80 and the empty string no longer bind a
  port, and its help no longer ends with a stray quote and no newline.
- README and docs/offline-review.md describe the dash and "=" forms.
options.at reached the verifiers unvalidated, and verifyRecoveryPreflight
raised RECOVERY_BUNDLE_INVALID for a bad "now". One typo in --at therefore
blamed the evidence, and verify-many marked every authentic drill invalid
while dropping the message that named the timestamp.

- crctl rejects an --at that is not an exact ISO UTC timestamp with
  USAGE_INVALID before any file is read.
- verifyRecoveryPreflight validates "now" before reading the bundle when
  requireCurrent is set and throws the new library code
  VERIFICATION_TIME_INVALID. The rail always passes its clock, so admission
  is unchanged; review, compare, link and markdown inherit the check.
- verifyRecoveryFiles validates the instant once before reading any file
  instead of producing a failure per file.
Ten conformance and schema tests read fixtures relative to process.cwd(),
so running the suite from any directory other than the repository root
failed them.

src/rail-test-helpers.js now exports repoPath(...segments), resolved from
the helper's own module URL (not import.meta.dirname, which early Node 20
lacks). The schema-field helpers and every cwd-relative read in the
conformance, postcondition, rail and recovery-preflight tests use it. No
assertion changes. From the parent directory,
node --test "consequence-rail/test/*.test.*" now passes in full.
The README's headline "Expected result" for the duplicate-fault demo omitted
the action and status_calls lines that crctl prints, and nothing guarded
either example block.

The block now matches real output (the action id is deterministic under the
fixed demo clock). test/readme-examples.test.js reads README.md, pairs each
"Expected result:" paragraph with the last crctl command in the preceding
text block, runs it from the repository root, and requires stdout (CRLF
normalised) to equal the following block exactly. It also requires at least
two examples, so removing them is caught too.
- SECURITY.md said the JSON parser does not reject duplicate members. HTTP
  bodies and artifact files do; only library callers passing parsed
  objects bypass the check, matching docs/threat-model.md.
- docs/architecture.md said there is one synthetic refund connector. There
  are two (refund and inventory allocation), and the sidecar exposes only
  refund.
- docs/architecture.md said an unknown final recourse leaves the action
  REVIEW_REQUIRED. The action stays in its pre-close state (for example
  SATISFIED on the satisfied path) with no receipt and a RECOURSE_FINALIZED
  "unknown" event. A new characterization test pins the satisfied path.
- The PR template carried issue-template YAML front matter, which GitHub
  renders as text in pull requests; it is removed.
- The recovery walkthrough writes .consequence-rail-evidence/, which was
  neither ignored nor excluded from the filesystem-walking integrity scan.
  It is now both.
The CI actions are pinned by commit SHA but had no update path, and their
"# v7" comments no longer identified the pinned release (setup-node's v7
tag has moved past the pinned v7.0.0). The repository also had no code
scanning.

- .github/dependabot.yml: weekly github-actions updates, grouped, with a
  "ci" commit prefix. No npm ecosystem; the package has no dependencies.
- .github/workflows/codeql.yml: javascript-typescript and actions analysis
  on push to main, pull requests and a weekly schedule. Top-level
  permissions are empty and the job requests only contents and actions read
  plus security-events write. Concurrency is set at workflow level so the
  matrix legs do not cancel each other. checkout reuses CI's SHA, and
  codeql-action is pinned to the dereferenced v4.38.2 commit.
- ci.yml: only the pin comments change, to v7.0.1 and v7.0.0. The SHAs stay;
  Dependabot will propose the bump.
Neither CLI nor the sidecar could report which build was running, and the
bug report template asked for a package version with no way to obtain it.

- src/version.js reads the version from package.json at runtime, so
  package.json is the only place it is written.
- crctl --version and rail --version print "consequence-rail <version>"
  and exit 0, checked before other parsing like --help; both help headers
  carry the version.
- GET /.well-known/consequence-rail adds implementation_version, declared
  as a required SemVer-patterned string in OpenAPI. protocol_version stays
  v0.1; inspect(), bundles, receipts and events are unchanged, so the
  pinned artifact hashes hold.
- README and the bug report template point at --version.
Twenty shipped versions (0.2.1 to 0.2.20) had no version heading, only
dated "candidate" titles; there were two Unreleased headings, 0.1.0 had no
date, and #68 had no entry, so release notes could not be extracted by
version.

- Header: Keep a Changelog 1.1.0, Semantic Versioning, and a note that
  protocol schema_version identifiers are versioned independently.
- "## [Unreleased]" keeps the #67 bullets and gains the missing #68 entry
  (receipt closed_at is the terminal CLOSED event's recorded_at).
- Each dated section becomes "## [0.2.N] - 2026-09-28" with its old title
  kept as a "###" line. The mapping was verified per heading with
  git log -S against the commit that bumped package.json.
- The sections merged while package.json stayed 0.2.0 (#38 to #46 and the
  earlier corrective candidates) become "###" subsections of [0.2.1], the
  first version that shipped them.
- "## [0.2.0] - 2026-07-27" and "## [0.1.0] - 2026-07-23"; link references
  point at each version commit and at the tags to be backfilled.

Historic bullet text is unchanged; only headings, the header, the #68
bullet and the link references differ.
Nothing stopped package.json, OpenAPI, README, release-status, CHANGELOG
and tags from drifting apart, and OpenAPI already had (0.2.0 against
0.2.20).

- scripts/release-metadata.js holds pure functions only (no main-module
  detection): parseChangelog, versionFindings, releaseNotes and a SemVer
  precedence comparator. versionFindings reports invalid SemVer, an OpenAPI
  info.version mismatch, README or release-status not stating
  "experimental v<version> source release", a missing [Unreleased], a top
  release heading other than the package version, versions not strictly
  descending, invalid or increasing dates (equal dates allowed) and, when a
  tag is given, a tag other than "v" + version.
- scripts/release-notes.js prints one release's CHANGELOG section for a
  vX.Y.Z tag, or exits 1.
- scripts/check.js runs the gate in every CI job and prints
  version_consistency: pass. The tag is read from GITHUB_REF_NAME only when
  GITHUB_REF_TYPE is "tag".
- api/openapi.json info.version moves from 0.2.0 to 0.2.20 so the gate
  starts green.
The project names its GitHub repository as the release channel but had
never produced a tag or a release, and documented no release step.

- .github/workflows/release.yml runs on vX.Y.Z tag pushes with empty
  top-level permissions and workflow-level concurrency that queues rather
  than cancels.
  - verify reruns the integrity check and the test suite on the full
    ubuntu/windows x Node 20/22/24 matrix with contents: read. Actions sets
    GITHUB_REF_TYPE=tag, so the version gate also requires the tag to
    equal "v" + package.json version.
  - publish (contents: write) refuses a tagged commit that is not an
    ancestor of origin/main, skips if the release already exists so reruns
    are safe, and otherwise creates the release with --verify-tag and notes
    from scripts/release-notes.js. Nothing is published to npm.
- docs/release-status.md describes the channel: package.json as the version
  source, annotated tags on main, Releases generated from CHANGELOG.md.
- CONTRIBUTING.md gains a Releasing procedure.
Bump the implementation version from 0.2.20 to 0.3.0 in package.json, the
OpenAPI info.version, README and docs/release-status.md, and move the
Unreleased entries (#67, #68) into a dated [0.3.0] section that describes
everything this release changes, with Added, Changed, Fixed and Security
subsections and updated compare links.

This is a minor bump: the release adds --version, --name=value flags,
implementation_version and release tooling, and it tightens observable
behavior (compound eq refused, unevaluable evidence closes disputed,
escalate breaches close via review, receipt-profile integrity bound to the
terminal event, VERIFICATION_TIME_INVALID, execute accepts only fault,
strict ports). Protocol identifiers are unchanged: schema_version strings,
the well-known protocol_version v0.1 and the signing seed labels.

README's status line no longer carries a series version ("v0.2"), so no
unchecked prose copy of the version remains.
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

The receipt-profile integrity check added for 0.3.0 required closed_at to
equal the terminal CLOSED event's recorded_at. Receipts written by 0.2.20 and
earlier took closed_at from a second clock read after that event was appended.
Under the sidecar's default system clock it trails the event by a millisecond
in roughly one export in eight, so authentic signed receipt bundles from the
previous release were reported as BUNDLE_TAMPERED.

The check now accepts a closed_at no earlier than the CLOSED event and at most
1000 ms after it. Current receipts still copy recorded_at exactly, and a
receipt that moves the close time, such as the 2036 case, is still
BUNDLE_TAMPERED. The outcome and postcondition-result bindings and the audit
profile's exact semantic check are unchanged.

A regression test re-signs receipt-profile receipts with the 0.2.20 shape
(closed_at one millisecond after the event) and at the 1000 ms edge, and
asserts that integrity and review pass, while -1 ms and +1001 ms fail. The
spec, architecture and offline review docs state the window.
The 0.3.0 Changed entry said a receipt-profile closed_at must equal the
terminal CLOSED event's recorded time. It now states the window, no earlier
and at most 1000 ms later, and why it exists: receipts from 0.2.20 and
earlier read the clock a second time and still verify.
@EauDoon

EauDoon commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Fixed the receipt-profile close-time finding in 7c55f3a, with the CHANGELOG wording in d81f9f6.

The exact closed_at check from 8bf3368 rejected authentic 0.2.20 sidecar receipts. Those receipts took closed_at from a second clock read, so it can trail the terminal CLOSED event by a millisecond. The check now accepts a closed_at no earlier than that event and at most 1000 ms after it. The 2036 case is still BUNDLE_TAMPERED. The outcome and postcondition-result bindings are unchanged, and so is the audit profile's exact semantic check.

A regression test re-signs receipts in the 0.2.20 shape (+1 ms) and at +1000 ms, and both pass. It also checks that -1 ms and +1001 ms fail. In a replay of receipt bundles exported by the 0.2.20 tree with the system clock, d099cbd failed all 51 lagged bundles out of 400. This head fails none. The PR body no longer claims that nothing changes for existing artifacts.

@EauDoon

EauDoon commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Fixed the 0.3.0 release date in 31fde8c. CHANGELOG line 14 now reads ## [0.3.0] - 2026-10-09, the UTC day of the merge and the v0.3.0 tag, so the tagged tree carries the true date. The commit changes only that line. Release-plan step 6 in the PR body now states the date instead of a follow-up adjustment. If the merge slips past 2026-10-10T00:00Z, the heading needs one more commit before merging.

At 31fde8c: node ./scripts/check.js passes (version_consistency pass), node --test passes 286 of 286, and all 6 CI jobs plus both CodeQL legs are green.

@EauDoon
EauDoon merged commit 78d6f8a into main Oct 9, 2026
10 checks passed
@EauDoon
EauDoon deleted the upgrade/v0.3.0 branch October 9, 2026 16:36
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.

2 participants