Repository navigation
Release 0.3.0: fail-closed evidence, verifier and CLI fixes, version gate and release workflow - #69
Conversation
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.
|
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:
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.
|
Fixed the receipt-profile close-time finding in 7c55f3a, with the CHANGELOG wording in d81f9f6. The exact 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. |
|
Fixed the 0.3.0 release date in 31fde8c. CHANGELOG line 14 now reads At 31fde8c: |
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.
VERIFYING/REMEDY_VERIFYINGwith no receipt and recourse still reserved. It now closesdisputed(EVIDENCE_FACT_MISSING,EVIDENCE_UNREPRESENTABLE) and starts no remedy. A breach under anescalatereservation now closesdisputedviaREVIEW_REQUIREDinstead of hanging inREMEDY_DUE. A compoundeqvalue, which strict equality can never match, is refused at both proposal boundaries.CLOSEDevent for every profile. For receipt-profile bundles it also bindsclosed_atto no earlier than that event and at most 1000 ms after it, so receipts from 0.2.20 and earlier, whoseclosed_atcame from a second clock read, still verify. A re-signed receipt claimingsettledover aSETTLEMENT_DISPUTEDchain is nowBUNDLE_TAMPERED.executeaccepts onlyfault.--name=value.--atisUSAGE_INVALID, not an invalid artifact.railparses ports strictly, and its help no longer ends in a stray quote.package.jsonis the single version source.--versionis on both CLIs andimplementation_versionis in the capability document.scripts/check.jsgates version consistency in every CI job, and in tag builds it also checks the tag.Authentic artifacts written by 0.2.20 and earlier still verify, with one deliberate exception: an audit bundle whose proposal carries a compound
eqvalue (array or object), which strict equality could never match, is nowBUNDLE_TAMPEREDoffline, 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 tookclosed_atfrom a second clock read after theCLOSEDevent 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. Noschema_version, signature or canonical-byte change for artifacts this repository produces. The pinned default artifact hashes (test/rail.bundle.test.js) pass unchanged, andinspect(), bundles, receipts and events gain no fields. Protocol identifiers are unchanged: schema versions, well-knownprotocol_version: v0.1and the signing seed labels.Review findings addressed
eqclauses with array/object values can never be satisfied, so a correct action is breached and remedied-, and has no--flag=valueformproposalOverride--atis blamed on the artifact; verify-many marks every drill invalidrail --helpends with a stray quote and no trailing newlineCommits
a07958cfix(rail): close disputed when evidence cannot be evaluated or signedd72a627fix(rail): route an escalate-only breach to review instead of REMEDY_DUE7f8d03ffix(postconditions): refuse compound eq values at both proposal boundaries8bf3368fix(verify): bind receipt outcome and close time to the terminal event2f8cd2afix(http): align sidecar statuses and execute input with OpenAPIc8e7018fix(cli): accept dash-leading digest pins and --name=value; strict ports86f1d1afix(cli): report a malformed --at as usage, not an invalid artifact99dfcb3test: resolve repository files from the test tree, not the cwdf890b16test(docs): execute README expected-output blocks and fix the stale one6ebdfe2docs: correct claims that contradict the code; ignore walkthrough output681980bci: add Dependabot for actions and CodeQL analysis2dd4a31feat: single-source the implementation version and expose itb172478docs(changelog): adopt Keep a Changelog with recoverable version historyc193b1abuild: gate version consistency in the repository checkf244a53ci(release): publish GitHub Releases from version tagsd099cbdchore(release): 0.3.07c55f3afix(verify): accept the legacy receipt close-time lagd81f9f6docs(changelog): describe the receipt close-time window in 0.3.031fde8cdocs(changelog): date 0.3.0 for its release dayEach 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=valueandimplementation_version. It also tightens behavior:eqis refuseddisputedVERIFICATION_TIME_INVALIDis a new library codeexecuteaccepts onlyfaultThe 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.jsonis the single source, read at runtime bysrc/version.js.scripts/check.jschecks these derived copies on every CI job:info.versiondocs/release-status.mdREADME's line 18 no longer carries a series version.
After merge:
gh pr merge <n> --merge), not squash, so the 19 commits are kept.mainhas no branch protection, so confirmgh pr checksis green by hand first.git fetch origin, confirmpackage.jsononorigin/mainsays 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.github/workflows/release.ymlfrom the tagged tree.verifyrerunsscripts/check.json the 6-job matrix. The tag gate is live there, so the tag must equalv+ package version. It also rerunsnode --test.publishrefuses a commit that is not onorigin/mainand skips an existing release.node scripts/release-notes.js v0.3.0.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.v0.1.0on5b316b1,v0.2.0on7cbf28b,v0.2.20on418a212. Those commits contain norelease.ymland CI ignores tag pushes, so no workflow runs.actions/setup-nodeto v7.1.0.Settings follow-ups (not commits; they change repository settings, so they need the owner's confirmation):
mainwith the six CI jobs as required checks.Verification
Run on Windows 11, Node v24.18.0, from the final branch tip unless noted.
node ./scripts/check.jsrepository_check: pass,files_checked: 118,json_valid: true,relative_links: pass,public_prose: pass,version_consistency: passnode --testmain: 254 of 254)npm run checknode --test "consequence-rail/test/*.test.*"GITHUB_REF_TYPE=tag GITHUB_REF_NAME=v0.3.0 node ./scripts/check.jsGITHUB_REF_TYPE=tag GITHUB_REF_NAME=v9.9.9 node ./scripts/check.jstag v9.9.9 does not match package.json version v0.3.0node ./cmd/crctl.js --version/node ./cmd/rail.js --versionconsequence-rail 0.3.0node scripts/release-notes.js v0.3.0/v9.9.9git check-ignore .consequence-rail-evidence/drill.jsonfiles_checkedunchangeddefault v0.1 artifact bytes remain pinned to the public base)closed_attrailed the event (51 of 400)origin/mainagainst this head, on refund and inventory demo bundles from v0.1.0, v0.2.0, v0.2.20 andmainorigin/mainwith this bodyNode 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):Analyze (javascript-typescript)andAnalyze (actions)both pass, and the CodeQL check passes, with 0 open code-scanning alerts on the PR ref.Deviations from the plan
closed_atcheck in 8bf3368 rejected authentic 0.2.20 sidecar receipts. The check now accepts aclosed_atno earlier than the terminalCLOSEDevent and at most 1000 ms after it. A moved close time, such as the 2036 test case, is stillBUNDLE_TAMPERED. The audit profile keeps its exact semantic check, which has applied since 0.2.14.configured_postcondition_resultcheck 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.--port ""value, which previously parsed as0and bound an ephemeral port, and leading-zero forms such as08080. Both follow from the planned pattern.--name=value(c8e7018). An unknown--flag=valuereports only the flag name, so values are not echoed.cancel-in-progress: true, which the plan left open. The schedule is weekly, Monday 03:17 UTC.###lines.[0.2.0]and[0.1.0]point atreleases/tag/v0.2.0andreleases/tag/v0.1.0, which resolve once the tags are backfilled.versionFindingsalso reports a release heading that is not valid SemVer. Without that check, the precedence comparison could not run on such a heading.publishsets up Node 24 with the pinnedsetup-nodeinstead of relying on the runner image's Node. It passesGH_REPOexplicitly and skips an existing release through a step output.