What the commit gate can and cannot re-verify from pushed history
alone, and the exact CLI surface of dw verify, the range verifier
that implements it. This is the design contract for WLA-8-02
(implementation) and WLA-8-03 (CI enforcement); the classifications
below are the specification the verifier is tested against.
The gate runs where core.hooksPath points at .githooks — and only
there. Three artifacts it relies on never leave the local clone:
- the certified contract,
.tmp/CONTRACT.md(cleared post-commit); - the contract archive,
.git/pmo-contract-archive/<sha>(.gitis never pushed); - the bundle consent file,
.tmp/BUNDLE-OK.md.
A clone without the hooks configured — or a deliberate
git commit --no-verify — produces commits that skipped every rule,
and nothing downstream notices. What does travel with every pushed
commit: its tree, its parents, its message trailers (PMO-Story:,
PMO-Contract-Digest:), and the evidence files (with captured
command runs) inside the tree. Remote verification is the discipline
of re-deriving every rule those artifacts can support, and being
explicit that the rest is attestation.
Every rule id the gate engine (lib/dw_pmo/gate.py) can emit,
classified. Re-derivable means a verifier with only the pushed
commits can re-check the rule mechanically. Attested-only means
the rule's inputs live in local-only artifacts; the trailers assert
it was checked, and the digest identifies which archived contract
certified it.
| Gate rule id | Classification | Remote re-derivation |
|---|---|---|
contract-missing |
attested-only | Contract text never leaves the clone. PMO-Contract-Digest: presence attests a contract existed. |
contract-facts-missing |
attested-only | Same artifact; same attestation. |
contract-tier-mismatch |
attested-only | Tier is a contract fact. |
contract-index-tree-mismatch |
attested-only | The stamped tree is in the contract; the commit's tree alone offers nothing to compare against. |
contract-head-mismatch |
attested-only | Stamped HEAD is a contract fact. |
contract-branch-mismatch |
attested-only | Branch is a contract fact; branch names are not commit properties. |
contract-sample-mismatch |
attested-only | Staged sample is a contract fact. |
contract-tests-capture-mismatch |
attested-only (portable-with-change) | The capture reference is a contract fact, but captured runs themselves ship in evidence files — see "Strengthening options". |
contract-unchecked |
attested-only | Checkbox state is contract text. |
contract-unknown-box |
attested-only | Same. |
contract-missing-box |
attested-only | Same. |
contract-boxes |
attested-only | Same. |
atomicity |
re-derivable | Count story files whose **Status:** header flips from not-done (in the first parent's tree) to a done-synonym (in the commit's tree). More than one flip without visible bundle rationale fails. |
contract-story-mismatch |
re-derivable | Every flipped story's ID must appear in the commit's PMO-Story: trailer. Subset semantics, mirroring the local gate: declared ⊇ flipped (phase-planning commits declare stories they do not flip — both such commits in real history, 62c5dce and ab66bec, flip none). |
evidence-missing |
re-derivable | A flipped story's evidence-story-NN.md must be added or modified in the same commit (unpadded numbering accepted, as locally). |
evidence-deletion-orphans-story |
re-derivable | Evidence deleted while its story remains done in the commit's tree fails. |
orphan-evidence |
re-derivable | Evidence added without its story flipping, or modified while its story is not done in the commit's tree, fails. |
Two checks exist only remotely — locally they are enforced by construction (the hooks stamp trailers mechanically), so the gate has no rule ids for them:
| Remote-only rule id | Check |
|---|---|
trailer-missing |
An in-scope commit (see scoping) carries no PMO-Contract-Digest: trailer, or flips a story with no PMO-Story: trailer. |
trailer-format |
PMO-Contract-Digest: value does not match sha256:[0-9a-f]{64}, or a PMO-Story: value is not a well-formed story ID. |
The re-derivable set uses first-parent diffs. Everything the local
gate derives from HEAD-vs-index (gate.py steps 5–9), the verifier
derives from first-parent-tree-vs-commit-tree — same status
vocabulary (DONE_STATUSES), same filename grammars, same pairing
integers.
Which commits dw verify examines, and with which rules:
- Roadmap commits only. A commit is in scope when its
first-parent diff touches the roadmap tree (the
pm/roadmap/prefix as resolved per-tree, so self-hosting layouts likepmo-roadmap/pm/roadmap/scope correctly). Commits that never touch the roadmap cannot flip stories or move evidence; trailer absence on them is not a violation (the local gate likewise requires only tier-appropriate contracts there, which are attested-only anyway). - The epoch. Trailers exist since contract v2 (
faa7de6); every later commit in this repository carries the digest trailer (verified 2026-07-03: 48 of 65 commits carry it, with zero gaps after the first). All remote rules apply from the epoch onward; pre-epoch commits are reported as out-of-scope, never as violations. The epoch is auto-detected as the first commit in the walked range carrying aPMO-Contract-Digest:trailer, and can be pinned explicitly (--epoch <rev>, orPMO_VERIFY_EPOCHinpre-commit.config) — policy lives in configuration, the verifier stays mechanism. Per-sha exception lists are expressly rejected; if real history fails a rule, either the rule classification or the epoch is wrong, and the design must be revisited rather than patched around. - Merge commits are out of scope. The commits a merge
introduces are themselves walked and verified individually, and
synthetic merges — GitHub's PR merge ref (what
pull_requestCI checks out), merge-button commits — carry no trailers by construction, so checking them would make every PR red. The accepted v1 limitation: an "evil merge" that smuggles roadmap content into the merge commit itself is not examined. Rebase-style linear history is the norm on this repository.
The local atomicity rule accepts multi-flip commits when
.tmp/BUNDLE-OK.md exists — a file a remote verifier can never see.
Decision: the commit-msg hook stamps the bundle rationale's first
line as a PMO-Bundle: trailer whenever BUNDLE-OK.md authorizes a
multi-flip commit. Remotely, atomicity then reads: more than one
story flip in a commit without a PMO-Bundle: trailer is a
violation. This makes atomicity fully re-derivable at the cost of one
trailer line, keeps the rationale in the audit trail (today it only
survives in the local archive), and changes nothing for the
single-flip norm. History is compatible: no multi-flip commit exists
before the trailer's introduction (verified 2026-07-03 — the only
multi-story-trailer commits flip zero stories).
Publishing archives (a refs/notes/ ref or a tracked directory)
would let a remote verifier re-derive the digest and the stamped
facts — and would also push every certified contract's full text into
the shared repository, add a second copy of facts git already proves
(tree, parent), and make history noisier for a marginal gain: the
facts worth verifying remotely are exactly the structural ones the
tree already carries. Decision: local-only in v1. The digest
trailer remains the attestation anchor — anyone with the committing
clone can prove which contract certified a commit
(git notes-style publication can be revisited if multi-machine
audit becomes a requirement; recorded as deferred in the phase
status).
dw verify [<base>..<head> | --all] [--epoch <rev>] [--porcelain]
-
Default range: merge-base of the default branch and
HEAD, toHEAD(i.e. "what this branch adds"). On the default branch itself, this is empty — use--allfor the epoch-to-HEAD full sweep. An explicit<base>..<head>overrides both. -
Read-only: never writes to the work tree, the index, or
.tmp/; safe on a bare CI checkout and on someone else's clone. -
Output grammar (one line per violation, greppable, mirroring
dw check):ERROR <short-sha>: <rule-id>: <issue>Clean exit prints
dw verify: ok (<N> commits verified, <M> pre-epoch skipped). -
--porcelain:key=valuelines per commit (commit=,in_scope=,verdict=,rule=on failure), matching thedw gate --porcelainidiom. -
Exit codes, aligned with the
dw nextconvention:0= every in-scope commit verified clean;1= at least one violation (details on stdout);2= usage or git error (bad range, not a repository, truncated/shallow history that prevents first-parent diffing — a shallow clone must fail loudly, never pass silently).
The verifier closes the trust gap only where the committer's hooks
cannot be assumed: in CI, on every push and pull request. This
repository's verify-history job in
.github/workflows/validation.yml is the reference wiring, and
adopting repositories can copy it verbatim:
verify-history:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
# dw verify refuses shallow clones (exit 2): a truncated
# history must never verify silently.
fetch-depth: 0
- name: Verify history against the gate's structural rules
run: python3 .githooks/dw verify --all(In an adopted repository the CLI lives at .githooks/dw; this
repository invokes its source copy at pmo-roadmap/bin/dw.)
The job runs the full epoch-to-HEAD sweep on both events rather than
plumbing per-event ranges: it is strictly stronger, immune to
force-push window edge cases (github.event.before is unreliable
after history rewrites), and costs seconds at this history's scale.
The verdict is blocking on this repository (configured 2026-07-03):
main requires verify-history, shellcheck, python-floor,
package-smoke, and the ubuntu integration leg as status checks,
with linear history required (merges never land, matching the
verifier's merges-out-of-scope rule), and force pushes and deletions
disallowed. Admin enforcement is deliberately off: the maintainer's
direct-to-main flow continues, with every push still audited by the
same sweep; contributor pull requests are hard-gated. The macOS leg
stays advisory (brew installs are the flakiest surface in the
matrix). Adopting repositories configure the equivalent in their own
branch-protection settings.
- Evidence-run presence: flipped stories' evidence files carry
captured runs (command, exit code) in-tree; a remote rule could
require at least one exit-0 captured run in the evidence a flip
ships. Deferred: pre-capture evidence (phases 0–5) would need its
own epoch, and the value over
evidence-missingis incremental. - Archive publication — see above; deferred unless multi-machine audit becomes a requirement.
- Signed trailers (GPG/SSH commit signing tied to the digest): orthogonal to this contract; git already supports it independently.
This contract is verified by, and must stay in lockstep with:
tests/verify-range.sh— fixture histories: clean, smuggled no-verify flip, double-flip withoutPMO-Bundle:, orphan evidence, missing trailer, pre-epoch grandfathering (WLA-8-02).tests/dw-core-tests.py— unit coverage of the re-derivation functions against synthetic commit data (WLA-8-02).- The
verify-historyCI job invalidation.ymlrunningdw verifyon every push and pull request (WLA-8-03). - A rule-inventory cross-check: every rule id
gate.pyemits appears in the classification table above (grep-provable; asserted in the WLA-8-01 evidence and re-asserted by unit test in WLA-8-02).