Standards governance
The rulebook for how standards change in BenchWeave. Enforced in review by the standards-governor agent (
.claude/agents/standards-governor.md); the drift gates inbenchweave.standardsare its mechanical half. Ratified 2026-09-16, founded by the version reset to the 0.1.0 baseline (prior ad-hoc version history lives in git before that date).
Identity and homes
A standard is <id> + semver (MAJOR.MINOR.PATCH), living at standards/<id>/<version>/ — machine corpus and prose companions together (see docs/doc-taxonomy.md for the full placement taxonomy). Two locks declare and pin it:
standards/standards-manifest.json— governance: which versions are current, their status, and the normative path setstandards/corpus-manifest.json— byte truth: one sha256 per machine file, rows relative tostandards/
Repo identity lives in committed data outside the corpus: the compatibility matrix’s Sources cell renders pyproject.toml [project.urls] Repository and the .gitmodules submodule URL, while standards-manifest.json’s top-level sdk_compatibility block mirrors the compatibility block of the SDK lock at the pinned gitlink commit — equality-enforced by benchweave.standards check wherever the submodule working tree sits at that pin; a moved working tree is refused by name, never mirrored from (the lock stays the authority); none of these are digest-pinned, so they move no corpus rows and need no repin (issue #158). The lock’s sdk field names the pinned SDK’s own version (issue #187 fork (a)), and benchweave.standards check anchors it to the pinned pyproject. The dependency_policy block (G-3 below; issue #215, parent #203) is governance data in this same family: a top-level block in standards-manifest.json, read by its own fail-closed loader, moving no corpus rows and needing no repin — the sdk_compatibility precedent.
Change classes and their bumps
| Change | Bump | Notes |
|---|---|---|
| Prose clarification (companion docs) | none | Prose is not digest-pinned; keep it truthful |
| Additive machine errata (backwards-compatible) | PATCH of that standard | The interface approver_token pattern: new optional field, nothing removed or retyped |
| Breaking machine change | MINOR at least | Old version retained frozen; supersedes recorded |
| New standard | admission | Review + status: stable; new tree dir at its first version |
| Deprecation | none (status) | Status change only; the version stays, digest-frozen |
| Deletion of a version | never | Retired corpora live in git history, not the tree |
Bump minimization (#69). One bump per (standard, release train). A release train is one merge window of a run: PRs opened concurrently against the same origin/main. Queued changes to the SAME standard that share a train batch into a single bump (one copy step from the current active version, one repin); a batch’s bump follows the highest change class it contains (errata batched with a breaking change bumps MINOR). Never stack a bump on an unmerged bump — if the predecessor merges first, re-copy from the new predecessor. Different standards may share a merge only when they are one increment. CI tests the merge result, so every in-tree artifact declaring the old version moves in-arc (in the same change/PR) with the bump — the in-tree motion is part of the bump, not a follow-up.
Bump window (#97). The train rule is prescriptive, not descriptive: a standard may not bump more than once per 24-hour window, measured between the committer timestamps of the commits that added each new version directory. A queued change to a standard still inside its window WAITS — it batches into the next bump of that standard (the highest-class rule above already governs what the batch becomes). The window is per standard: bumping otdp does not open or close a window for registry. Exempt: a standard’s first version (admission), and a reset-class commit — identified by a shape heuristic (one commit adding version directories for three or more standards, the 2026-09-16 signature), not by the Resets section’s full definition; the known residual is that a coordinated multi-standard increment of that same shape also escapes the window, accepted because resets are executive-rare. Enforced mechanically by benchweave.standards.train_window in the standards suite; the clock self-anchors at that module’s own arrival commit, so history before the rule is grandfathered by mechanism. The floor was ratified at 48 hours as a starting figure and revisited once, not silently: 2026-09-23, explicit owner ruling after one window ran — the observed cost was latency (a queued fold-after-release waited 34 hours for its train), not churn. The floor is 24 hours. The window prices the release ledger only: pre-release authoring accumulated on a dev head is not a bump and waits for nothing (see The dev stage below) — the floor still bounds everything consumers see, and no longer bounds the edit.
G-5 (bump window supersession, 2026-10-12, issue #451; supersedes the paragraph above in part). The #97 time-based window is superseded. A standard may not bump while the same standard’s previous released version’s SDK leg is outstanding: the commit adding a new version directory of standard S must arrive on a tree whose SDK lock already carries S’s previous released version (sync-completion pacing, not wall time). Rationale on the record: under multi-version serving (R-2, VR-47.2) a consumer is bound by its pin, resolved digest-verified from the retained corpus — the window’s stated purpose (“the floor still bounds everything consumers see”) no longer describes the machinery; what remains priced is the release ledger (the SDK train, the validation-report regen, the in-arc sweep, permanent version-dir retention under copy-never-move), and the sync-completion requirement prices exactly that. Retained unchanged: one bump per (standard, release train); never stack a bump on an unmerged bump (re-copy from the new predecessor); the highest-class batch rule; the per-standard scope; the first-version (admission) and reset-class exemptions (same shape heuristic). Emergency pacing — a bump that must outrun an outstanding SDK leg — is a coordinator exception written into the promotion PR body, successor to the window exception, carrying the same #97 reopen obligation. Enforced mechanically by
benchweave.standards.train_window, re-anchored at this amendment’s own arrival commit (history before it grandfathered by the same self-anchoring mechanism).
Bump mechanics — copy, never move. A version bump copies the old version dir to the new version and edits bytes only in the copy; the old dir and its corpus-manifest rows stay in place, digest-frozen. Moving or in-place-editing a retained version is a governance violation, not a shortcut. The new version’s rows record the old corpus path as their source. A dev-stage promotion adds one more edge: its rows also record the pre-dev active version’s corresponding paths as lineage — the dev path named by source is a deleted staging directory and cannot carry the retention chain alone. Both edges are walked by the derived corpus-dir guard (tests/contract/test_baseline.py), with the same historical-terminal rule for -dev segments; repin accepts the optional lineage field on rows and refuses one naming a -dev path (that edge is what source carries) — ordinary supersession keeps both trees justified side by side, dev promotions included. Digest pins move only through uv run python -m benchweave.standards repin — the loop is edit → repin → export, never a hand-spliced digest. A bump carrying corpus bytes for any standard with a machine-written validation report (otdp, registry, execution, interface) regenerates that version’s report via its writer (check_<suite>.py --write-report, which refuses on a failing run). The four standards-tree family scripts derive their corpus directory from standards-manifest.json‘s active entry for their standard — devices names it OUT, the siblings name it CONTRACT_DIR — and the pin test’s report paths and mutation fixtures derive the same way (#102 D2, generalized to the family by D1: each script reads the manifest and refuses loudly without it), so a bump makes no hand-moves on those. Report titles are version-free constants needing no derivation (# Registry contract verification, # Procedure and bench contract verification, …) — devices’ alone embeds its derived version (# OTDP {version} specification verification). Closure’s report is docs-rooted (CONTRACT_DIR = DOCS / "acceptance" — no version directory to derive); its CROSS-standard contract reads are manifest-derived like the rest. The docs/README.md rows linking the reports remain conventional moves; a FORGOTTEN row is caught mechanically by the pin’s exactly-once link assert (a stale row left beside the new one is not — copy-never-move keeps the old target resolving). With the version moved, the pin fails on the copied stale report until the regen runs. Derived version surfaces are not hand-swept — each moves by its own mechanism: the compatibility matrix renders from the standards manifest’s active versions (matrix --check), the public site’s version stamps render at assembly from the manifest’s active versions and the sdk_compatibility mirror (scripts/assemble_docs_site.py — claim sites in website/index.html carry {stg-*} tokens, and no three-component version literal is allowed in the class-11 source set, website/ plus index.qmd), and the docs/README.md report rows are caught by the manifest-derived pin when forgotten; the inventory of version-bearing surfaces and their motion mechanisms lives in docs/internal/drift-and-obligations.md.
The dev stage (-dev)
A standard may carry at most one dev head: an optional dev block on its standards-manifest entry naming a version (<target>-dev, the target strictly greater than the active version), the date the head was opened (the machine-readable stale-head age), and a normative path set under standards/<id>/<target>-dev/. The dev directory is staging, not a version: it is created by a copy of the active version (its corpus rows cite the active path as source), edited in place across any number of changes — each edit follows the edit → repin loop, and validate treats dev pins exactly like active pins — and it never appears in the exported bundle, the SDK lock, the vendored tree, the compatibility matrix, the identity block, or the public docs site (the site assembly skips -dev directories on the packaging-side lexical rule — both the page staging and the corpus copy beside it). The SDK never consumes -dev bytes: immutability starts at release, and a lock pinned at a -dev version would have to churn its version per edit or carve an exemption into the same-version refusal — neither is sanctioned.
G-1 (recorded supersession, 2026-09-26, issue #203; supersedes the paragraph above per the dev-pin ruling VR-47.3). “The SDK never consumes
-devbytes” is replaced by: consumers may pin a dev head under three conditions — explicit per-plugin opt-in in the plugin’s constraints; the lock row records content identity (git sha plus per-file digests); every coupled guard keeps its force (one head per standard, the window promise, wheel exclusion, promotion deletes dev bytes). Immutability still starts at release; a dev PIN is immutable by content, not by label.
The head is train-shared state: one head per standard, carried on the train’s working branch, owned by no author, named for the target version — no author suffix, because authorship already lives in git history and PR review. Contributors edit it through the ordinary PR flow; two authors’ overlapping edits are ordinary MODIFY-vs-MODIFY merges resolved in review. A second concurrent head is refused by the one-optional-field shape, not by convention. Before opening or editing a head, a contributor re-reads the manifest’s dev block — the machine-readable shared-state token; python -m benchweave.standards versions lists open heads — and rebases on the head-carrying branch.
Head lifecycle. OPEN is a PR adding the block, the directory and its rows; the standards-governor lane (mandatory for any standards/ touch) is the gate — there is no separate pre-approval. EDIT is a PR to the head-carrying branch. CLOSE is either promotion (below) or abandonment: the standards coordinator deletes the directory, its rows and the block through the same PR flow; an orphaned head (author unavailable) closes as an abandonment. A stale head (open, idle, target still open) is a coordinator ruling, not a gate — the block’s opened date keeps its age machine-readable so the governor review sees it on every standards/ touch, and a clock on authoring is exactly the conflation this stage removes. The standards coordinator may declare a head a release candidate (the optional boolean candidate field on the dev block, owner ruling 2026-09-23): the declaration is advisory and machine-readable — it changes no enforcement, testing runs through the --corpus lane regardless, and a release candidate as a separate released directory stays rejected.
The dev-proof lane: each standards-tree family script (devices, registry, execution, interface) accepts --corpus <dir>, which points its census at the manifest-declared dev head for that script’s standard — dev bytes are proven in place, before promotion, through the same checker lane that validates releases. The override accepts exactly the declared head (anything else refuses), never combines with --write-report (the machine-written reports are a property of released versions; a dev run is a check, not a report), and is read-only for the SDK’s lock, vendored tree and pointer. Without the flag, every script resolves the manifest-active tree exactly as before. Cross-standard reads stay manifest-active regardless of the override.
The bump window does not see the head — the collector counts pure-semver version directories only, and the active entry’s version must be pure semver (standards_entry_version_invalid on anything else), so the window cannot be dodged by suffixing a release directory. Promotion is the bump event, and it is a bump in every existing respect: copy the dev directory to the released version (its corpus rows cite the dev path as source and the pre-dev active version’s corresponding paths as lineage — a dev promotion severs the copy-never-move chain the bump flow would otherwise carry: the promoted bytes’ direct producer is the dev directory, which teardown deletes by design, so the predecessor edge is named separately rather than left to git history), apply the version sweep, delete the dev directory and its rows, remove the dev block, repin, regenerate the validation report, export and sync — under the change-class rules with the batch being everything the head accumulated; the head’s version names the intended target, the class rule governs the promoted version. Immutability starts at release: a version directory with no -dev suffix is frozen exactly as before. The promotion PR also appends the promotion record — dev_edit_sha and dev_tree_digest (derived with dev_tree_digest_at) — to standards/promotion-records.json; landing_sha names a commit that does not exist while the PR is authored and is filled by the immediate post-landing append on main, until which the record stays pending.
An emergency patch on a standard with an open head never opens a second head. Two sanctioned paths, named by the standards coordinator: strip and promote — revert the head’s unfinished items out (cheap MODIFY reverts) and promote the remainder early under a recorded window exception — or a priority claim, where the emergency claims the head and the paused work re-lands after promotion.
Recorded ruling — VR-47 points 1 and 2 (R-1/R-2, 2026-09-26, issue #203)
Points 1 and 2 are mutually load-bearing — the reopen (point 1) exists to make the CON-10 amendment (point 2) buildable — so they are recorded as ONE ruling (append-with-evidence; the original assertion texts above are never edited). Framed as fresh supersession on new evidence under the recorded-ruling rule; the executed precedent for revisiting a ratified figure by owner ruling is the bump-floor history above.
R-1 (reopen, VR-47.1). Issue #97’s withdrawal of “range pins, a resolver, lock formats, a mutable
-devstage, forward-compat tolerance” is superseded in part. REOPENED: range pins, the resolver, lock formats, and multi-version serving of retained versions — the out-of-tree breakage axis (a plugin outside the tree cannot move in-arc with a bump) post-dates the #97 closure and is new evidence. The-devrecord’s “Not in scope, ever” (issue #97, the dev-stage row) is superseded for the serving/pinning question only, on the same evidence, joining the earlier sanctioned reopen of the mutable-devstage (#168/#169). NOT reopened: cross-version tolerance (PR #59’s rejection stands — serving retained versions EXACTLY is the only tolerance; ranges never fuzzy-match) and external standards indexes (the PRD’s non-goal, unchanged).R-2 (CON-10 amendment, VR-47.2). CON-10 keeps what it protects — one shared authority (the retained corpus and its manifests), one descriptor dialect, one projection — and drops what it never needed: the singularity of the served pointer. A descriptor validates against exactly one OTDP version — its own pin — resolved from the retained corpus, digest-verified. PR #174’s rejection of version-matched schema resolution is superseded on both its stated grounds together: multi-version serving answers the single-version-tree ground (the manifest, export, sync and wheel now carry every served version), and wheel-bundling of the served set answers the “structurally unbuildable” SDK leg (the SDK validates offline against the pinned version’s bundled bytes; PRD VR-32, owner Q8). The one-entry-per-id manifest shape is undisturbed; the equivalence census extends across the served set. CON-1’s #63 amendment and CON-8 each gain the dated amendments in
docs/internal/invariants.md. Nothing in PR #59 moves (VR-47.5): per-plugin exactness relocates one global gate to N per-pin gates; out-of-range stays non-conforming, unretained stays refused.
Retention
Superseded versions stay digest-frozen forever. A version is retired by deleting its directory AND its corpus-manifest rows only as part of a reset class event (see below); ordinary supersession keeps both trees side by side. The obsolete-version guard in tests/contract/test_baseline.py refuses any corpus the manifest does not admit.
Resets
A reset re-baselines every standard to a chosen version label in one versioned act (founding precedent: the 2026-09-16 0.1.0 reset). Resets are rare and executive decisions; they must reset every surface together — governance versions, tree dirs, and version strings inside the normative bytes — following the cascade order in the governor’s runbook. History is preserved by git and by the source provenance fields in the corpus manifest: reset-imported rows name the upstream authoring path, and supersession-copied rows name the corpus path they were copied from (per Bump mechanics above) — never a path that did not produce the bytes.
Gates (mechanical, all must be green)
- Drift gates: normative bytes changing without a version bump is refused (
normative_hash_mismatch,content_drift_without_version) - Coverage: corpus-manifest rows == machine files on disk (nothing vendored escapes the manifest, nothing listed is missing; both directions are enforced fail-closed by
benchweave.standards repin, the only mechanical pin writer) - Architecture validator suites for the touched standard(s)
- SDK round-trip:
make check-sdk-standards(manifest, bundle, lock and vendored tree agree) - Compatibility matrix matches a fresh render (
matrix --check)
Direction
Standards changes land main-side first, flow outward via the exported bundle, and are consumed by revision-pinned locks (SDK vendored tree; plugin contract locks). Never SDK-first, never consumer-side edits to corpus bytes.
Deliberately versioned elsewhere (not governed here)
Sub-capability ids (otdp:dc_psu:* profiles, otdp.*/*/ action ids), registry fixture package versions, plugin release versions, the architecture document edition (STG 1.5), and the MCP date. These carry their own versions; only the standards above are governed by this file.
The adapter API version is declared in the corpus-manifest identity block (identity.adapter_api); its authority is the active OTDP descriptor schema’s $defs.adapter.properties.api_version const, and validate_identity fails closed on absence or disagreement at every export/check. Identity-block edits move no corpus rows and need no repin. Promotion trigger: when the adapter surface stabilizes, this becomes a seventh corpus standard carrying a canonical importable Python contract; until then the declaration plus the three-way agreement test (tests/sdk/test_adapter_agreement.py) is the honest boundary — it pins names, arity, keyword-only-ness, coroutine-ness and key/enum sets, not semantics.
G-2 (amendment, 2026-09-26, issue #203): under multi-version admission the adapter API version’s authority is the PINNED version’s descriptor schema $defs.adapter.properties.api_version const, not the active’s; the identity block’s active-declared value is unchanged (CON-8).