Skip to content

Epic/0.7.1 control plane authority - #440

Merged
GionaGranchelli merged 102 commits into
release/0.7.0from
epic/0.7.1-control-plane-authority
Oct 3, 2026
Merged

GionaGranchelli merged 102 commits into
release/0.7.0from
epic/0.7.1-control-plane-authority

Conversation

@GionaGranchelli

Copy link
Copy Markdown
Owner

Purpose

Scope

  • Change class: [runtime-behaviour | public-api | build-logic | canonical-baseline | quality-deviation | ci-workflow | documentation | baseline-migration]
  • Files touched: [list]

Behavioural Contract

Architecture Impact

  • No new dependency edge introduced
  • Core does not depend on an adapter/framework
  • No ownership/lifecycle change (or described below)
  • No global mutable state introduced
  • No duplicate of an existing authoritative model (provider routing, failure taxonomy, runtime events, module catalog, module boundaries, structured contract)
  • If any box is unchecked, describe the impact and the ADR/design reference: [describe]

Compatibility

  • Stable public API unchanged
  • Preview API unchanged
  • Persisted representation unchanged
  • Event/reason/error code unchanged
  • Configuration semantics unchanged
  • If any box is unchecked, describe the impact and the migration path: [describe]

Correctness & Concurrency

  • Cancellation behaviour unaffected (or described)
  • Retry behaviour unaffected (or described)
  • Concurrency unaffected (or described)
  • Evidence/audit/telemetry emission unaffected (or described)
  • Change is replay-safe
  • Which contract test proves the change? [test name(s)]
  • If this fixes a defect: what discriminator failed against the baseline before the fix? [RED → GREEN evidence]

Verification

  • Focused test: [command + result]
  • ./gradlew verifyPr: [result]
  • Specialized gates (if applicable): [command + result]
  • Skipped checks and why: [list or "none"]

Quality Impact

  • No baseline regeneration
  • No deviation ceiling increase
  • No quality gate weakened
  • If any box is unchecked: [describe]

Remaining Risks

Non-Claims

This PR needs review before merge.

GionaGranchelli and others added 30 commits September 13, 2026 16:43
* feat(core): add typed workload/configuration/deployment/run identity contract (0.7.1b)

* docs(0.7.1): record 0.7.1b workload identity contract task

* fix(core): expose identity contract as plain JVM classes for Java interop (0.7.1b review)

* docs(0.7.1): correct 0.7.1b task evidence wording after Java-interop review

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
…e boundary (0.7.1c) (#403)

* feat(control-plane): add authoritative workload registration and state boundary (0.7.1c)

* fix(control-plane): enforce CAS immutability, truthful stale versions, clean static analysis (0.7.1c review)

* chore(0.7.1c): remove unused import, clarify race-handshake KDoc

* fix(control-plane): advance state version only on genuine mutation (0.7.1c review)

next() now runs only inside the mutation branch of updateMetadata and
transitionLifecycle — stale, same-state, no-op and invalid commands are
observational and never require a future version. A genuine mutation at
Long.MAX_VALUE still fails closed on overflow.

- InMemory CAS aligned to the JDBC concurrency token (deployment scope +
  state version); SPI wording makes the token explicit, TCK pins it so the
  two stores cannot drift semantically.
- Authority test seeds Long.MAX_VALUE records and pins all non-mutation
  outcomes at the boundary; TCK gains the scope+version token discriminator.
- Module/TASK docs test counts corrected (authority 26, TCK 17/store).

* fix(control-plane): verify immutable witness in store CAS (0.7.1c review)

A fabricated expected carrying a different configuration or fingerprint
for the right scope+version previously passed CAS: InMemory stored the
whole updated record (rewriting immutable authority), while JDBC returned
true but never touched immutable columns. Same SPI call, two outcomes.

CAS now requires the stored immutable authority to match expected's
witness, alongside the scope+stateVersion token:

- InMemory: predicate checks current.identity and
  current.configurationFingerprint against expected's.
- JDBC: single UPDATE gains configuration_id/version predicates plus an
  EXISTS witness over tramai_configuration_revision fingerprint.
- SPI KDoc states the token/witness split explicitly.
- TCK: 2 new discriminators — fabricated configuration and fabricated
  fingerprint at the correct scope+version both return false and leave
  the registration unchanged (19 cases/store).

Mutable metadata/lifecycle in expected remain deliberately ignored.

* fix(persistence-jdbc): use non-deprecated PostgreSQLContainer in 0.7.1c tests

The registration store tests added by 0.7.1c imported the deprecated
org.testcontainers.containers.PostgreSQLContainer, producing compiler
warnings not covered by the warnings baseline (additions forbidden).
Switch to org.testcontainers.postgresql.PostgreSQLContainer (2.0.5
relocation, non-generic, same fluent API) so verifyCompilerWarnings
stays clean without a baseline change.

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
Review follow-up on #404: both control-plane tables stored ids, owner and
purpose as unrestricted TEXT, so a hand-written or corrupted row could hold
state the typed contracts reject and fail during typed reconstruction on read.

- V8: CHECK constraints for the identity columns (length 1..128) and for
  owner/purpose (1..256 / 1..512) on both tables.
- JdbcWorkloadRegistrationStoreTest: a row exactly at each contract bound is
  accepted and round-trips through the typed mapper; one character over each
  bound is rejected with SQLSTATE 23514. Proven mutation-sensitive: removing
  the constraints fails the test.
- 0.7.1c task doc records the database-side bound defence.
…ulti-line bodies (#408)

`verifyJUnitTestSignatures` reported 51 violations on the Epic base that are not
violations: every one of them is the Unit-safe form the verifier's own contract
documents --

    fun `x`() =
        runBlocking<Unit> { ... }

The head of an expression body was read from the line containing `=`, so when the
expression starts on the next line the head was empty; an empty head matches none
of the Unit-safe shapes, and the declaration was rejected before `scanFile` could
append the continuation line. Same-line heads were unaffected, which is why the
existing discriminators (all same-line) never caught it.

Evidence that these were false positives rather than real hazards: the flagged
classes execute every declared test (WorkloadRegistrationAuthorityTest 26/26,
JdbcWorkloadRegistrationStoreTest 6/6), because `runBlocking<Unit>` already makes
the JVM return type void.

`decide` now routes both `=` paths (plain and explicit-return-type) through one
`bodyDecision` helper that returns `Continue` while the post-`=` text is blank, so
the declaration stays pending until the continuation line arrives. No call site was
modified to satisfy the analyzer: the fix is in the analyzer.

Discriminators: 8 new cases, 21 in the class, 0 skipped, covering both directions --
`= <newline> runBlocking<Unit> {`, `: Unit = <newline> runBlocking {`,
`= <newline> Unit`, `= <newline> runTest {`, multi-line signature with a Unit-safe
continuation head; and the adversarial cases that must still fail closed --
`= <newline> runBlocking { ...assertThat }`, `= <newline> assertThat(...)`, and a
multi-line signature with a non-Unit continuation head.

Result on the Epic base: verifier findings 51 -> 0 with zero test-source changes.

Known fail-open, documented on the helper: a file ending exactly at `fun x() =`
now stays pending instead of being rejected. It cannot occur in a compiling test
source.

Co-authored-by: TramAI Test <tramai-test@invalid>
…ion oracle (#409)

`ModuleCatalogMutationTest` D5 fails on the Epic base:

    catalog description for tramai-control-plane must exactly match the pre-B8 policy
    expected: <null> but was: <Authoritative control-plane registration and lifecycle
    state boundary for governed workloads.>

Which side is authoritative, checked before changing anything:

- The catalog says this module is published, and `TramaiPublishingPlugin` reads that
  entry: `build.gradle.kts` applies the publishing convention to every `java-library`
  module, the plugin gates on the catalog's publishability, and it fails closed when a
  catalog-published module has a blank description. Deleting the description to satisfy
  D5 would break the publishing contract instead of fixing it.
- `LegacyPublicationDescriptions` is a frozen record of the pre-B8 `projectDescription()`
  output for the modules that existed then. `tramai-control-plane` is new in 0.7.1c, so it
  has no pre-B8 POM description and the oracle's null lookup is the stale side. Its
  description is the module's authored policy (the module card carries the same one-liner).

Repair: keep the frozen table verbatim and add a `postFreeze` overlay holding the authored
descriptions of modules published after the freeze. D5 stays total over the published set —
a newly published module must be recorded rather than silently inheriting the generic
fallback — and no legacy string is edited.

Verified: ModuleCatalogMutationTest 14 executed, 0 failures (D5 was the failure);
verifyChangePolicy -PchangeClass=build-logic -PchangePolicyBase=c700f935 green.

Co-authored-by: TramAI Test <tramai-test@invalid>
The 0.7.1d review found that the JUnit test-signature authority was reachable only through
the local `verifyPr` path. A `@Test fun x() = runBlocking { ...assertThat... }` whose JVM
return type is not void is silently discarded by JUnit, so the suite reports green with the
test never running — and nothing in CI reported it. That class of defect could therefore
reach the default branch.

`verifyJUnitTestSignatures` is now a fourth entry in the `quality` job matrix, alongside the
compiler-warning and static gates that already run there. Strictly additive: no existing
step, trigger, gate or matrix entry is modified. No JDK pinning and no analyzer change, by
design — the analyzer's semantics were fixed separately.

`JUnitTestSignatureCiWiringTest` proves the wiring by extracting the `quality` job and then
the `junit-test-signatures` matrix entry, asserting the conjunction of the entry name and
`gates: verifyJUnitTestSignatures` inside that entry. A workflow-wide search would also pass
on an unrelated mention, which is exactly the failure this shape avoids. Three adversarial
discriminators prove the assertion bites: associating a different gate with the entry,
dropping the entry, and moving the entry into another job.

Verified: JUnitTestSignatureCiWiringTest 4 executed / 0 failures / 0 skipped;
verifyJUnitTestSignatures green; spotlessCheck; verifyStaticAnalysis; verifyCompilerWarnings;
verifyChangePolicy -PchangeClass=ci-workflow -PchangePolicyBase=9d02ccc7.

Co-authored-by: TramAI Test <tramai-test@invalid>
The `tests` job background-runs `./gradlew test` and then polls it for
`18 x sleep 40` = 12 minutes inside a `timeout-minutes: 15` step, killing the build and
dumping worker threads if it is still running. That threshold was written as flake
containment ("instead of letting a concurrency flake hold the pipeline hostage for the
full timeout"), but measured suite variance has grown into it, so the watchdog is now
classifying identical candidate content by runner timing rather than by correctness.

Measured `tests` job durations, last 11 runs:

    a2f9db2  12.83 min  success        e3d6a24   8.33 min  failure (real assertion)
    aa6e989  12.83 min  success        67f827c  11.03 min  failure
    6480e1d  12.17 min  success        94be147  10.67 min  success
    e6ac987  12.15 min  success        741a76f  10.62 min  success
    c0c937b  11.27 min  success
    48e1daf  12.03 min  success  <-- the certified 0.7.1d candidate, inside the budget by seconds
    a4eab8b  12.42 min  failure  <-- killed twice, annotations:
                                     "Run tests exceeded 12 minutes — dumping threads and aborting"

The #412 candidate changes one line of `config/quality/maintainability-deviations.yml`; its
two failures produced no failing test, no `tests completed` tally, and skipped every
downstream step (cancellation safety, critical coverage, mutation ratchet, against PR base
and previous master). Ordinary successful runs already reach ~12.8 minutes, so there is
effectively zero headroom between normal variance and the containment threshold.

Change: `seq 1 18` -> `seq 1 21` (21 x 40s = 14.0 min), with the comments and the failure
message updated from 12 to 14 minutes. The step's `timeout-minutes: 15` is unchanged, so the
two-tier mechanism is preserved with a full minute of separation:

    normal suite variance      <= ~14 min
    containment watchdog        ~14 min
    hard GitHub step timeout      15 min

This is CI calibration, not slowing the suite down: a genuine hang still gets killed, it
just no longer shares a boundary with ordinary runtime.

Verified:
- YAML parses; watchdog arithmetic 21 x 40s = 840s = 14.0 min; 1.0 min gap to the 15-minute
  step timeout; `bash -n` clean on the extracted step script
- :build-logic:test --tests "*CoverageWiringTest*" --tests "*CompilerWarningsWiringTest*"
  --tests "*JUnitTestSignatureCiWiringTest*" — 8 / 17 / 4 executed, 0 failures, 0 skipped
  (these are the repository tests that read .github/workflows/ci.yml)
- verifyChangePolicy -PchangeClass=ci-workflow -PchangePolicyBase=9d33e0e8 — PASSED,
  1 changed file, no violations
- one file changed: .github/workflows/ci.yml, no source or test file touched

Co-authored-by: TramAI Test <tramai-test@invalid>
… deviation (#412)

`verifyMaintainabilityBaseline` fails on the Epic base `9d33e0e8` (reproduced with #407
absent, and again on pristine checkout):

    FAIL: NEW_GLOBAL_STATE_FINDING: 1 new finding(s) in :tramai-control-plane
          — no covering deviation

The authority is local-only (no CI job runs it), which is why 0.7.1c merged with this open.

Exact finding, from `./gradlew generateGlobalStateInventory`:

    module:      tramai-control-plane
    file:        .../controlplane/InMemoryWorkloadRegistrationStore.kt
    declaration: InMemoryWorkloadRegistrationStore : WorkloadRegistrationStore
    kind:        mutable-collection
    type:        HashMap
    mutable:     true

Classification: intentional architecture, not a defect and not a scanner-classification
error to be papered over. Every operation of that store runs inside `synchronized(lock)` on
a store-wide monitor, so both maps are mutated only under that lock and the
create/compareAndSet compound invariants stay atomic across them — that atomicity is the
authority contract, and its KDoc states the single-lock design deliberately.

Why not change the implementation instead: `GlobalStateInventory` matches a declaration from
its source text alone, and emits `threadSafety = "unknown"` / `lifecycle = "process"` as
constants for every match. Its collection regex flags `HashMap`, `mutableMapOf` and
`ConcurrentHashMap` identically, so swapping the collection type cannot clear the finding —
and a concurrent map would in fact break the cross-map atomicity the store requires. The
remaining honest options are a scanner capability (distinguish lock-guarded declarations,
a build-logic change) or retiring the reference store; both are recorded in the deviation.

Remedy: `MQ-0026`, metric `globalMutableState`, scope `:tramai-control-plane`, baseline 0,
allowed 1. The module did not exist at the 0.6.0 freeze so there is no committed identity to
inherit; `allowed: 1` covers exactly this finding and blocks growth in the module rather than
exempting it broadly. The canonical `0.6.0-baseline.json` is untouched (per config/quality
AGENTS.md rules 5 and 6 a scanner migration or direct baseline edit is not the mechanism
here).

Verified:
- verifyMaintainabilityBaseline — PASSED; verification-report.json records
  "1 new finding(s) — accepted by MQ-0026 (1 total in scope <= 1)"
- verify060Architecture — PASSED
- verifyChangePolicy -PchangeClass=quality-deviation -PchangePolicyBase=9d33e0e8 — PASSED,
  1 changed file, no violations
- spotlessCheck — green
- one file changed, no runtime or test source touched

Co-authored-by: TramAI Test <tramai-test@invalid>
…entity (#415)

Two independent policy changes settled for the 0.7 development line.

1. One version authority, release-bound migrations.

`tramaiVersion` becomes `0.7.0-SNAPSHOT` — the artifact version, and the value release
publishing already refuses to ship. `TramaiVersions.releaseVersionOf` strips exactly that
suffix, and API migration entries declare the resulting release, so `targetVersion: "0.7.0"`
is ACTIVE both during the 0.7.0-SNAPSHOT line and after the cut. The previous ACTIVE rule
compared against the raw project version, which made a truthful entry for an unreleased line
impossible: a 0.6.0 entry would have recorded false release history.

2. Stable means backward compatible, not byte-identical.

Contract-2 equated any stable dump inequality with instability, so a legitimate additive
stable change could not pass, and `:tramai-core` (stable) failed on the Epic base for exactly
that reason — a new `GovernedRunContinuityException` class with one constructor, nothing
removed or altered. `ApiDumpCompatibility` compares declarations by identity: a class's
declaration head must be preserved and its supertype set may only grow, and every base member
must still exist. Additions pass; removals, signature changes and visibility reductions fail;
migration entries still cannot authorize stable breakage.

The version surfaces follow the same split: `verifyVersionAlignment` enforces the development
surfaces against the project version and the release-scoped surfaces (dated CHANGELOG section,
release-readiness document, roadmap release train, consumer coordinates) against the release
they describe — plus a new invariant that a development line must target a release *after* the
last promoted one. No second version authority is introduced.

Verified (JDK 21, serial):
- ApiStabilityPolicyTest 17 executed / 0 failures — the settled policy table: additive passes;
  removal, signature change, visibility reduction and supertype removal fail; a breaking stable
  change fails even with an exact entry; preview needs an exact entry; a 0.7.0 entry is ACTIVE
  on 0.7.0-SNAPSHOT; a wrong target and a hash-mismatched entry fail; landed history under a
  later line authorizes nothing and is not itself reported stale
- ApiCompatibilityMutationTest 30/0 and ApiBaselineVerifierTest 13/0 (B1 inverted: additive must
  now pass; B1b added: stable removal fails even with an entry)
- verify060Architecture against the Epic base: `:tramai-core` and `:tramai-platform` no longer
  fail; only preview modules lacking entries remain
- verifyVersionAlignment: PASS at 0.7.0-SNAPSHOT, FAIL at 0.6.0-SNAPSHOT with "must target a
  release after the last promoted release 0.6.0" (three-state check)
- verifyStaticAnalysis: 0 new findings (4793 baseline)
- verifyChangePolicy -PchangeClass=build-logic -PchangePolicyBase=84c54726: PASSED
- verifyMaintainabilityBaseline, verifyCompilerWarnings, spotlessCheck: green
- one hardened store's deviation (MQ-0026) still reports accepted, unchanged

Refactor note: the version-alignment contract moved into VersionAlignmentVerifier.kt behind a
thin delegating member; every diagnostic message is preserved byte-for-byte, and the move also
clears the pre-existing LongMethod/line-length debt that the touched-file static-analysis
ratchet would otherwise surface.

Co-authored-by: TramAI Test <tramai-test@invalid>
* docs(0.7.1d): add authoritative run attribution task spec

Locks the 0.7.1d contract before implementation: RunId wraps the existing
workflowId (never a second identifier), attribution is snapshotted once at
run creation and never recalculated on resume, the runtime binding is a
pointer to 0.7.1c authority rather than authority itself, and missing or
conflicting attribution fails closed at every reconstruction boundary.

* feat(0.7.1d): establish governed run attribution continuity

One governed execution establishes exactly one GovernedRunIdentity, and that
identity must survive execution, checkpoint persistence, restart and resume
unchanged.

- GovernedRun: additive envelope carrying the canonical identity ALONGSIDE the
  existing WorkflowContext. WorkflowContext(workflowId, attributes) keeps its
  contract exactly, so ungoverned runs cannot acquire attribution and governed
  runs cannot silently lose it. The boundary invariant
  (context.workflowId == identity.runId.value) is enforced at construction.
- Attribution persists as reserved framework metadata keys of the checkpoint
  (a codec, not an authority): all keys absent means a legacy ungoverned
  checkpoint, a partial set is corruption and fails closed, and the reserved
  keys are written last so application metadata cannot override them. No
  WorkflowCheckpoint or store ABI change, and no store implementation changes.
- Resume now gates on WHOLE-identity continuity before any other validation:
  run-id-only matching, dropping any deployment component, or resuming a
  governed checkpoint without attribution are all rejected.
- Worker/recovery reconstructs the identity from the durable checkpoint
  instead of re-deriving it, and routes governed runs through the governed
  resume path.

Proven by GovernedRunAttributionTest (20 tests): envelope invariant, codec
round-trip through all four checkpoint stores, legacy un-attributed
compatibility, partial/invalid attribution fail-closed, the full substitution
matrix, and execution continuity through run/resume.

* feat(0.7.1d): thread the canonical run id into engine invocations

EngineExecutionIdentity.workflowRunId must be the canonical RunId inside a
governed execution; the engine must not mint a second workflow run id for the
same run.

- GovernedRunScope (tramai-core): execution-scoped carrier of the whole
  GovernedRunIdentity. The framework establishes it at the governed execution
  boundary (WorkflowRunner wraps each governed step execution), so subsystems
  invoked from application step code read identity instead of inventing it. It
  carries the whole identity, not just the run id, because a run id alone cannot
  detect a deployment/configuration substitution.
- InvocationExecutionCoordinator: reads the scope and reuses its run id
  verbatim. Without a scope the previous generated-identity behaviour is
  unchanged, so legacy/ungoverned invocations keep working exactly as before.
  Correlation ids remain engine-generated: they are a different concept.

Proven by GovernedRunScopeIdentityTest: a governed execution whose identity
source THROWS if sampled still completes, proving no run id is minted inside a
governed execution, while correlation is still sampled; outside a governed
execution the engine generates its own run id exactly once.

Known limitation (documented, not silent): the scope propagates through suspend
service calls. The blocking Java-friendly proxy path starts its own coroutine
context, so it does not observe the scope and keeps generating an id — governed
engine attribution therefore requires the suspend call path.

* feat(0.7.1d): bridge governed identity across the blocking proxy boundary

Both supported proxy paths must carry the same canonical run identity. Suspend
invocations inherit the caller's continuation context; blocking invocations
start their own runBlocking context, which previously dropped the attribution
entirely and let the engine mint a second run id for the same governed run.

- GovernedRunScope is now a ThreadContextElement: the identity is installed for
  the duration of the governed coroutine on a thread and restored afterwards,
  including on failure and cancellation. That is the sanctioned mechanism, so
  nested/sequential/parallel executions cannot leak into each other without any
  hand-managed state; the thread bridge is transport only and never regenerates
  or infers identity.
- TramaiInvocationHandler captures the attribution in force BEFORE runBlocking
  and installs the same scope inside the engine context.
- InvocationExecutionCoordinator resolves context first, thread bridge second,
  and still generates its own identity when no governed execution is in force.
- Marked with the existing internal-API marker: it is now absent from the public
  API dump (0 references), so it stays plumbing rather than application
  vocabulary.

Proven by GovernedRunScopeIdentityTest (9 discriminators): suspend and blocking
proxies both observe the exact identity and the identity source is never sampled
(an identity source that throws if used still completes the invocation); legacy
blocking keeps generating its own id; consecutive runs leave the bridge clear;
concurrent runs do not cross-contaminate; a SUSPENDED governed run does not
attribute an unrelated blocking call on the same thread; failure and
cancellation restore the previous thread context; dispatcher switches preserve
identity.

* feat(0.7.1d): bind governed schedules to a deployment and recover identity on wake-up

Scheduler attribution has two opposite requirements, and both are now explicit:

- a schedule is NOT a run, so it never stores a run identity. It is durably bound
  to a WorkloadDeploymentIdentity (new GovernedScheduleBindingStore capability,
  implemented by the in-memory and JDBC stores; the JDBC side adds one additive
  table, no ALTER of the released schedule tables). Every tick is a NEW
  execution: same deployment, FRESH run id.
- a delayed wake-up is a CONTINUATION, so it recovers the exact existing
  GovernedRunIdentity from the run's own durable checkpoint via the new
  recoverGovernedRun carrier, and never rebuilds it from the schedule or the
  binding that exists at wake-up time.

Consequences that fall out of that split: a schedule binding that changes while
a run is suspended cannot rewrite that run's identity, and a governed checkpoint
with partial or malformed attribution fails closed instead of degrading into a
legacy un-attributed wake-up.

ScheduleRecord's public constructor is untouched (the binding is a separate
durable capability); ScheduledWorkflowTimer gains an overload rather than a new
parameter, so the existing registration signature is unchanged.

Proven by GovernedScheduleAttributionTest (5 tests): governed tick runs under the
bound deployment with a fresh run id; two ticks share the deployment and never
the run id; a delayed wake-up continues the exact original identity even after
the binding changed; corrupted attribution fails closed without executing;
a schedule without a binding still runs un-attributed.

* feat(0.7.1d): make approval suspension carry durable whole-run attribution

Approval replay is an independent resume authority: it reconstructs from
SuspendedInvocationMetadata + ApprovalContinuation and never sees WorkflowContext
or the orchestration resume gate, so run-id-only attribution there could be
replayed against another deployment.

- GovernedSuspendedInvocation(metadata, runIdentity) states the bidirectional
  invariant (engine workflowRunId == canonical runId.value) once, at construction.
- GovernedSuspendedInvocationStore is an ADDITIVE capability rather than new
  methods on SuspendedInvocationStore, because third parties implement that SPI.
  A governed suspension REQUIRES the capability and fails closed with a
  ConfigurationException BEFORE creating any approval/continuation state — the
  degradation this prevents is 'governed until the first approval boundary'.
- Suspension: when a governed scope is in force, the suspension is persisted
  through the governed path as one record.
- Continuation: the durable witness is the authority. A standalone resume
  RECOVERS the persisted identity and installs it around the resumed execution;
  a caller-supplied identity is only a consistency precondition. Legacy record +
  governed resume is rejected, and a governed record + different identity is
  rejected before the continuation is claimed or the tool executes.
- ResumeApprovalCommand is unchanged: no identity fields are added to the
  command, so there is no second source to reconcile.
- GovernedRunContinuityException (tramai-core) is the explicit fail-closed
  contract for continuity violations.

Closes the indirect engine identity proof: the suspension record's engine
identity is now asserted equal to the canonical run id, and the tool observes
the exact recovered identity on resume.

Proven by GovernedApprovalAttributionTest (7 tests): persisted identity agrees
in both directions; standalone resume recovers and installs it; resume inside
the identical scope proceeds; all six component substitutions (workloadId,
configurationId, configurationVersion, environmentId, deploymentId, runId) are
rejected without executing the tool; a legacy suspension cannot be resumed from
a governed execution; a legacy suspension still resumes unchanged; a legacy-only
store fails closed before any durable state.

Also expanded the pre-existing wildcard imports in the two touched approval
coordinator files, which the formatting ratchet now requires.

* feat(0.7.1d): carry governed attribution in the file and JDBC suspension records

Both durable stores now implement the governed capability with the semantics the
in-memory store already had, and both keep their released V1 records readable as
legacy. No migration, no backfill.

File store: PersistedSuspendedInvocationRecordV2 carries metadata + replay
envelope + the whole GovernedRunIdentity in ONE encrypted record, written through
the same single atomicEncryptCreate as V1 — there is no second file, so a crash
cannot leave a half-governed suspension. Decoding dispatches on the OUTER schema
version: 1 legacy, 2 governed, anything else unsupported. A V2 that fails to
decode is corruption and is never retried as V1, because the version already
said the record claims to be governed. The bidirectional invariant
(engine workflowRunId == canonical runId.value) is checked on create and again
on decode.

JDBC: payload v2 inside the SAME encrypted payload of the same row and the same
single insert. No new columns and no migration — the existing decision that
workflow/actor identity stays inside the ciphertext rather than in plaintext
query columns is preserved. A payload with no version field at all is V1, which
is exactly the legacy case; v2 requires the identity, rejects blank components,
rejects a mismatched run, and an unknown version fails closed.

Also fixes a real classification bug found by the new tests: an unknown schema
version was being reported as corruption because the store wrapped the decode
exception; it now surfaces as unsupported-format, as before.

Classification is never derived from metadata reads: the engine asks the store
capability, so a governed record cannot be read as metadata and then resumed
through the legacy path. The engine tests already discriminate that — a governed
record resumed inside its identical scope must proceed and a standalone resume
must install the recovered identity; both fail if governance is erased.

Proven: 7 new tests in the file store suite (round trip, restart, legacy stays
legacy, mismatched run, partial identity, unknown version, duplicate semantics)
and 8 in the JDBC suite (same matrix plus a plaintext-column scan proving the
attribution is not promoted into queryable columns). Full suites for both
persistence modules are green, including the legacy SPI TCK and the restart
tests.

* feat(0.7.1d): admit governed server runs from the authoritative registration

Server-level governed deployment (option A): one server instance runs as one
workload deployment. Unset is the default and every legacy path is unchanged.

Authority: new additive WorkloadRegistrationAuthority.resolveForNewRun(expected)
is the minimal internal admission lookup — not a query surface. It rejects an
absent registration, a registered identity that differs anywhere including
configuration id/version, and a registration that is not ACTIVE, and returns the
AUTHORITATIVE identity. Lifecycle is checked here, at new-execution admission
only; a later SUSPENDED/RETIRED transition does not retroactively invalidate an
already-admitted run. WorkloadAdmissionRejectedException carries a stable reason
code.

Server: the edge to tramai-control-plane is implementation-scoped, and no public
signature mentions a control-plane type. WorkflowController's public constructor
is untouched — the seam is an internal property populated by ServerConfiguration,
so apiCheck shows only additive entries (ServerGovernance, WorkflowEntry overloads).

Start ordering, exactly as specified:
1. findByIdempotencyKey first: an existing run wins, with no admission lookup and
   no new RunId;
2. otherwise resolve the configured deployment through the authority — a
   rejection happens before any run record exists;
3. fresh RunId candidate;
4. getOrCreate: created starts the governed execution with the authoritative
   deployment and that RunId; losing the race returns the winner's run and
   launches nothing.
The run id IS the canonical RunId: no second identifier is generated.

Resume: always recover identity from the checkpoint first, never from
configuration or registration. configured == persisted deployment, else fail
closed; no governed configuration means fail closed rather than degrade; a legacy
checkpoint stays legacy even on a governed server. No lifecycle re-consultation.

Partial governed configuration fails closed at startup, and governed
configuration without a registration source is refused — a typo must not
silently start an ungoverned server.

Proven by ServerGovernedRunAttributionTest (13 tests): registered ACTIVE starts
governed with the run id as RunId; two runs get distinct run ids; unregistered
deployment creates no run record and executes nothing; SUSPENDED and RETIRED
cannot start; configuration-version-only substitution is rejected; an idempotent
retry still succeeds after the registration is suspended; 8 concurrent
same-idempotency requests yield one run and one execution; suspend/resume
preserves the exact identity; a lifecycle change after suspension does not block
the run; a different deployment cannot resume it; an ungoverned server cannot
resume a governed checkpoint; a legacy run stays legacy; partial configuration
fails closed. Full server suite (72 tests incl. the Spring MockMvc context) and
control-plane suite are green; spotlessCheck green; apiCheck additive only.

* fix(0.7.1d): review round 1 — API leak, resume ordering, guard, static analysis

Correction round on the reviewed head ff89cf9. Four fixes, plus the static-analysis
cleanup they required. No production semantics changed beyond what each fix states.

1. Server public-API leak. ServerGovernance and its factories are internal, so neither the
   type nor WorkloadRegistrationAuthority appears in the server API dump; the module edge
   stays implementation-scoped and Spring still discovers the internal @configuration class
   (proven by the MockMvc context test). The governed server beans moved to their own
   configuration class so ServerConfiguration is byte-identical to its base revision.

2. Authorization before resume mutation. The order is now requireResumable -> recover the
   persisted GovernedRun? -> authorizeContinuation -> markResuming -> launch with the
   already-recovered envelope, never re-derived. An unauthorized resume can no longer move
   another run out of DELAYED; the negative tests assert a synchronous rejection AND that
   the run is still DELAYED with no step executed, and a concurrency discriminator shows
   only one caller passes the admitted-resume transition. Authorization itself mutates
   nothing.

3. Runtime-identifier guard. The five governed server properties are declared exactly as
   configPropertyLiterals, with no prefix exemption and no weakened rule. The other real
   offenders were the reserved checkpoint metadata keys, which were squatting the tramai.
   namespace the guard reserves for runtime identifiers and configuration properties; they
   are now checkpoint.identity.*, with the reason recorded next to them. Guard test and the
   full observability suite pass.

4. Static analysis: 84 -> 0 new findings, with the legacy baseline count unchanged at 4793
   and no baseline edits, no config changes and no suppressions. The reduction comes from
   structural change:
   - Server: explicit layering — @value -> ServerGovernanceProperties -> pure parser ->
     ServerGovernance, so the all-or-nothing rule is provable without a container.
   - Orchestration: WorkflowExecutionFrame and WorkflowSessionInputs replace parameter
     plumbing (the step loop drops from eight parameters to four, the persistence session
     from ten to five); resume is split into load+validate, establish, execute; run and
     resume share one terminal failure handler.
   - Relocations rather than legacy refactors: the governed capability moved off the
     suspension stores and the scheduler stores into their own types. Adding an interface
     to a released class changes the analyzer identity it is baselined under, which
     unmasks unrelated legacy debt; composition avoids that without weakening anything.
   - Durability is unchanged by construction: the suspension wrappers delegate into the
     SAME single atomic write (one encrypted file, one encrypted row, identity inside that
     record). No sidecar, no second transaction, so a crash can never leave a suspension
     without its attribution.
   - Persistence failure modes are pinned by tests: exact whole-identity recovery after
     restart, partial/malformed attribution failing closed, legacy staying legacy,
     attribution never in plaintext columns, and removal leaving no stale attribution.
   - The shared terminal failure path has a direct cancellation discriminator on both run
     and resume.

Verified: core, engine, orchestration, scheduler, server, control-plane, observability,
persistence-file, persistence-jdbc and the JDBC sovereign starter suites; spotlessCheck;
apiCheck with the regenerated dumps reviewed (the only deltas are this round's intended
relocations and visibility changes); verifyChangePolicy (public-api, 110 changed files, no
violations); verifyStaticAnalysis at zero; verifyPr.

* fix(0.7.1d): make governed schedule classification durable, not registration-local

Blocker: a governed schedule could be silently downgraded to unattributed ticks.

The tick read the binding through the current in-memory registration, so any later
ordinary re-registration (or a restarted process registering through the ordinary
overload) hid the surviving durable binding and the next tick ran unattributed. The
tick now discovers the binding capability from the STORE:

  binding present                      -> governed; exactly the persisted deployment
  no binding + governed registration    -> fail closed; nothing executes
  no binding + ordinary registration    -> legacy

CompositeWorkflowSchedulerStore composes the released scheduler store with the binding
store by interface delegation, so the timer sees the capability independently of the
current registration and both legs stay the released implementations. The old
declaration-level fallback is gone: a transient declaration never becomes runtime
authority again.

Governed registration now persists fail-safe: binding -> schedule -> publish the
in-memory registration, so a failed binding write cannot leave an executable schedule
behind.

Discriminators (all verified to fail under the previous behaviour): an ordinary
re-registration cannot downgrade a governed schedule; a restarted timer cannot
downgrade a surviving durable binding; an explicitly governed registration without a
durable binding executes nothing; a failed binding write publishes no schedule and
executes nothing; a genuinely legacy schedule still runs unattributed.

Compiler warnings: the three engine files that use the internal governed-scope
plumbing opt in explicitly (@file:OptIn), and the new persisted fields (3 V2 record
fields and 6 identity fields) carry explicit annotation use-sites instead of a bare
@JsonProperty. No baseline change.

Verified: scheduler and all touched module suites, spotlessCheck, verifyStaticAnalysis
(zero new findings), verifyCompilerWarnings (gate green, 129 baseline identities),
verifyCancellationSafety, apiCheck/apiDump, verifyChangePolicy.

* fix(0.7.1d): assert the governed suspension wiring in the JDBC E2E

The jdbc-profile E2E asserted the injected SuspendedInvocationStore was exactly the plain
JdbcSuspendedInvocationStore. The governed suspension capability composes on top of it (one
row, one transaction, the whole identity inside that row's payload), so the bean is the
governed wrapper delegating into the JDBC store. The assertion now states that contract,
matching the sibling starter test, and keeps the exact-type teeth: the wrapper is only ever
constructed around the PostgreSQL-backed store.

Found via the release-candidate workflow's validate job, which also failed on the previously
reviewed head. Verified: :examples:spring-sovereign-starter:e2eTest green (both profiles).

* fix(0.7.1d): assert the governed suspension wiring in the file starter test

The file persistence starter's auto-configuration test asserted the injected
SuspendedInvocationStore was exactly the plain FileSuspendedInvocationStore. Since the governed
suspension capability composes on top of it (one file, one write, the whole identity inside
that file's encrypted payload), the bean is the governed wrapper delegating into the file
store. Same correction as the JDBC E2E, in the sibling starter module that had not been part of
the local battery — the wiring contract that changed is round 1's, deliberately.

Verified: :tramai-spring-boot-starter-sovereign-persistence-file:test 18 tests, 0 failures,
0 skipped, and the two cases CI reported as failing both execute;
:tramai-spring-boot-starter-sovereign-persistence-jdbc:test green.

* fix(0.7.1d): keep the file starter test ktlint-clean after the wiring assertion fix

The wiring commit edited SovereignFilePersistenceAutoConfigurationTest without re-running the
formatter, so spotlessCheck (CI lane 'quality (static)') failed on it. The file was not
ktlint-clean to begin with, so the ratchet reformatted it: import ordering and blank lines
only, no behaviour change.

Verified locally, the exact three gates that lane runs: spotlessCheck, verifyStaticSafetyGuards
and verifyStaticAnalysis (0 new findings, baseline 4793). File starter suite green.

* fix(0.7.1d): resolve the ktlint/detekt line-length conflict in the file starter test

The reformat in the previous commit unmasked a detekt MaxLineLength finding: a fully-qualified
approval type made the one-line signature 126 characters. ktlint (limit 140) collapses that
signature back onto one line while detekt (limit 120) rejects it, so the two gates disagreed
about the same line. Importing the type removes the conflict at its source rather than picking
a side: the signature collapses to 70 characters and both gates accept it.

Verified locally by exit code, the exact three gates that CI lane runs: spotlessCheck 0,
verifyStaticAnalysis 0 (zero new findings, baseline 4793), file starter suite 0.

* fix(0.7.1d): enroll the governed suspension stores in the shared TCK contract

CI's tests job failed on the architecture guard in tramai-testing: every concrete
SuspendedInvocationStore implementation must ship a <Store>TckTest runner extending
SuspendedInvocationStoreTck in its own module. Round 1 added two implementations (the governed
file and JDBC wrappers) without enrolling them, and tramai-testing was not part of the local
battery — the same coverage gap that let the starter assertion slip.

The two runners compose the wrappers over the same stores the existing runners build, so the
delegation is now proven transparent for the whole released contract rather than only for the
governed paths: 39 cases each, both modules, no production code changed.

Verified by exit code: :tramai-testing:test (the failing guard) 0, :tramai-persistence-file:test
0, :tramai-persistence-jdbc:test 0, spotlessCheck 0, verifyStaticAnalysis 0 (zero new findings).

* fix(0.7.1d): use the non-deprecated PostgreSQLContainer in the governed JDBC TCK runner

verifyCompilerWarnings failed with 2 uncovered warnings on the new runner: the deprecated
org.testcontainers.containers.PostgreSQLContainer class plus its generic argument. The
non-generic org.testcontainers.postgresql.PostgreSQLContainer is the same fluent API and is
already used by two other files in this module.

Verified by exit code: verifyCompilerWarnings 0 (gate green, 129 baseline identities),
:tramai-persistence-jdbc:test 0 with the governed TCK at 39 cases / 0 failures, spotlessCheck 0.

* evidence: define governed runtime attribution contract

Commit 1 of the evidence-emission boundary for 0.7.1d: the contract only, with no
exporter or source-side wiring yet.

RuntimeEvidenceAttribution is a string-level codec, deliberately free of any
GovernedRunIdentity dependency so tramai-security gains no edge on the core identity
model. The run component is not carried in metadata at all: RuntimeEvidenceRecord.workflowRunId
stays the single canonical run identifier, so a second run identifier can never disagree
with the first. Only the deployment tuple is added, as five reserved keys.

Validation is atomic and fails closed: zero keys is legacy evidence and stays valid, all five
keys require a non-blank canonical run id, and a partial tuple is corruption rather than a
silent downgrade. merge() overlays framework attribution last so caller metadata can never
substitute identity.

Composition lives in RuntimeEvidenceContractValidator, not in the bundle writer: the writer's
per-family sets are event-family vocabulary, governed attribution is cross-family framework
metadata, and the validator is the boundary that already owns metadata admissibility. The
writer therefore stays byte-identical to 444c99e -- no legacy file is touched, and no
pre-existing finding is unmasked or baselined.

Invariant for the follow-up propagation commit: AuditEvent.metadata may STORE authority but
may never ORIGINATE it. The five strings must be injected by the framework before
AuditEngine.emit() so they are covered by the event hash, and the exporter may only copy the
exact reserved keys, overlay them onto the effective metadata, and then compute the payload
digest -- never after.

Verified: :tramai-security:test 0 (837 tests, 0 failures), spotlessCheck 0,
verifyStaticAnalysis 0 new findings.

* feat(0.7.1d): V2 file persistence codec for governed outbox attribution

Durable governed approval/outbox attribution, file persistence half, plus the
ops-side types the boundary needs.

- GovernedSovereignOpsAuditOutboxRecord: ordinary record + complete canonical
  GovernedRunIdentity, with runIdentity.runId.value == record.workflowRunId.
- GovernedSovereignOpsAuditOutboxStore / SovereignOpsAuditOutboxGovernance:
  additive capability. JVM-public because file/JDBC live in separate modules, so
  Kotlin internal cannot serve them; explicitly marked
  @ExperimentalTramaiInternalApi rather than presented as stable user-facing API.
- ApprovalRunAttribution + resolveApprovalRunAttribution: provenance is resolved
  from record existence first. A null identity means legacy for an existing
  suspension and nothing at all for an absent record, so an absent record is
  never silently treated as legacy and NoSuspension is left to the caller's
  policy. The identity comes from the store's governed capability, never from
  ApprovalRequest.binding.workflowRunId.
- FileSovereignOpsAuditOutboxCodec: the single file codec and the only place
  that decides V1 vs V2. V1 is permanently legacy; V2 nests the V1 DTO verbatim
  plus one non-optional identity object, so a partially attributed V2 is not
  constructible. Unknown schema version is an unsupported format (never an old
  record), malformed content is corruption, and a V2 identity that disagrees
  with the ordinary record's workflowRunId fails closed.
- FileSovereignOpsAuditOutboxStore: every persistence path (mutate, claimPending,
  markReadyForDispatch, markEmitted, markFailed, get, findByEventKey, listPending,
  listByStatus, listExpiredEmitting, rebuildIndex, verifyAll) now decodes, mutates
  only the ordinary record, and re-encodes with the decoded identity carried
  around the mutation. The previous update helper handed the updater a domain
  record and re-serialised it, which was a live V2->V1 downgrade path.

Verified by exit code: file module tests 0 (109 tests, 0 failures; outbox TCK 64,
store suite 27 - the legacy V1 contract held through the new paths),
compileKotlin 0, spotlessCheck 0, verifyCompilerWarnings 0 (gate green, 129
baseline identities), apiDump 0 then apiCheck 0, verifyChangePolicy 0
(public-api, 123 files).

NOT DONE, next in order: V2 discriminators including the full restart lifecycle,
governed append/read entry points plus wrapper, JDBC V2 payload, governed JDBC
mutation inside its existing single transaction, in-memory governed mutation,
then dispatcher/emitter/exporter wiring.

Known gate state: verifyStaticAnalysis reports 4 findings unmasked by editing
this legacy file - ThrowsCount in validateManagedDirectory and validateRegularFile,
MaxLineLength at lines 93 and 183. All pre-existing, deliberately not refactored
here per the mandate against refactoring unrelated legacy code because Detekt
exposes it, and not baselined or suppressed.

* fix(0.7.1d): close the four static findings on the file outbox slice

All four are fixed at the cause; none baselined, none suppressed.

MaxLineLength (2): both were my commit's formatting, not legacy code. spotlessApply
collapses expression bodies, and ktlint requires the collapsed form, so the fix is to
shorten the expressions until the collapsed form also fits Detekt's 120:
- toPersistedV1's parameter is now `version`, taking the collapsed signature to 115;
- getLockForDigest's parameter is now `key`, taking its collapsed body to 118.
The alternative (manually wrapping after every spotlessApply) was rejected: the
formatter would re-collapse on the next run, which is how these two appeared.

ThrowsCount (2): validateManagedDirectory and validateRegularFile each had three throws.
They now call a `permissionFailure(message)` helper, so the exception type and every
message are byte-identical while the validators carry no throw statements. The helper is
hosted in the file codec rather than beside its callers, because the legacy store file
sits at its TooManyFunctions ceiling and adding a top-level function there reproduced a
third finding. Verified both ways: with the helper in the legacy file the count tipped
over, so it lives in the codec with the reason documented.

Verified by exit code: spotlessCheck 0, verifyStaticAnalysis 0 (zero new findings),
:tramai-spring-boot-starter-sovereign-persistence-file:test 0 (109 tests, 0 failures).

Still open on this slice, before JDBC: replace the nullable findGovernedById with
governanceById returning SovereignOpsAuditOutboxGovernance, implement the governed file
store wrapper, and prove the V2 transition/restart lifecycle plus the malformed/mismatch
fail-closed discriminators.

* feat(0.7.1d): governed file outbox continuity reference

Closes the file half of the governed approval/outbox path.

governanceById replaces the nullable findGovernedById. Provenance is resolved, not
nullable: V2 exists -> Governed, V1 exists -> Legacy, absent -> NoRecord, so a missing
identity can no longer be read as legacy or corruption by accident.

GovernedFileSovereignOpsAuditOutboxStore composes the released file store by delegation:
its interface list stays untouched, legacy behaviour stays transparent, and the governed
capability adds appendGoverned plus the resolved read. One encrypted atomic record, no
second file, no identity sidecar.

appendGoverned persists the ordinary outbox fields and the complete identity in a single
write and keeps the duplicate-id and duplicate-event-key protections. It returns nothing:
the caller already holds the governed record, and repeating that long return type was what
pushed the signature past the line limit in the first place.

Discriminators: the full lifecycle across two real store restarts (append PREPARED,
markReady, claim, markFailed retryable, reclaim, markEmitted) asserting complete
GovernedRunIdentity equality at every observation, plus the fail-closed matrix - unknown
schema is unsupported format, malformed payload is corruption, V2 with a mismatched run id
is corruption, partial V2 is corruption, V1 stays V1 and never re-encodes as V2, a governed
payload stays V2 across rewrites, legacy reads as Legacy, absent reads as NoRecord, and
duplicate protection holds for governed appends.

Two lessons encoded in the code: file-level @OptIn where an experimental governed type is
named, and no renaming of an override's parameter without renaming the supertype's (that
warning is indistinguishable from a real one in a delta-scoped gate).

Verified by exit code: spotlessCheck 0, verifyStaticAnalysis 0, verifyCompilerWarnings 0,
apiCheck 0, verifyChangePolicy PASSED (public-api, 125 files), file module 117 tests /
0 failures including the 8 new governed discriminators.

* fix(0.7.1d): enroll the governed file outbox store in the shared TCK

CI's tests job failed on 67f827c, and the cause was a real candidate defect, not
infrastructure: SovereignOpsAuditOutboxStoreTckEnrollmentArchitectureTest requires every
concrete SovereignOpsAuditOutboxStore implementation to ship a <Store>TckTest runner in its
own module. GovernedFileSovereignOpsAuditOutboxStore is a concrete implementation, and round
one of this slice enrolled none.

Enrolling the wrapper is worth more than satisfying the guard: the released outbox contract
now runs through the delegated layer, which proves legacy behaviour stays transparent instead
of only asserting it in KDoc. 64 TCK cases pass through the wrapper, 0 failures.

This is the third time this slice family has been missed by a local battery - the same
architecture guard family caught the two governed suspension stores earlier, and it belongs in
the pre-push battery for this module alongside the module's own tests.

Verified by exit code: enrollment guard 0, file module 181 tests / 0 failures (64 governed
TCK + 8 governed discriminators), spotlessCheck 0, verifyStaticAnalysis 0,
verifyCompilerWarnings 0, apiCheck 0, verifyChangePolicy PASSED.

* feat(0.7.1d): governed JDBC outbox continuity

The JDBC outbox gains the same V1/V2 payload shape the file store already has,
through one shared codec every writer of an outbox row now uses:

- V2 carries the complete canonical GovernedRunIdentity as one non-optional
  nested object; V1 stays byte-for-byte the released shape
- a payload with no declared schema version, or one the codec does not know, is
  never read as legacy; a partial V2 and an identity naming another run fail
  closed with a distinct code
- every released transition decodes the row's identity and re-encodes it, so a
  status transition cannot drop attribution as a side effect
- GovernedJdbcSovereignOpsAuditOutboxStore exposes the governed capability by
  composition and is enrolled in the shared outbox TCK in this same commit

No side table, no identity column: attribution lives in the same encrypted row.

Verification: :tramai-spring-boot-starter-sovereign-persistence-jdbc:test 328
tests / 0 failures; governed TCK 64/64; governed outbox 14/14; spotlessCheck,
verifyStaticAnalysis (4793 baseline, 0 new), verifyCompilerWarnings green.

* feat(0.7.1d): record governed attribution in the JDBC approval mutation

The approval mutation keeps its native transaction and its rollback precedence.
When the approval's durable suspension carries the canonical identity, both
outbox writes inside that transaction encode V2 with it:

- provenance is resolved BEFORE the transaction opens, so a resolution failure
  fails closed with the approval untouched and the transaction keeps its shape
- appendGoverned() is never called from inside the transaction: the shared
  codec is used inline, so nothing commits independently mid-transaction
- the already-existing, unused resolveApprovalRunAttribution becomes public
  experimental API rather than a second copy of the classification
- injected-statement failures cover the outbox insert, the approval update and
  the final outbox update; each leaves no half-governed mutation behind

ABI: JdbcSovereignOpsApprovalMutationStore and the autoconfig bean method gain
one parameter with a default (source-compatible); both apiDump files updated.

Verification: shell 6/6 mutation discriminators, 328-test JDBC module suite and
292-test ops suite green; spotlessCheck, verifyStaticAnalysis, verifyCompiler-
Warnings, verifyChangePolicy green.

* fix(0.7.1d): make the governed test evidence actually execute

`verifyJUnitTestSignatures` (a verifyPr gate) failed on this branch, and the
cause is a test-validity hole rather than a style nit: 13 `@Test` functions were
declared with an expression body whose JVM return type is not void, which JUnit
Jupiter silently discards at discovery. The suite reported green with those
tests never running.

Converted to block bodies (the only form the gate accepts when the expression
head sits on a continuation line):

- FileGovernedSovereignOpsAuditOutboxStoreTest: 8 tests, including
  `governed identity survives every transition and two restarts` and
  `partial V2 identity fails closed`
- GovernedRunAttributionTest: 6 tests, including
  `governed attribution round-trips unchanged through every checkpoint store`

Turning them on surfaced a real defect that had been invisible: the partial-V2
corruption fixture tampered for `"deploymentId":"dep-1",`, but `deploymentId` is
the LAST field of the persisted identity, so the replacement never matched, the
tamper was a no-op and only the "tamper must change the payload" assertion
failed. The fixture now removes `,"deploymentId":"dep-1"` and the test proves
the codec fails closed for the reason it claims.

Verified: FileGoverned 10/10 executed (was reporting green with 8 skipped),
:persistence-file:test, :persistence-jdbc:test, :sovereign-ops:test and the
orchestration attribution class — 823 tests, 0 failures; spotlessCheck,
verifyStaticAnalysis (4793 baseline, 0 new), verifyCompilerWarnings green.

Not fixed here: `verifyJUnitTestSignatures` still reports 53 findings in
tramai-control-plane, JdbcWorkloadRegistrationStoreTest and
WorkflowTerminalFailureTest — all present at this branch's base and outside this
PR. That repo-wide debt needs its own change; this commit brings this PR's own
files to zero.

* feat(0.7.1d): fail closed when a governed run reaches an un-attributed approval gateway

0.7.1d requires that a governed run's canonical identity survives supported
runtime boundaries and that governed/legacy classification never silently
downgrades. Two public, auto-configured approval entry points could break that:
both build an `ApprovalGatewayPersistenceRequest`, which carries no
`GovernedRunIdentity`, and persist approval/suspension/continuation records
through the un-attributed path. A governed execution that suspended through
either one would have durably recorded a run whose attribution had vanished,
with nothing to signal the loss.

Both writers now fail closed before any persistence happens:

- `DefaultApprovalGateway.requestApproval` (preview adapter, also the
  auto-configured fallback) rejects before the request factory is even called.
- `SovereignOpsTransactionalApprovalGateway.requestApproval` rejects before the
  mutation store is touched, via a private `rejectGovernedRun()`.

The discriminator is the existing `GovernedRunScope.resolve(currentCoroutineContext())`
(authoritative for the caller's coroutine, already used by the guarded
suspension path) and the existing `GovernedRunContinuityException`. The failure
message names the run so the rejection is diagnosable. Both class docs now
declare the path unsupported for governed runs instead of leaving that
implicit.

This adds no capability: it stops 0.7.1d from claiming continuity that an
existing public approval route can erase. Carrying `GovernedRunIdentity`
through `ApprovalGatewayPersistenceRequest` remains a follow-up (the larger
redesign is deliberately out of scope).

Verified — an active governed scope must reject with nothing persisted:
- DefaultApprovalGateway: rejection + approval rows 0, suspension rows 0,
  continuation rows 0
- transactional gateway: rejection + mutation store never called, no audit
  intent, no durable rows
- both tests fail if the guard is removed; ungoverned execution unchanged

Gates: spotlessCheck, verifyStaticAnalysis (4793 baseline, 0 new),
verifyCompilerWarnings (gate green), apiCheck (no ABI change — no member
added, removed or changed), verifyChangePolicy; engine 12/12 and ops 12/12
gateway suites; example E2E 18/18 (approval gateway golden path, JDBC E2E,
sovereign runtime E2E, profile smoke).

Note: local gates must run on JDK 21. On this machine's default JDK 25 the
kotlinx ABI validator fails with "Unsupported class file major version 69",
which is a toolchain mismatch, not a code defect.

* feat(0.7.1d): authorize the slice's preview API transitions for the 0.7.0 line

`verify060Architecture`'s api-architecture gate requires an exact hash-bound migration entry
for every preview/experimental dump transition, and eight modules change dump in this slice.
This records the eight transitions the slice owns, each declaring the release this line
targets (`targetVersion: "0.7.0"`, the release of the `0.7.0-SNAPSHOT` development version),
so the entries stay truthful during development and after the release cut.

Hashes come from this exact rebase, not from earlier measurements: `fromSha256` is the
post-#415 Epic base dump and `toSha256` is the rebased slice dump, both taken directly from
the gate's own diagnostics.

Seven transitions are purely additive — new governed types only: the admission rejection
(`:tramai-control-plane`), the governed suspension and run types (`:tramai-engine`,
`:tramai-orchestration`), the file and JDBC governed stores, governed scheduling, and the
governed approval attribution. The eighth, `:tramai-spring-boot-starter-sovereign-persistence-jdbc`,
adds a constructor parameter with a default: source-compatible, but its JVM descriptor changed,
so its entry records the recompilation requirement instead of claiming the change was additive.

No stable module needs an entry: `:tramai-core`'s change is additive and passes under the
backward-compatibility rule settled in #415, so this slice carries no inherited API blocker.

Verified: verify060Architecture PASSED against the exact Epic base e8ad713.
Before these entries the same gate failed with exactly these eight modules and nothing else.

* fix(0.7.1d): strict schema-version dispatch, and a task authority matching the implementation

Two candidate-owned blockers from the review of 46adb2f.

1. V1/V2 dispatch no longer coerces a schema declaration.

All three governed codecs read the declared version through `asInt()`, which coerces `"2"` and
`2.5` to 2 and a non-numeric text node to 0. A damaged payload could therefore decode as a valid
V1/V2 record, or be reported as an "unsupported version" when it is simply damaged. A schema
declaration is now an integral, in-range JSON number or corruption: missing, null, text, float and
out-of-range are corruption; 1 and 2 decode; any other integral value is an unsupported version.
Applied at all three sites — file suspended-invocation, file sovereign-ops outbox, JDBC
sovereign-ops outbox.

Each family gained a wrong-type discriminator, and each was verified to fail against the old
coercive read before the fix (JDBC 1 failure, file suspended-invocation 1, file outbox 1). The two
outbox discriminators use a well-formed governed payload, not a stub, so a coerced decode would
genuinely succeed and the test genuinely bites.

2. The 0.7.1d task authority now describes the architecture it governs.

The task document still claimed attribution carried by `WorkflowContext` / `WorkflowRunRecord` /
`WorkflowCheckpoint` with "JDBC explicit identity columns (not JSON, not metadata)" — the opposite
of what was built. It now states the implemented design: `GovernedRun` as the typed runtime
envelope with `WorkflowContext` unchanged; the five reserved `checkpoint.identity.*` keys as an
internal persistence encoding rather than an authority, with the run id carried by the checkpoint's
own `workflowId` and never duplicated; unchanged checkpoint-store schemas; V2 suspension/outbox
provenance; the durable scheduler binding; continuation recovering the persisted identity and
comparing the whole identity rather than the run id. Mutation/adversarial certification is
explicitly deferred to 0.7.1g per the Epic, and the compatibility section now states that the JDBC
starter constructor change is source-compatible through a default but not JVM ABI-neutral.

Also removes the two `setString` calls that the six-value loop immediately overwrote in
`JdbcGovernedScheduleBindingStore` (review cleanup, no behaviour change).

Verified (JDK 21, serial):
- focused suites (persistence-file, file and jdbc sovereign persistence starters, scheduler):
  936 tests, 0 failures
- the three wrong-type discriminators fail against the old coercive read, pass with the strict one
- verify060Architecture -PchangePolicyBase=e8ad7136 PASSED — no API dump changed, so no migration
  hash moved
- verifyChangePolicy -PchangeClass=runtime-behaviour -PchangePolicyBase=e8ad7136 PASSED (83 files,
  0 violations)
- verifyJUnitTestSignatures clean; verifyStaticAnalysis 0 new findings (4793 baseline);
  verifyMaintainabilityBaseline PASSED; verifyCompilerWarnings green; spotlessCheck green

* docs(0.7.1d): state the schema-version contract the codecs actually implement

The corrected task authority still carried one contradictory sentence: it called a declared but
unsupported schemaVersion corruption. In the implemented contract an *unknown integral* version is
unsupported format; a malformed or mistyped declaration is corruption, and neither case is legacy.
The sentence now says exactly that. Documentation only: no runtime code, no API dump and no
migration hash changes.

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
* docs(0.7.1e): specify the control-plane authority contract

Specifies 0.7.1e before implementation, per the repo rule that a task may not silently widen its
parent Epic and the two authority-vs-implementation drift rounds 0.7.1d cost.

Central invariant: commands reach authoritative state, queries reach authoritative state or an
explicitly classified read projection, a query/projection path cannot mutate runtime authority, and
mutation carries the expected authoritative state version where concurrency matters - a stale
expected version is rejected, never overwritten.

The audit before invention matters here: 0.7.1c already built the concurrency machinery. The
authority already returns typed outcomes (MetadataUpdateOutcome / LifecycleTransitionOutcome with
Stale(currentVersion, expectedVersion)) and already reports the CURRENT authoritative version on a
lost race; compareAndSet is atomic on deployment scope + state version + immutable witness; and
WorkloadStateVersion is monotonic. So this slice builds the external contract around existing
machinery and proves the separation - no CQRS framework, no new concurrency control.

Two facts that settle placement: tramai-control-plane is framework-agnostic (its only api
dependency is tramai-core), so the HTTP If-Match/ETag surface belongs in tramai-server, which
already wires governance in ServerGovernance and already answers with ProblemDetail. And nothing in
the repository implements If-Match today, so this defines the contract rather than extending one.

* docs(0.7.1e): freeze the command port, the 412 semantics and the If-Match form

Review rulings applied to the spec before freezing it.

Port shape: the command port mirrors the authority semantic shape (register, updateMetadata,
transitionLifecycle with expectedVersion) instead of inventing convenience operations, and
WorkloadRegistrationAuthority IMPLEMENTS the port - no forwarding wrapper that would duplicate the
authority. Lifecycle targets are expressed through transitionLifecycle(target = ...), keeping one
place for lifecycle rules. Reads get their own public port, WorkloadControlPlaneQueries, because that
separation is what this slice actually adds.

HTTP semantics: 412 for a valid-but-stale precondition, 409 only for a current version with an
illegal transition, 428 for a missing mandatory precondition, 400 for a precondition that is present
but malformed or unsupported - the absent/malformed overlap is gone. The 412 body carries both
expectedVersion and currentVersion and the response returns the current ETag, so a client can
reconcile without parsing prose.

If-Match form is frozen to exactly one strong numeric ETag. W/"17", "*", "16", "17" and non-numeric
values are rejected as unsupported forms - in particular "*" must not satisfy a mutation without
naming an expected version. Covered by test-matrix item 11 and an adversarial case.

Also records explicitly what is public contract (semantic concepts: expectedVersion, Stale,
QueryConsistency, observedVersion, the command/query ports) versus what stays server-only
(If-Match/ETag parsing, status codes, ProblemDetail, Spring types, DTO mechanics).

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
* fix(build): recognize multi-generation API migration history

The registry history rule accepted a past entry only when its toSha256 was the module current
base hash, so a module that transitioned twice could never both keep its older entry and have
the newer one land: after the newer transition landed, the older entry was reported stale
forever, clearable only by deleting release history.

Validity is now reachability to the module real base hash, walked through the module entries as
a sequence: no successor, two or more successors, or a repeated hash is not history. The trust
anchor stays the base dump from Git, so fabricated entries cannot corroborate each other, and
ambiguous branching is rejected because a migration history is a sequence of authoritative API
states rather than a graph.

Contract-2 is unchanged: a live base-candidate transition still needs an exact ACTIVE entry, so
history never authorizes a later change.

Seven chain discriminators added, plus a repo-level proof: verify060Architecture now PASSES on
the real registry (it failed on the two historical 0.5.0 entries that connect into the 0.7.0
ones) with nothing deleted or rewritten.

verify060Architecture also now runs in CI on every PR, via the JDK 21 maintainability workflow
(AGENTS.md rule 6) with the exact PR base, and ApiStabilityPolicyTest — previously in no lane —
is enrolled in scanners-coverage (254 -> 278).

* ci: run the architecture authority on every PR

The gate first landed in maintainability-baseline.yml, which triggers only for PRs against
master - so on every epic-targeted slice, where the API/migration policy actually drifts, it
would never have run. That is the same disease (an authority nobody executes) with a new coat.

It now has its own workflow: all pull requests plus master pushes, JDK 21 (build-logic
authority, AGENTS.md rule 6) rather than the JDK 25 production matrix, exact PR base SHA with a
fallback for the all-zero before-SHA on new-branch pushes, base resolution via fetch-depth 0, and
the architecture report uploaded on failure.

The API compatibility/policy suites were in the same blind spot: they lived only in the
master-gated workflow. A new ci.yml contract-tests partition (api-compatibility, 67 tests =
ApiCompatibilityMutationTest 30 + ApiBaselineVerifierTest 13 + ApiStabilityPolicyTest 24, count
verified locally) runs them on every PR.

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
…concurrency adapter (#420)

* fix(observability): declare the control-plane configuration prefix

CI's full `tests` job caught what the focused local suites could not: the repository-wide
tramai-literal scan (RuntimeEventCatalogueArchitectureTest) rejects any `tramai.` string outside the
runtime event catalogue, and the adapter's gate prefix `tramai.control-plane.http` is such a literal.

The scan's own doctrine is that configuration is not protocol and that the ONLY sanctioned `tramai.`
literals are the exact declared Spring configuration-property names, so the prefix is declared in
`configPropertyLiterals` rather than the rule being relaxed. The exactness is pinned: a new literal
beneath the declared prefix (tramai.control-plane.http.mutations) still fails closed.

* style(observability): spotless formatting for the verifier-rules test

* fix(0.7.1e): correct the precondition edge, the witness claim and the read contract

Review corrections on the frozen 0.7.1e contract.

1. Present-but-empty If-Match is now 400, not 428 (blocker). The frozen mapping distinguishes
   absent (428) from present malformed (400); the parser had collapsed null, "" and blank into
   Missing. Only a null header is Missing now: any present value that does not carry exactly one
   strong numeric ETag, including empty or blank, is Unsupported. Pinned by the parser test and by
   a new HTTP-level test asserting an explicitly present empty header is 400 with zero mutation.

2. The "a read value cannot be a mutation witness" claim was false as written (contract
   truthfulness). ClassifiedRead.observedVersion is a WorkloadStateVersion — the exact type commands
   accept — and the lag test itself submits it and correctly observes Stale(17,16). Corrected to the
   real invariant wherever it was stated: a read RECORD is never accepted as mutation input
   (commands take identity + expectedVersion + payload), while an observed version may be submitted
   as an explicit precondition and is authoritative only if the authority's current version still
   equals it. Lag is defeated by the compare-and-set, not by the type system, so no second version
   token type is introduced. Test renamed to `a command takes an explicit version, never a read
   record`.

3. ClassifiedRead now enforces registration.stateVersion == observedVersion, and the read ETag is
   derived from observedVersion explicitly, so a future projection implementation that shipped
   contradictory state/version evidence fails fast instead of emitting a body and header that
   disagree. (Copilot flagged the same inconsistency.)

4. Removed three genuinely unused imports from the controller (ClassifiedRead, WorkloadStateVersion,
   ProblemDetail).

No compatibility rule, migration entry or API dump was changed: verify060Architecture still PASSED
against the exact Epic base, so the authorized transition stands.

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
…inning 0.4.0 (#422)

Fixes #421. The example selection guide names the published coordinates it consumes, so that
version is release-scoped: it must be the last promoted release, and it must follow the CHANGELOG
rather than a literal that silently goes stale at every release cut.

- Add promotedReleaseVersion(rootDir), reusing the dated-CHANGELOG-heading notion the version
  alignment verifier already relies on for release-scoped surfaces.
- verifyExampleSelectionGuide now requires the Kotlin Spring Boot section to name that derived
  release, instead of the stale literal 0.4.0 that made the full workflow_dispatch release closure
  red on the Epic base while every PR-scoped gate stayed green.
- Add ExampleGuideReleaseAssertionTest: the derivation, the real guide being accepted, and a
  version-agnostic negative (naming any other release must fail). The positive case fails against
  the pre-fix guard, which is the discriminator.

Sweep of the same class: the only other 0.4.0 literal, in VersionAlignmentVerifier, asserts that
STATUS.md still documents 0.4.0 as the release before 0.6.0. That one is intentional history and is
left alone; it passes on the real tree today.

Not a docs change: nothing in examples/README.md moves. The guard was the stale side.

Co-authored-by: TramAI Test <tramai-test@invalid>
…droom (#424)

The `tests` job watchdog was an absolute-duration kill calibrated when successful runs reached
~12.8 min: it slept 21 x 40s and killed Gradle if the PID was still alive. Measured runs have since
reached 13.3-14.4 min, so the guard began aborting healthy suites mid-run — it had become an
assertion about suite duration rather than a hang detector.

Evidence from the failing head (2d0245f's predecessor 7bd0c29):
  * Run tests step: 14.03 min, aborted at the deadline
  * last test progress marker at 14.02 min — still passing, not stalled
  * largest silent gaps were compilation, not a stuck test
  * the P1 head with none of the newer container tests already consumed 14.13 min

Two independent guards, because a hang and a slow-but-healthy suite need different treatment:
  * inactivity: no test-result progress for 4.5 min -> dump threads and abort (a real stall)
  * ceiling: 20 min wall-clock -> dump threads and abort regardless of progress (growth margin)
The step timeout rises to 25 min, above the watchdog ceiling. Thread-dump artifact upload is
unchanged and still gated on `if: always()`.

The inline loop moves to .github/scripts/test-watchdog.sh so it is runnable locally and testable,
with a documented row in .github/AGENTS.md. Progress is read from Gradle's test-result tree, which
advances as tests execute.

New contract-test partition `ci-wiring` (6 tests) keeps both guards wired and proves the behaviour
against the real script rather than asserting on text:
  * wiring: the tests job runs the script; ceiling >= 20 min; inactivity in the 4-5 min band; the
    step timeout exceeds the ceiling; the dump artifact name/path/always() survive
  * behaviour: a stalled run is aborted by the inactivity guard; a progressing run is not aborted;
    a run that keeps progressing is still aborted at the ceiling

CI-only plus a build-logic test; no production source changes.

Co-authored-by: TramAI Test <tramai-test@invalid>
…425)

* fix(formatting): make the KtLint max-line-length policy actually take effect

The root .editorconfig declares no enforced max line length, but the KtLint step never consumed it:
ktlint_official DEFAULTS max_line_length to 140 in KtLint 1.8.0, so the effective policy was 140 and
the .editorconfig text was inert.

Observed as a formatting gate that could only fail, never converge: once a file became Spotless
ratchet-visible, dormant long lines that pre-date the change started failing as
`ktlint(standard:max-line-length) Exceeded max line length (140)`, and spotlessKotlinApply could not
resolve it (it wrote nothing across 3 consecutive runs).

Diagnosis (all reproduced with --rerun-tasks --no-build-cache against the Epic base ref):
- `max_line_length = off` in .editorconfig            -> still 140
- plus `ktlint_standard_max-line-length = disabled`   -> still 140
- probe: `indent_size = 2` with 4-space source        -> NO indent findings at all, proving the root
  .editorconfig does not reach the KtLint step
- `.setEditorConfigPath(root .editorconfig)` alone    -> still 140 (discovery hypothesis disproven)
- `.editorConfigOverride(max_line_length = off)`      -> rule absent; only autofixable import-order
  findings remained, and spotlessCheck passes on pristine content

So the override is what works, and it is documented as such rather than silently replacing the
authority: setEditorConfigPath stays to express the intent, and the comment records why the file
alone is insufficient. No rule was suppressed globally and no unrelated file was reformatted.

Verification: spotlessCheck BUILD SUCCESSFUL on pristine Epic content with this change applied.

* docs(formatting): state the narrow finding and guard the policy statically

Two corrections to the previous commit, both about not overstating what was fixed:

1. The comments. setEditorConfigPath does NOT make EditorConfig properties generally effective with
   Spotless 8.10.1 + KtLint 1.8.0 (the indent_size probe proves it), while editorConfigOverride makes
   max_line_length effective. The comments now say exactly that, and explicitly decline to claim that
   any other EditorConfig property is enforced. .editorconfig carries a CAUTION to the same effect.

2. A durable static guard, FormattingGatePolicyTest, in the same CI partition as the other
   FormattingGate*Test classes (ci.yml filters on that prefix). It asserts that all three declarations
   survive together: the .editorconfig policy text, the explicit editorConfigPath, and the mirrored
   override. Declaration only — runtime behaviour is proven by the reproduction in this PR; a
   behavioural check would nest Gradle inside :build-logic:test, which the repo avoids for this reason.

* docs(formatting): stop claiming a policy that is not enforced

The root .editorconfig declared "no enforced max line length", which was never true: with
Spotless 8.10.1 + KtLint 1.8.0 that file's properties are not propagated into the KtLint step, and
the rule was using ktlint_official's own default of 140. A probe setting indent_size = 2 in the same
file produced no standard:indent findings, which is what pins the cause to the handoff rather than to
the rule.

Earlier revisions of this branch tried to make the declaration true. That is reverted, deliberately:

- setEditorConfigPath does not change which properties are enforced, and reading an absolute path at
  configuration time is hostile to configuration-cache compatibility.
- editorConfigOverride(max_line_length = off) does make that one property effective, but honouring the
  declared no-limit policy makes KtLint demand joins in files the 140 limit had been masking: the
  formatting gate's own configuration-cache contract test then fails on
  build-logic/.../VersionAlignmentVerifier.kt, a file this change never touches. That is a
  repository-wide reformat, which belongs in its own change, not here.

So this commit only stops documenting something untrue: max_line_length is removed from .editorconfig
and both comments now state the effective policy (140) and say what fixing it would require. No
behaviour changes. Long lines in a file you are already changing should simply be fixed, which is what
#423 does for its own test file.

---------

Co-authored-by: TramAI Test <tramai-test@invalid>
…418)

P0 of #418: the ownership and trust model the implementation must satisfy.

Derived, not assumed: durable governed-suspension storage already exists and is wired
(GovernedSuspendedInvocationStore.createGoverned, ApprovalSuspensionCoordinator.resolveGovernedSuspension,
sovereign autoconfiguration). The missing capability is getting the two gateway creation paths onto
that governed path, carrying attribution onto the approval lifecycle, and enforcing continuity when
that lifecycle is reconstructed.

Frozen in the spec: creation authority is GovernedRunScope and the factory is never trusted for
identity; ApprovalBinding.workflowRunId stays the single stored run-id source with the five
remaining components in reserved framework-owned metadata keys (all-or-none, collision rejected,
immutable across lifecycle transitions); the approval row is an attribution snapshot and not a
second authority, with exact equality required against the canonical governed suspension identity
wherever both exist; reconstruction decodes from durable state only and never synthesizes identity
from correlation ids, workflow ids alone, caller input or ambient scope; ambient scope at
reconstruction may corroborate but never override, and disagreement aborts.

The factory-produced ApprovalGatewayPersistenceRequest is deliberately NOT widened with a nullable
identity: a gateway-resolved attribution seam keeps the factory identity-blind, so the silent
downgrade cannot reopen.

No code in this commit; implementation follows P1-P6.
Attribution carrier for governed approvals, per the frozen 0.7.1d1 model.

- ApprovalAttributionKeys: five reserved framework keys carrying workload, configuration,
  configuration version, environment and deployment. The run id is deliberately absent:
  ApprovalBinding.workflowRunId stays the single stored run-id source, so persistence cannot hold a
  second identifier that diverges from the run it describes.
- encode/decode over the existing approvals.sanitized_metadata carrier: no schema change. Decode is
  fail closed — all keys absent is legacy (un-attributed), a partial set or a blank component is
  corruption and throws rather than yielding a partial identity, and an unparseable component is
  wrapped as corruption with its cause.
- mergeApprovalAttribution rejects application metadata that supplies a reserved key instead of
  overwriting it, so an attribution-injection attempt is observable rather than hidden.
- ApprovalRunAttribution (Ungoverned | Governed) keeps the gateway-resolved attribution structurally
  distinct from the factory-produced persistence payload; no nullable identity on the request.

Tests: 9/9. Round trip with the run id taken from the binding, exactly-five-components encoding,
per-key partial-set corruption, blank component, collision rejection (injected value proven absent
from the merge), governed merge preserving application metadata, and legacy byte-compatibility.
…row (P2, JDBC)

Adds the durable half of #418's carriage model, following the existing governed-store pattern.

- GovernedApprovalStore (tramai-engine): additive capability interface, mirroring
  GovernedSuspendedInvocationStore's rationale — third parties implement ApprovalStore, so adding a
  required method would break them. A governed approval REQUIRES the capability and fails closed
  when the store does not provide it. Public requireAttributionMatchesBinding makes "run A +
  workload/configuration/deployment B" unrepresentable at the durable boundary, mirroring the
  runId == workflowRunId guard on GovernedSuspendedInvocation.
- JdbcApprovalStore implements it: governed and legacy creation share ONE code path (legacy passes
  Ungoverned, so existing rows stay byte-identical), the five reserved keys are written into the
  existing sanitized_metadata JSONB — no schema change — and approvalAttribution decodes fail closed.
- The attribution rides a nested framework-owned metadata field rather than flat siblings: the
  ApprovalMetadata record is typed with ignoreUnknown = true, so a flat encoding would be silently
  erased by the first transition's parse -> copy -> write. The lifecycle test asserts the raw JSONB,
  not just the decoded value, so "D1 holds only at creation" cannot hide.

Tests: 5/5. Snapshot survives create -> approve -> consume with the raw JSONB asserted afterwards;
legacy rows carry zero reserved keys and decode as un-attributed; a mismatched identity is rejected
and writes nothing; one key removed by raw SQL is corruption, never a legacy approval; an unknown
approval raises the precise ApprovalStoreNotFoundException (the SPI KDoc was corrected to name that
type rather than its base class).

No new concrete ApprovalStore implementation is introduced, so the TCK enrollment guard needs no new
runner; the pinned allowlist is untouched. FileApprovalStore's capability follows next.
… (P2, file)

File-store parity for #418's carriage model, following the repository's existing governed-store
precedent rather than inventing a third persistence shape.

- V1 when ungoverned, V2 when governed (PersistedApprovalRequestV2 composes the V1 payload plus
  PersistedApprovalAttributionV1). One atomic encrypted write either way, so a crash cannot leave a
  half-governed approval behind.
- The persisted attribution keeps the five identity components and deliberately NO run id:
  ApprovalBinding.workflowRunId stays the single stored run-id source, and decoding reconstructs and
  validates the full identity against it through the same shared codec JDBC uses.
- Version dispatch reads schemaVersion from the document itself, so a malformed V2 fails closed and
  is never reinterpreted as an un-attributed V1 record. Strict decoding (FAIL_ON_UNKNOWN_PROPERTIES,
  FAIL_ON_NULL_FOR_PRIMITIVES) makes a missing component a corruption error, not a default.
- Rewrite safety is structural: every full-record rewrite starts from the record it read
  (readRecord -> copy(request = ...) -> writeRecord), so transition and consumeFreshApproval cannot
  drop attribution by forgetting to pass it.
- The governed capability is exposed by a wrapper (GovernedFileApprovalStore) delegating to internal
  store methods, mirroring GovernedFileSuspendedInvocationStore, so the concrete store's declaration
  and detekt baseline signature are unchanged.

Tests: 13 new lifecycle/corruption discriminators plus TCK enrollment for the wrapper (delegation is
transparent for the whole released contract). Attribution loss is caught by asserting the raw
persisted JSONB, not only the decoded value, and by re-reading from disk through a fresh store after
every lifecycle step.
…proval tests (P2)

The two new JDBC tests used the deprecated org.testcontainers.containers.PostgreSQLContainer, which
the exact-head compiler-warning gate correctly reported as new warnings. Testcontainers 2.0.5 is
already in use, and every current test in this module (including the governed suspended-invocation
twin) imports org.testcontainers.postgresql.PostgreSQLContainer, which is non-generic.

Test-only change: no production code, no warning/detekt baseline edits. The deprecated package is
left untouched in the ~20 pre-existing tests, whose warnings are already baseline-covered.
…container (P2)

The two P2 JDBC classes each started their own postgres:17-alpine, and the suite now sits on top of
the CI watchdog deadline. Folding the five attribution discriminators into
GovernedJdbcApprovalStoreTckTest recovers a container startup inside that budget while keeping
ownership where it belongs: that class already owns the governed JDBC store, is enrolled in
ApprovalStoreTck, owns a real PostgreSQL container, and loads the exact sovereign schema.

Chosen over a cross-class shared database fixture: one less container, no shared mutable state
between classes, no cross-class ordering assumptions, and no new test infrastructure.

- All five discriminators and their helpers move verbatim: raw JSONB lifecycle preservation,
  legacy/un-attributed distinction, run/binding mismatch with zero write, partial-attribution
  corruption, unknown approval != un-governed.
- The class now inherits t0/expiry/clock from the TCK base instead of redeclaring them.
- JdbcGovernedApprovalAttributionTest is deleted. TCK enrollment is unchanged.
- Test-only change: no production code, no baselines, no API dumps, no watchdog change.
…l gateway (P3, engine half)

The preview gateway could not carry a governed run's canonical identity, so it rejected every
governed execution outright. It now resolves the scope once and treats that identity as the single
value for the request.

Ordering (frozen contract):
1. resolve GovernedRunScope -> the canonical identity, or null when ungoverned
2. validate a caller-supplied run id against the canonical identity (disagreement aborts)
3. derive the effective run id: for a governed request it comes from the identity, so an omitted
   argument is derived rather than left absent
4. require BOTH governed store capabilities before invoking the factory
5. invoke the identity-blind factory, and verify its binding still carries the canonical run id
6. persist: governed via createGovernedApproval + createGoverned(GovernedSuspendedInvocation(...)),
   legacy via the three existing calls in the same order

Why the capability check precedes the factory: the factory is application-facing input translation,
and a deployment that cannot persist governed records has no reason to run it. This also makes
partial wiring fail as ONE precondition instead of committing an approval and then refusing the
suspension.

Ownership: the identity is the only P3 value. The approval attribution is derived from it at the
governed store boundary, which is what keeps ApprovalGatewayPersistenceRequest identity-blind — no
nullable identity field was added to the factory payload. The same identity instance goes into the
governed suspension record, so the two paths cannot reconstruct or reinterpret identity
independently. The suspension shape mirrors ApprovalSuspensionCoordinator: ONE durable record.

Taxonomy: missing governed capability -> ConfigurationException with zero factory calls and zero
writes; caller/canonical disagreement, a factory that re-points the binding, or an existing approval
whose durable attribution differs -> GovernedRunContinuityException; an approval whose attribution is
unreadable propagates the store's own not-found failure rather than being reinterpreted.

No fallback: a governed request never reaches ApprovalStore.create or SuspendedInvocationStore.create.
Legacy behaviour is unchanged.

Discriminators (new GovernedApprovalGatewayTest, 8 tests) assert exact call counts, not just
outcomes: derived run id received by the factory; governed creates 1/1 with legacy creates 0/0;
legacy-only wiring ConfigurationException with factory calls 0 and every write path 0; partial
wiring failing as one precondition; factory re-pointing aborting before anything durable exists;
existing un-attributed approval not adopted; existing approval of another identity not adopted;
existing canonical approval returned idempotently. The released gateway suite keeps its own
caller/canonical disagreement case, reframed to say what it actually proves.
…rmatting gate pending]

Adds the additive governed mutation capability and routes the transactional
approval gateway's governed path through one JDBC transaction:

- GovernedSovereignOpsApprovalRequestMutationStore: additive SPI on top of
  SovereignOpsApprovalRequestMutationStore, taking the full GovernedRunIdentity
  (not a nullable identity, not only the five-component attribution snapshot).
- JdbcSovereignOpsApprovalRequestMutationStore: one internal transaction body;
  legacy and governed callers converge on createApprovalRequestInternal with an
  ApprovalRunAttribution; a single canonical identity feeds both identity-bearing
  writes (approval attribution snapshot + governed suspended invocation).
- Existing-row paths are treated as security-sensitive: both the initial
  selectApproval existing-row return and the post-rollback PK-race Existing
  return verify persisted attribution via requireExistingIdentityMatches.
- Legacy path never decodes attribution; property-local @JsonInclude(NON_NULL);
  nullable payloadVersion so ungoverned writes keep the released JSON shape.
- Defense-in-depth run-id check inside the governed mutation method itself.
- Exception taxonomy preserved at the transaction boundary.

State: focused governed JDBC suite 25/25 green. One KtLint
standard:max-line-length violation remains (cancellation-test assert region)
and the gateway wiring of SovereignOpsTransactionalApprovalGateway is not
started. Not the #423 head; pushed to a review branch only.
…action

Review finding on 844123f: the transactional path bypassed the
GovernedSuspendedInvocation run-id continuity invariant. It serialised
metadata.identity.workflowRunId from the request and governedRunIdentity.runId
from the canonical identity as independently sourced values, checking only the
approval binding. A request with binding=run-A, identity=run-A and suspended
metadata=run-B would have been persisted as a V2 suspension that the canonical
JDBC reader rejects as corruption.

- createGovernedApprovalRequest now validates all three carriers (approval
  binding, continuation, suspended invocation metadata) against identity.runId
  before any DB work, raising GovernedRunContinuityException. Zero writes by
  construction: the check precedes the transaction.
- Two adversarial discriminators (mismatched suspension metadata, mismatched
  continuation) assert the exception plus zero approvals/suspensions/
  continuations rows. Both fail against the pre-fix code.
- Positive test now proves all six identity components by reading the row back
  through the canonical governed reader (GovernedJdbcSuspendedInvocationStore),
  not just the run id.
- Removed the unused PayloadGovernedRunIdentity.toDomain() reverse conversion
  and the eight identity imports that existed only for it.
- Test file: added the assertThatSuspendCallThrows boundary helper and hoisted
  the cancelling codec out of the argument list (ktlint collapses a
  single-expression anonymous object into its argument position).

State: focused governed JDBC suite 27/27 green. Formatter still reports one
standard:max-line-length violation at the next site (L309, the replay-envelope
digest mismatch test) — the fix loop is in progress. Not yet promoted to #423.
…ates pending]

Review-visible intermediate state on top of the carrier-validation fix.

Test file:
- added the assertThatSuspendCallThrows boundary helper (operation stays inside
  the capture lambda; runBlocking lives in the helper only);
- converted all chained `assertThatThrownBy { runBlocking { ... } }.isInstanceOf(X)`
  sites to that helper, wrapper removal only, inner call verbatim;
- re-bound the digest-mismatch chain and the rollback chain to `val thrown =`
  so no collapsed statement carries a long tail;
- hoisted the cancelling replay codec out of the argument list (KtLint collapses
  a single-expression anonymous object into its argument position);
- extracted the pure mismatched digest and de-duplicated the doubled
  request() builder call.

State: focused governed JDBC suite 27/27 green.
Gates: verifyChangePolicy PASS. Formatter still reports ONE
standard:max-line-length violation; verifyStaticAnalysis reports 4 new findings
(LongMethod + NestedBlockDepth on createApprovalRequestInternal, TooManyFunctions
on the store class, LargeClass on the test class). Both are mine and mechanical,
not architectural. Not promoted to #423.
…ws helper

The formatter sweep's wrapper-removal regex matched the helper's own body (same
shape, same indent) and rewrote it into self-recursion:

    private fun assertThatSuspendCallThrows(block: suspend () -> Unit) =
        assertThatSuspendCallThrows { block() }

Restored to the AssertJ boundary it is supposed to be:

    private fun assertThatSuspendCallThrows(block: suspend () -> Unit) =
        assertThatThrownBy {
            runBlocking {
                block()
            }
        }

The 27/27 results reported after the sweep came from stale JUnit XML: Gradle did
not recompile, so the broken helper was never exercised. Re-proven on a forced
run (--rerun-tasks): 27 tests, 0 failures on this exact tree.
…he audit write

Two detekt-class findings traced to a real design slip, not formatting debt.

1. JdbcSovereignOpsApprovalRequestMutationStore declared `: Governed-…MutationStore`,
   which widens a released type: the interface extends SovereignOpsApprovalRequestMutationStore,
   so the store's analyzer identity changed and its baselined TooManyFunctions and
   LongParameterList findings reported as new. It also baked an optional capability into the
   single implementation. Now a wrapper, matching the governed suspension store decision:

     GovernedJdbcSovereignOpsApprovalRequestMutationStore(delegate)
       : SovereignOpsApprovalRequestMutationStore by delegate,
         GovernedSovereignOpsApprovalRequestMutationStore

   The store keeps `internal suspend fun createGovernedApprovalRequest` (defaults restored, since
   they were interface-inherited), so the single-transaction body is unchanged.

2. createApprovalRequestInternal was over the method-length ceiling; the prepared-outbox write and
   its pending transition moved verbatim into writeAuditOutboxArtifacts.

Focused governed JDBC suite: 27/27 green on a forced run. Remaining detekt findings: NestedBlockDepth
on createApprovalRequestInternal, LargeClass on the test class.
…Depth

createApprovalRequestInternal kept the catch handling inline, which held the method over the
nesting ceiling. The recovery now lives in recoverExistingAfterPrimaryKeyRace(error, approvalId,
governedIdentity), returning null for anything that is not a primary-key violation so the caller
keeps its database-failure wrapping.

Preserved exactly: rollback stays in the catch before the call; the re-read happens on the new
connection; reconciliation keeps using requireExistingIdentityMatches, so continuity and corruption
exceptions propagate unchanged; two returns, since the first draft tripped ReturnCount.

Focused governed JDBC suite: 27/27 on a forced run, with the three PK-race tests individually green.
Production detekt findings for this module are now zero; only LargeClass remains, on the test class.
…462)

* docs(0.7.1g1G5b): adjudicate the 36 suspension-sentinel identities TOOLING_LIMITATION

Adjudicates G5A-M01 (the 36 TIMED_OUT identities of the frozen 52) against the
existing g1E standard, with an independent second approval-family measurement at
aa0cc52 (narrowing only), identity-exact reconciliation 36/36 with zero status
movement, per-identity bytecode re-proof (if_acmpne over the child suspend call's
result with dup before the branch), and a 36/36 BYTECODE_ABSENCE concealment audit.

No production, test, baseline, admission, classification, mutator, timeout or
ceiling changes.

* docs(0.7.1g1G5b): correct the numberOfTestsRun claim

numberOfTestsRun is an XML attribute on <mutation> and is present on every
TIMED_OUT mutation (70/70 discovery, 62/62 approval family), carrying 0 - PIT's
own measurement, not a coercion of an absent element. The superseded claim is
recorded in a correction record rather than erased. No disposition changes:
36/36 TOOLING_LIMITATION stands.

* docs(0.7.1g1G5b): fix three provenance defects in the durable evidence

1. campaign1.measuredCommit 5856530 -> 18f10db (the P1 discovery campaign tip
   that produced 7081ed74; 5856530 was the stale committed population value).
2. campaign2.testsRun -1 -> 0 for all 36, consistent with the numberOfTestsRun
   PIT attribute they already carried.
3. All 36 evidenceReason strings: 'TIMED_OUT with no test count reported in two
   independent campaigns' -> 'TIMED_OUT with numberOfTestsRun=0 reported by PIT
   in both independent campaigns'.

No disposition changes: 36/36 TOOLING_LIMITATION.

* docs(0.7.1g1G5b): name the discovery campaign commit in the report
…#463)

* test(0.7.1g1G5c): pin the structured-parse-failed diagnostic to the real failure class

The existing assertions used startsWith("structured-parse-failed"), which is satisfied
both by the authoritative diagnostic (structured-parse-failed: StructuredOutputException)
and by the negated-conditional mutant's output (structured-parse-failed: unknown). The
assertions now pin the full contract, so the diagnostic's class-name component is
observable behaviour rather than an unchecked prefix.

* docs(0.7.1g1G5c): close the SURVIVED cohort - 1 KILLED, 11 EQUIVALENT

Adjudicates the 12 frozen SURVIVED identities at base 42d9d94: 12/12 EXACT
mappings (the five G5a ambiguities resolved by canonical identity), 1 semantic
KILLED (a9760bea3a92, via an exact diagnostic assertion), 11 instruction-level
EQUIVALENT proofs (6 caller-discard, 2 catch-handler dominance, 1 compiler guard,
2 successor convergence), 0 UNDETERMINED, 0 KILLED regressions.

No production changes; no authority changes.

* docs(0.7.1g1G5c): fix two custody defects in the SURVIVED manifest

1. reconciliation.movements[0].identity is the full 64-hex canonical identity
   (was the malformed prefix a9760bea3a92f).
2. The six NullReturnVals rows carry the mutated areturn PCs (161, 100, 244, 341,
   267, 54), each re-verified by javap at the census base against the line table,
   instead of null. Call-site pop PCs remain as supplementary evidence.

No disposition changes: 1 KILLED / 11 EQUIVALENT / 0 UNDETERMINED.
…465)

The four frozen NO_COVERAGE identities are all on the suspension protocol of
resolveGovernedSuspension() / resolveGovernedIdentity(), neither of which can
suspend (0 getCOROUTINE_SUSPENDED, 0 if_acmpne), so the COROUTINE_SUSPENDED exit
and the resumed-path throwOnFailure can never execute. 4/4 mapped to exact
bytecode PCs; no tests added; no production, authority or test changes.

Regression gate: identity-exact campaign at cd46f17 (throwaway 56364ae3) -
918/918 identities, 0 status movements, 0 KILLED regressions, 0 new timeouts.
Fresh unrestricted 7-family canonical campaign at checkpoint 284faa4 (no
narrowing): 2544 rows, complete-population digest 9aebd3202288c82ff006f2db33c95cac0772746fa3c3061569167cd3f45df9b0,
validated against the committed digest 9e2febc7 by the repository's own
canonicalProjection recipe.

Reconciliation: shared 2195 / base-only 189 (all 189 relocated by source-level
key, 0 unexplained) / candidate-only 349; appearing NON_KILLED 67, all 67 with an
admissible adjudication (16 prior + 36 G5b + 11 G5c + 4 G5d), 0 missing.
Determinism: independent re-aggregation reproduces the digest exactly.

No authority change; P1 not minted; P2 not consumed.
…ities (#467)

* feat(0.7.1g1P1): mint authority for the 67 admissible appearing identities

P1 mint through the existing M30-M39 population-admission mechanism: 67
preauthorizations (16 prior adjudications + 36 G5b TOOLING_LIMITATION + 11 G5c
EQUIVALENT + 4 G5d UNREACHABLE), each bound to fromBaseSha 5bcb030 (the actual
squash merge of #466) and to the G6 population digest 9aebd320, with per-entry
analyzer semantics and the adjudication provenance recorded in reason.

The KILLED twelfth G5c identity is excluded. The population does not change; P2
consumption is out of scope. No other authority file is touched.

* docs(0.7.1g1P1): complete the rationale for three retained admissions

Three of the sixteen prior-adjudication admissions carried a truncated reason
field (a half-serialized Python list repr cut at 140 characters). Their complete
committed chronology is now stated explicitly:

  final adjudication EQUIVALENT in TASK-0.7.1g1G4-RESIDUAL-11-MANIFEST.json;
  predecessor TASK-0.7.1g1G4-RESIDUAL-25-MANIFEST.json disposition UNDETERMINED

Verified against both committed manifests: all three are UNDETERMINED in the
residual-25 record and EQUIVALENT in the residual-11 record. Only the reason
fields changed; identity, status, outcome, analyzer, fromBaseSha,
populationDigest and cohort membership are untouched. P1 invariants re-proven:
67 admissions, 36/27/4 statuses, 36/16/11/4 provenance, 0 KILLED.
…tation) (#469)

* docs(0.7.1g1P2): reconcile the population-admission digest with C7 (design record)

Four complete unrestricted campaigns at the same effective PIT inputs produced four
distinct raw-status population digests while the canonical authority projection
(identity|outcome|family|module + topology + analyzer) was byte-identical in all four:
e6ad01dc1d2966894a6555304bc8ca9a04c8174e3c83ae88760fcfebf1464dad

Establishes from repository code that C7 (BaselineModel.kt:348-354) declares raw PIT
status diagnostic and non-authority while M34 binds it for the whole population through
the measurement-proof projection; that the 67 authorized rows are exactly stable across
all four campaigns (so M32's status-inclusive binding is not the defect and is retained);
and that the instability is 3 of 2544 non-authorized neighbours.

Recommends Option A (canonical authority digest) over Option B, and specifies a bounded
versioned migration for the existing 67 v1 authorizations that preserves provenance.

Design only: no rule, code, authority or configuration change; P2 not performed.

* docs(0.7.1g1P2): replace in-place digest migration with a base-side migration certificate

Review finding, proven from code: MutationPopulationAdmissions.kt:75-88 enforcedPayload()
includes populationDigest, and MutationPopulationAdmissionCeremony.kt:314-317 isRetainedRewrite()
compares it -- so the earlier revision's in-place digest migration would have been a retained
authority rewrite, and its claim that M36 stayed unchanged was false.

The corrected design keeps M36 and M37 literally unchanged: the 67 admissions stay byte-identical,
and the raw-v1 -> authority-v2 semantic upgrade is carried by a separate bounded base-side
digest-migration certificate (fromAlgorithm/fromDigest/toAlgorithm/toDigest/admissionSetDigest/
fromBaseSha/reason), minted in a P1M transition that touches only the certificate ledger and
consumed in P2, then removed with the admissions. Adds M40-M44 and discriminators T13-T16
(including the M31 analogue: a candidate cannot mint and consume the certificate in one
transition). Section 6.1 records the withdrawn proposal rather than erasing it.

Empirical conclusion, C7 reconciliation, Option A, authority-v2 projection and individual-row
binding unchanged.

* docs(0.7.1g1P2): correct T6 to the certificate's fields

* docs(0.7.1g1P2): specify the certificate's full fail-closed lifecycle

The certificate is base authority in its own right, so consumption rules alone are
insufficient. Added explicit lifecycle rules and discriminators rather than relying on
generic loader validation, which for the existing ledgers covers shape only (required
fields, canonical 64-hex identity, 40-hex fromBaseSha, duplicate ids, schemaVersion) and
cannot enforce transition guarantees.

Lifecycle: mint against the exact base (M45, the M35 analogue) -> retain byte-identically
(M46, the M36 analogue, judged over an explicitly stated enforced payload: fromAlgorithm,
fromDigest, toAlgorithm, toDigest, admissionSetDigest, fromBaseSha, reason; audit-only
metadata excluded) -> consume only from base (M43+M42+M41+M40) -> remove in the valid
consuming transition (M44+M47, M47 being the M37 analogue: a certificate may only disappear
by being consumed, never by silent cancellation). Discriminators T17-T19 added; the matrix
is now T1-T19.
…raw PIT status (#470)

* feat(0.7.1g1P2-1): bind M34 to a canonical authority projection, not raw PIT status

Step 1 of the design record (TASK-0.7.1g1P2-AUTHORITY-MODEL-RECONCILIATION.md): the semantic
split, with no certificate logic.

MutationPopulationEvolutionProof now carries two hashes, both computed by the verifier from the
same fresh measurement:

- projectionHash (unchanged): the raw-exact canonical comparison projection, raw status included.
  It remains the measurement proof (M21, exactComparison) and is deliberately NOT relaxed.
- authorityProjectionHash (new): identity|outcome|family|module plus family/module topology and
  analyzer semantics, with raw PIT status deliberately excluded.

M34 now consumes the authority projection. C7 (BaselineModel.kt) declares raw PIT status
diagnostic evidence and not authority, and four complete unrestricted campaigns at identical
effective PIT inputs produced four distinct raw-status digests - three identities of 2544
oscillating only between SURVIVED and TIMED_OUT, all NON_KILLED in every campaign - while this
projection was byte-identical every time. M34's previous digest was the measurement proof, so a
scheduler race on unrelated neighbours invalidated adjudicated authority; that was an unintended
reuse of one hash for two different trust questions, not a deliberate override of C7.

Raw status stays authoritative where it belongs: the fresh<->committed exact comparison (M21) and
each individual authorization's exact row (M32, unchanged and still status-inclusive - the 67
authorized rows were byte-identical across all four campaigns).

Discriminators: T1 (a neighbour's raw status movement leaves the authority projection unchanged,
while the raw-exact proof still distinguishes the populations) and T2/T8 (outcome and analyzer
semantics changes do move it) at pure-verifier level; T1 again at ceremony level, where an
authorization minted while a neighbour was SURVIVED must remain consumable when it is TIMED_OUT.

Two test corrections, both evidenced:
- the M34 negative test asserted a diagnostic substring ('complete measured population') that the
  deliberate rewording removes; it now asserts 'authority projection' and still asserts rejection.
- MutationRatchetAuthorityTest passed admissions = NONE in the candidate while the real base holds
  67 pending authorizations, which asserts a transition M37 forbids. This failure reproduces with an
  identical signature at the pristine base ec8b4da (8 tests, 1 failure, M37 on 050d2b83), so it is
  pre-existing and not caused by this change; the candidate now retains the base authorizations, so
  absent targets are M39 warnings as designed.

No certificate, no consumption rule changes, no P2. The 67 admissions, baseline, classifications,
evolution records, M06/M13/M36/M37 and the canonical outcome mapping are untouched.

* fix(0.7.1g1P2-1): transport fixture must mint the authority digest; dedup projection suffix

Two review corrections on #470, both narrow.

1. The real authority-transport fixture derived the wrong digest semantics.
   MutationPopulationAdmissionIntegrationTest.projectionDigest() returned
   exactComparison(...).proof?.projectionHash - the raw-v1 measurement digest - while production M34
   now compares authorityProjectionHash. The fixture therefore minted a digest the verifier no
   longer consumes, and the lane passed while the real transport was broken. It now derives the
   authority projection, and is renamed authorityProjectionDigest so its semantics are explicit;
   the doc comment records why minting projectionHash here is the wrong thing.

   This was the miss in the previous head: the support-helper digest was corrected but the
   task-level fixture is a separate implementation. Verifier tests prove semantics; real-task tests
   prove authority transport, and only the former was fixed.

2. The Copilot suggestion: canonicalProjection() and authorityProjection() duplicated the
   topology= and analyzer= serialization verbatim. Both now end with append(contextLines()), one
   private helper. Output is byte-identical (same appendLine calls, same order, same buffer), so no
   digest moves - the assertion that the authority projection is exactly the raw projection minus
   raw status is now structural rather than incidental.

Callers swept, not just the reported file: the discriminator test's uses are proof.matches(...) -
the measurement proof, which correctly stays raw - and the wiring test derives no digest. No raw
digest derivation remains in any transport fixture.

Verification:
- :build-logic:test (mutation-authority family, --rerun-tasks) - BUILD SUCCESSFUL, 142 tests, 0 failures
- spotlessKotlinCheck -PtramaiFormattingBaseRef=ec8b4da5... - BUILD SUCCESSFUL
- :build-logic:canonicalProbeIntegrationTest - fails on CanonicalProbeFunctionalTest.kt:507 with an
  identical signature at the pristine base ec8b4da (BASE_INTEGRATION_RC=1), so it is pre-existing
  and NOT regenerated here.

No Step 2, no certificate, no P2, no authority or config migration.

* fix(0.7.1g1P2-1): wrap the transport assertion for MaxLineLength

MutationPopulationAdmissionIntegrationTest.kt:466 exceeded MaxLineLength after the rename to
authorityProjectionDigest. Wrapped into the standard multiline form rather than baselined or
suppressed: the Detekt baseline is unchanged (base 4792 -> current 4792, 0 added, 0 removed).

Gate: verifyStaticAnalysis - no new findings. spotlessKotlinCheck - clean.
verifyChangePolicy - PASSED, 7 changed files, change class build-logic, no policy violations.
…ority object (M45/M46/M47) (#471)

* feat(0.7.1g1P2-2): digest-migration certificate as a first-class authority object

Step 2 of TASK-0.7.1g1P2-AUTHORITY-MODEL-RECONCILIATION.md 6.2: the mechanism only. No
certificate-aware consumption, no M40-M44, no P2, and the 67 admissions untouched.

The certificate carries a *semantic* upgrade of the whole-population authority digest without
rewriting historical authority. The 67 P1 authorizations stay bound to their raw-v1 digests and
byte-identical; the certificate stands beside them in its own ledger and states, in base authority,
that one exact fromDigest is equivalent to one exact toDigest for one exact bounded set of
authorizations. M36's payload comparison never sees it, so M36 and M37 stay literally unchanged - no
exception branch, no permitted-field list.

Three files, mirroring the admissions trio:

- MutationAuthorityDigestCertificate.kt - the type, the ledger, and the bounded admission-set digest.
  Fields exactly as designed: fromAlgorithm, fromDigest, toAlgorithm, toDigest, admissionSetDigest,
  fromBaseSha, reason. enforcedPayload() is that field set with authorizedBy/authorizedAt excluded as
  audit-only, so M46 judges byte-identity over bound fields while touching who/when can never
  manufacture authority. The admission-set recipe (sorted identities joined with a newline plus a
  trailing newline, SHA-256) is documented as contract, not implementation detail.
- MutationAuthorityDigestCertificateLoader.kt - parses
  config/quality/mutation-authority-digest-certificates.yml. An absent file means no certificates,
  which is fail-closed: no migration is possible and pre-authority-v2 authorizations stay
  unconsumable. A present but malformed file is a hard failure. Validates shape only - required
  fields, 64-hex digests, 40-hex fromBaseSha, known digest semantics, unique source digest, and no
  certificate that migrates a digest to itself.
- MutationAuthorityDigestCertificateCeremony.kt - the lifecycle: M45 mint-time base binding, M46
  retained immutability, M47 removal custody. Standalone over the two ledgers plus the authority
  baseSha, because a lifecycle rule is a transition property that cannot be expressed by parsing one
  file.

M47 takes its strictly fail-closed half. With no certificate-aware consumption path there is no way
to *prove* a valid consumption, so any base certificate absent from the candidate fails. That is
deliberate and documented in the rule and the test, not an unfinished branch: a disappearance must
never become its own evidence. When M40-M44 land it becomes "unless validly consumed by the same
transition", judged against an independently established consumption. This is the T19 boundary
flagged during Step 1.

No ledger instance is created: minting the P1M certificate is a later base transition, so this
change touches no config/quality file and no authority record.

Tests (22 new): loader shape contract including every fail-closed case; the admission-set recipe
pinned against the real committed 67-admission set; M45 in both directions plus the discriminator
that binding is enforced at mint only, so a retained certificate survives intermediate merges; M46
driven as a loop over each bound field, which fails if a field is ever added to the type without
being classified into the enforced payload; audit-only metadata changing without failing; and M47's
removal custody in both directions.

Verification:
- :build-logic:test --tests 'dev.tramai.build.quality.MutationAuthorityDigestCertificate*' --rerun-tasks
  - BUILD SUCCESSFUL, 8 tasks executed
- spotlessKotlinCheck, verifyStaticAnalysis, verifyChangePolicy - see the pull request

* fix(0.7.1g1P2-2): address both Copilot findings on #471

1. Medium, valid: MutationAuthorityDigestCertificateLoaderTest.repositoryRoot() assumed the test
   starts exactly one level below the repository by using a single parent hop. This test is
   deliberately pinned against the real committed 67-admission ledger, so a brittle path guess would
   make it fail for a reason unrelated to the recipe. It now walks upward to the `gradlew` marker,
   the same idiom CanonicalProbeFunctionalTest uses, and fails with an explicit message naming the
   starting directory if no root is found.

2. Low, readability: parseCertificates declared `val raw = raw["certificates"]`, shadowing its own
   parameter. The parameter is now `ledger` and the binding `certificatesRaw`, so the two are
   distinguishable at the point of use.

Both fixes are narrow: no rule, field, threshold or test expectation is weakened, and no suppression
or baseline entry was added.

Verification:
- :build-logic:test --tests 'dev.tramai.build.quality.MutationAuthorityDigestCertificate*' --rerun-tasks
  - BUILD SUCCESSFUL
- spotlessKotlinCheck -PtramaiFormattingBaseRef=e6e0d69... - BUILD SUCCESSFUL
- verifyStaticAnalysis - Detekt baseline unchanged (4792 -> 4792, 0 added, 0 removed), no new findings
- verifyChangePolicy -PchangePolicyBase=e6e0d69... - PASSED, 5 changed files, class build-logic
…igration certificate (#472)

P1M: the single isolated transition between Step 2 and Step 3. It establishes certificate authority;
it does not consume it.

One certificate, derived from repository truth rather than chosen:

- fromAlgorithm raw-v1 / fromDigest 9aebd3202288c82ff006f2db33c95cac0772746fa3c3061569167cd3f45df9b0
  This is the digest every one of the 67 committed P1 admissions is bound to. The ledger holds
  exactly one distinct populationDigest value across 67 admissions, so the historical authority
  source is coherent; nothing was changed to make it match.
- toAlgorithm authority-v2 / toDigest e6ad01dc1d2966894a6555304bc8ca9a04c8174e3c83ae88760fcfebf1464dad
  Recomputed independently over the frozen unrestricted measurement (2544 identities, the population
  the raw-v1 digest was taken over) from the canonical authority projection semantics: identity,
  canonical outcome, family, module, topology, analyzer. The same value was byte-identical across all
  four preserved campaigns. It is not copied from any candidate.
- admissionSetDigest 98a9587a1068ec0cd7158a05ba6909c61c522308c272e7fa9cb27d5edd93a79c
  Reproduced from the exact 67 committed identities using the Step-2 contract (sorted, joined with a
  newline plus the trailing newline, SHA-256). It equals the value pinned by the Step-2 test.
- fromBaseSha 64d0545 - the exact post-#471 epic tip, so M45's
  mint-time binding is against the real authority base and not a provisional preview SHA.
- reason records the migration without implying any historical admission was upgraded or rewritten.

The certificate ledger is the only changed path. The 67 admissions are byte-identical: no admission
was modified, no digest rewritten, no metadata touched, and no mutation authority was re-measured.

Ordering this preserves: a candidate may only consume migration authority that already existed in
its base. A later transition (Step 3) consumes this certificate; P1M minting and consuming its own
authority in one transition is exactly what the M43 analogue forbids.

Mechanical gate P1-P8 (changed-path isolation, admission byte identity, semantic population
identity, certificate cardinality, M45 binding, bounded admission set, source binding, independent
target recomputation) is recorded in the pull request. No permanent rule was added for this
migration, and no gate, threshold, suppression or baseline was weakened.
…ositive fact (#473)

* authority(0.7.1g1P2-Step3a): establish certificate consumption as a positive fact

M40-M43 in MutationAuthorityDigestCertificateConsumption, one predicate per function and
chained with ?: so the refusal order is readable and the whole consumption is a single
expression: only a certificate passing every predicate yields the valid consumption.

M44 (single use) is new. M47 now takes the established fact as a parameter instead of
rediscovering it, so a disappearance can never become its own evidence - the enforcement is
the shape of the API, not a convention a refactor can quietly undo. validConsumptions
defaults to the empty set, the fail-closed state, so existing behaviour is unchanged: the
eight pre-existing ceremony tests pass untouched.

M43 is a named rule with its own diagnostic rather than an implicit consequence of how a
certificate was found, so provenance stays visible. Consumption stays bounded: toDigest must
equal the verifier's fresh projection, fromDigest must match the cited admission, and
admissionSetDigest must equal the exact base authorization set.

Two genuine Detekt findings fixed by hand, not baselined: ReturnCount in the consumption
chain and the MaxLineLength on the same line.

* authority(0.7.1g1P2-Step3a rev2): consumption becomes an attributed fact, not a caller claim

Review blocker on the previous head: Set<String> let a caller state "trust me, this digest
was consumed", and certificateFromBase: Boolean let a caller assert M43 provenance. Both are
now closed structurally rather than by convention.

- CertificateConsumption is a sealed result. M44 and M47 take Set<CertificateConsumption.Valid>,
  never raw digests, so the attribution travels with the value: there is no way to express an
  unproven consumption in the lifecycle API.
- M43 is derived inside verifyCertificateConsumption by looking the certificate up in the base
  ledger and comparing enforced payloads. The fromBase Boolean is gone, so provenance cannot
  drift from the truth, and the two failure modes (absent from base / base payload differing
  from the cited certificate) are distinct diagnostics.
- The Valid constructor is internal, so the normal public API cannot fabricate a fact. Not a
  capability system: the point is that no ordinary call site can bypass the proof.
- verifyCertificateConsumption returns the fact, so Step 3b's call site cannot become part of
  the authority proof: a refactor cannot add a digest under the wrong condition and have M44/M47
  still trust it.

Every Valid fact in the tests is obtained by running the real verifier over a real base ledger;
none is hand-constructed.

Rules untouched: M36/M37 literally unchanged, and validConsumptions still defaults to the empty
fail-closed set, so the eight pre-existing ceremony tests pass unmodified.

Verification: 33 tests / 0 failures (consumption 13, loader 12, ceremony 8); spotless clean;
Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED, class build-logic.
All formatting and Detekt findings fixed by hand - no suppression, no baseline entry.

* authority(0.7.1g1P2-Step3a rev3): Valid becomes unconstructible outside the proof

Follow-up blocker: `internal` on the constructor is module-wide, and the Gradle call sites
live in this very module, so Step 3b could still legally write:

    val fake = CertificateConsumption.Valid(fromDigest = "...", ...)

and hand it to the ceremony without ever running M40-M43. The fact was attributed, but still
forgeable by ordinary same-module code.

- Valid's constructor is now `private`, so nothing outside the class can construct one -
  including other files in build-logic.
- The verification itself moved inside Valid's companion (`Valid.verify`), because that is the
  only scope from which a private constructor is reachable. So the proof and the fact live in
  the same place by construction: there is no path to a Valid that skips M43-M42.
- The top-level verifyCertificateConsumption now delegates to it, keeping the call-site name
  stable while leaving exactly one implementation of the proof.
- @ConsistentCopyVisibility keeps the generated copy() private too, so it is not an escape
  hatch under Kotlin 2.3's visibility behaviour.

This property is verified by the compiler, not by a test: the forgery snippet above no longer
compiles. No test needed editing, because the existing tests obtain every fact through the
verifier and never construct one.

Verification: 33 tests / 0 failures; spotless clean; Detekt baseline 4792 -> 4792 with 0 added;
verifyChangePolicy PASSED, 3 changed files, class build-logic.

* docs(0.7.1g1P2-Step3a): file KDoc states the private constructor, not internal

Review nit on 861d31a: the file-level KDoc still claimed the Valid constructor is
`internal` while the class KDoc and the implementation correctly say `private`. A stale
description of a security property is worth correcting rather than leaving, because it is
the kind of line a later reader trusts instead of the code.

Documentation only - no behaviour, no rule, no test changed.
… the real transport (#474)

Step 3a proved the consumption semantics on fixtures. This proves the transport: the real
committed certificate ledger and the real committed admission ledger, loaded through the real
loaders from the repository root, fed to the real consumption verifier.

This closes the load path P1M reported as unwired - until now nothing loaded
config/quality/mutation-authority-digest-certificates.yml from the repository root, so the
ledger's own load was only ever exercised against fixtures.

Six discriminators, all against committed authority rather than fixtures:

- the canonical ledger loads through the real loader and holds exactly one certificate;
- that certificate's admissionSetDigest equals the digest of the real committed identities,
  so adding or removing an admission from the real ledger changes this outcome;
- its fromDigest equals the single populationDigest the real committed admissions are bound
  to, which is M41's real binding rather than a fixture's;
- real base authority produces a Valid consumption, so the positive fact is reachable from
  the committed ledgers end to end;
- a certificate absent from the real base ledger is refused by M43, and one whose target
  digest was not this transition's measurement is refused by M40.

The negative cases matter as much as the positive one: they show the verifier is fed real
base authority and real measured data, not values arranged to agree with the certificate.

No production code changed. The remaining Step 3b work is certificate-aware M34 and the
T1-T19 matrix at both levels.

Verification: 39 tests / 0 failures (transport 6, consumption 13, loader 12, ceremony 8);
spotless clean; Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED.
…epts certified migration + M44/M47 on the same facts (#475)

* authority(0.7.1g1P2-Step3b): read the certificate ledger at the base revision

M43/M47 are judged against the certificate ledger as it exists in the BASE, but the only loader
read the working tree. So the base side had no way to see a certificate at all, and M34 had no
facts it could ever consult. This closes that gap.

The base-side read now goes through the same path the population, classifications, enrollments
and admissions already use: git show "<baseSha>:<path>" into the temp tree, then the ledger's
own loader. One authority snapshot (MutationRatchetAuthority) therefore carries the whole base
context - population, classifications, enrollments, admissions and now certificates - which is
what one coherent transition context means.

Absent at base is handled like the other optional ledgers: a base that predates the certificate
ledger has none, which is the most restrictive state, since no certified migration exists and
every raw-v1 admission still fails M34. A present ledger is validated by its own loader, so a
malformed one fails hard rather than degrading into "no certificates".

Two discriminators, both against real immutable history rather than fixtures:

- at 9ebe7b4 (the P1M merge that minted the certificate) the authority snapshot exposes
  exactly one certificate, and its fromDigest is the single populationDigest the base's own
  admissions are bound to;
- at 64d0545 (the base P1M was proposed against, which predates the ledger) the snapshot
  exposes no certificates, while the same snapshot's admissions are non-empty - so the test
  isolates the certificate ledger instead of showing that some empty snapshot is empty.

The default on MutationRatchetAuthority.certificates is the conservative NONE for fixtures; the
loader always passes it explicitly from the base revision.

Verification: 40 tests / 0 failures (base-revision transport 2, ratchet authority, population
admission suites); spotless clean; Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy
PASSED, 2 changed files, class build-logic.

Next: M34 consults these facts at the verifier's decision points, then the T1-T19 matrix.

* authority(0.7.1g1P2-Step3b): M34 accepts certified digest migration

Completes the certified-migration path: the base snapshot now carries the certificate ledger, and
M34 consumes the facts it proves.

- MutationRatchetAuthority gains the base-revision certificate read, using the same git-show-into-
  temp-tree path as the population, classifications, enrollments and admissions. Absent at base is
  the most restrictive state, as for the other optional ledgers.
- appearanceVerdict's M34 branch rejects a raw-v1 admission only when no certified consumption
  covers that admission's own historical digest. The facts come from one production site,
  certifiedConsumptions(...), which runs the real verification for each distinct historical digest
  against the base ledger and the exact base authorization set. A Valid exists only if M43-M42
  passed, so the branch cannot be reached by asserting anything.
- AdmissionAuthority carries the fresh projection plus the consumptions it certifies as one value.
  That is what makes the two judgment paths - the lifecycle scan and the appearing-identity path -
  receive the same object. Feeding one without the other would have made P2's diagnostics
  contradict themselves for the same 67 rows: admitted in the scan, M34-rejected in the appearing
  path. It also keeps both signatures inside the parameter budget without suppressing anything.

Without a certificate the M34 condition is exactly as before, and AdmissionAuthority(null) is the
fail-closed default everywhere. M36/M37 are untouched.

Verification: 158 tests / 0 failures, including the new discriminators - certifiedConsumptions
yields one fact from a real base certificate and none for an uncertified projection or no
certificate; M34 accepts the certified raw-v1 admission and still fails the uncertified one. Plus
the base-revision cases: the certificate is present at 9ebe7b4 (the P1M merge) and absent at
64d0545 (P1M's base) while that base's admissions remain non-empty.

spotless clean; Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED, 6 changed
files, class build-logic. Four genuine Detekt findings fixed by hand - the two LongParameterList
ones by introducing the carrier, not by suppressing them.

Next: M44/M47 consume the same fact set, and the T1-T19 matrix at both levels.

* authority(0.7.1g1P2-Step3b): M44/M47 consume the same Valid facts

Last row of the Step 3b path: the certificate lifecycle is now driven by the facts M34 already
uses, produced at one site.

- MutationRatchetCandidate gains the transition's own certificate ledger, loaded from the
  repository like the candidate admissions. It is validated (M45 mint binding, M46 retained
  immutability) but can never authorize a consumption: only the base ledger is consulted, so a
  candidate that both mints and consumes fails M43.
- The verifier now calls MutationAuthorityDigestCertificateCeremony.checks with the base and
  candidate certificate ledgers and validConsumptions taken from
  base.admissionAuthority(...).certifiedConsumptions - the same object M34 decides on, so M44
  (single use) and M47 (removal custody) cannot disagree with the admission verdicts about what
  was consumed.
- M47's consuming half is now exercised end to end: a certificate may disappear only because a
  consumption was independently proven in the same transition, never because it was absent.

Two existing test fixtures were corrected rather than the rules relaxed:

- MutationRatchetAuthorityTest's identity-transition candidate now retains the base's
  certificate, exactly as it retains the base's admissions. Passing NONE asserted a transition
  that removes the certificate with no proven consumption, which M47 rightly refuses. This is
  the same class of fixture defect (and the same fix) as the admissions case in Step 1.
- CertificateCustodyTransportTest drives the real committed ledger through the real ceremony:
  unchanged, it survives; dropped without a proven consumption, M47 fails; dropped with the
  facts produced from the real ledgers, removal is permitted.

M47 firing on that fixture is the evidence that the consuming half is genuinely live, rather
than a branch nobody reaches.

Verification: 162 tests / 0 failures across the certificate, admission and ratchet suites;
spotless clean; Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED, class
build-logic. A full :build-logic:test run reached 855 tests with zero genuine test failures
before the console call was truncated (its only entry was an infrastructure pseudo-failure from
the kill itself); CI runs the complete suite.

Remaining: the T1-T19 matrix at verifier and real-task level.

* authority(0.7.1g1P2-Step3b): T14/T17/T18 at the transport level, over the real ledger

Audited the discriminator matrix against the rules that actually have tests: every rule M01-M47 is
exercised somewhere, but three certificate-lifecycle attacks had only rule-level coverage, not the
real-task transport coverage the design requires. All three now run against the committed
certificate ledger loaded through the real loader.

- T18: a retained certificate whose enforced payload was rewritten (here its reason, which is in
  the payload) fails M46. The rewrite is derived from the real certificate's own fields, so the
  test cannot pass by accident of a fixture mismatch.
- T17: a certificate introduced with a fromBaseSha other than the transition's base fails M45, even
  alongside the legitimate retained certificate.
- T14: a certificate absent from the real base ledger cannot be consumed, however it is cited - the
  verifier refuses it as M43 rather than accepting the citation.

Together with the earlier cases this gives the certificate lifecycle transport coverage for
introduction (M45), retention (M46), consumption (M40-M43), single use (M44) and removal custody
(M47), all against the real committed ledger.

Verification: 165 tests / 0 failures; spotless clean; Detekt baseline 4792 -> 4792 with 0 added;
verifyChangePolicy PASSED, 9 changed files, class build-logic.

* authority(0.7.1g1P2-Step3b): produce the authority facts once, share the instance

Review finding: there was one fact-production function but three fact-production events.
MutationRatchetVerifier recomputed base.admissionAuthority(...) for the outcome-ratchet context and
again for the certificate ceremony, and MutationPopulationAdmissionCeremony recomputed it once more
inside its own consumption loop for the appearing-identity verdicts. The three results were
value-equal, which is not the property the contract asks for: deterministic recomputation is not
shared evidence, and value equality lets a later change drift one consumer away from the others
without any test noticing.

Now:

  val admissionAuthority = base.admissionAuthority(freshAuthorityProjectionHash)   // once

and that exact instance is threaded to every consumer - the evolution context (M34 and the
appearing-identity path), the admission lifecycle ceremony, and the certificate ceremony (M44/M47)
via .certifiedConsumptions.

- MutationPopulationAdmissionCeremony.checks/lifecycleChecks/consumptionChecks now take the
  AdmissionAuthority instead of the raw projection hash and pass it through unchanged; the internal
  recomputation at the consumption loop is gone. Public signature change: checks() has exactly one
  caller, the verifier.
- The AdmissionAuthority KDoc states the invariant: produced once, never re-derived.
- CertificateCustodyTransportTest pins it with a source-level wiring assertion, the form the review
  sanctioned where a runtime === check would require exposing internals: exactly one
  admissionAuthority( production call in the verifier, zero in the ceremony, and the certificate
  ceremony consuming the shared local.

Rebased onto the post-#474 epic tip a2c6c3a; diff scope verified as exactly 9 files, and #474's
merged CertificateLedgerTransportTest.kt is present at the tip and absent from this diff.

Verification: 172 tests / 0 failures (certificate, admission and ratchet suites); spotless clean;
Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED against base a2c6c3a, class
build-logic.
…zen count (#477)

The mutation-authority-integration step "Assert the proof was not vacuous" hard-coded
`executed = 2`. Step 3c's T7 discriminator adds a third authority-transport case to the
integration class that proves the real transport (verifyMutationRatchet -> RECORDED_EVOLUTION
-> nested canonicalMutationProbe -> fresh PIT -> exactComparison -> proof -> consumers), which
is exactly where that evidence belongs. The step then failed on its own cardinality contract:

  executed=3 failures=0 errors=0
  ::error::expected exactly 2 authority-transport tests, got 3

The step's name is a minimum-execution invariant, not a frozen suite cardinality. Make the code
say what the name says:

  [ "$executed" -ge 2 ]

A floor and not `= 3`, because this workflow PR must be independently testable against the epic
it targets - which still has two tests until #476 merges. `= 3` could not pass against its own
base, which would defeat the point of separating the workflow change.

Verified on this exact base, replaying the step's own commands and logic:

  :build-logic:canonicalProbeIntegrationTest --tests '*MutationPopulationAdmissionIntegrationTest*'
  -> TASK_RC=0; exactly one report matched; executed=2 failures=0 errors=0
  -> executed=2: old [ = 2 ] PASS, new [ -ge 2 ] PASS
  -> executed=1 and executed=0 (hypothetical): PRESSERVE FAIL under BOTH guards
  -> new guard on the current base: PASS (independent)

The step still fails loudly when the task selects nothing, which is the entire reason it exists.
verifyChangePolicy PASSED, class ci-workflow. Workflow-only, per .github/AGENTS.md rule 5.
…at both levels (#476)

* authority(0.7.1g1P2-T7): pin the fresh-measurement authority context at both levels

T1-T19 closure, starting from an audit of the test sources against the §7 matrix rather than an
assumption. Auditing each row's expected rule id inside the test bodies showed coverage for T1, T4,
T5, T11, T12, T13, T14, T15, T16, T17, T18 and T19 - and exactly one gap: T7, the row the design
annotates "transport test: the fresh path is used" and the review called critical.
authorityProjectionHash appeared in zero test bodies.

The property already holds structurally: MutationPopulationEvolutionProof has a private
constructor, is built only inside the verifier's own exactComparison from the fresh side
(fresh.authorityProjectionHash()), and only when the fresh measurement matches the committed
candidate everywhere. What was missing was the evidence, not the mechanism - which is precisely
the difference between a property and a property that is pinned.

- MutationPopulationEvolutionProofTest: T7 where the property lives. An exactly matching candidate
  obtains a proof; every candidate-side difference yields null instead of a partially trusted
  proof, including a raw-status change - a field the authority projection deliberately ignores.
  There is no partial trust by construction, and the test now says so.
- CertificateCustodyTransportTest: T7 over the real committed baseline through the real loader.
  Identical -> proof; one fabricated row -> no proof. An edited ledger cannot put its own authority
  context in front of M34/M44/M47.

Tests only: no production change, no rule, threshold, suppression or baseline touched.

Verification: 179 tests / 0 failures across the proof, custody, certificate, admission and ratchet
suites; spotless clean (hand-fixed); Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy
PASSED against base c4d3c84, class build-logic.

Not claimed: T2, T3, T8, T9, T10 remain unverified row-by-row, and the single audit hit for M13 is
the compiler-warnings M13 - a different namespace, not the PIT-status rule. Those rows are next.

* authority(0.7.1g1P2-T7): prove the fresh path at TASK level, not just by calling exactComparison

Review finding: the previous "real-task half" called MutationPopulationEvolutionProof.exactComparison
directly. It never ran verifyMutationRatchet, so it could not show that the production transport -
RECORDED_EVOLUTION, nested canonicalMutationProbe, fresh PIT population, proof, verifier - establishes
the authority context. Calling it transport evidence overstated it.

The discriminator now runs inside the existing integration harness, on the transition that passes
unperturbed:

  checkout(consumedCandidateSha)            # a PASSING transition
  flip ONE raw PIT status in the committed candidate baseline
  assert authorityProjectionDigest(committed) == authorityProjectionDigest(displaced)
  commit the perturbation                   # provenance gate requires a clean worktree
  verifyRatchet()                           # real verifyMutationRatchet, fresh nested PIT
  assert the task FAILS with
    "no proof of the canonical fresh measurement exists ... Admission fails closed"

The authority-equivalence assertion is the load-bearing one: authority-v2 excludes raw status, so a
task that accepted a candidate-supplied authority digest would still pass. Asserting failure is
therefore evidence that the FRESH exact measurement established the proof.

Three things this test cost, each fixed rather than papered over:

- The perturbation must be COMMITTED. An uncommitted edit leaves the fixture worktree dirty, the
  measurement's provenance gate refuses, and the task fails for that reason instead of on authority -
  and it poisoned the sibling tests that checkout their own SHAs.
- The target row must not be an identity this transition authorises. Flipping an authorised row fires
  M32/M37, which is the status-inclusive row binding (T3), not T7.
- `byFamily` is derived truth: moving raw status without moving the counters is a hand-edited ledger
  and fails closed on that. The counters move with the row.

The test also asserts `byFamily` and `M32` are ABSENT, so it cannot silently drift into proving a
different rule. M37 is deliberately allowed and documented: with no authority context the retained
admissions cannot be recognised as consumed, so it is the designed cascade of the same missing proof,
not an artifact of the perturbation.

Tests only: no production change, no rule, threshold, suppression or baseline touched.
Verification: :build-logic:canonicalProbeIntegrationTest - 3 tests, 0 failures (all three in the class
pass, including the two pre-existing transport tests); spotless clean; Detekt baseline 4792 -> 4792
with 0 added; verifyChangePolicy PASSED against base c4d3c84, class build-logic.

Scope note: @tag("integration") classes are EXCLUDED from :build-logic:test, so the affected-suite
tallies quoted earlier in this slice never contained this class. Verifier-level results stand; the
real-task evidence now comes from the task that actually runs it.
…actually reaches (#478)

* authority(0.7.1g1P2-3c2): T3/T2/T8 mapped, T9 pinned to the chain it actually reaches

Step 3c2 proof closure for T2, T3, T8 and T9. Audit applied the rule that a rule-id hit is not
coverage: a row closes only when a test reproduces the canonical scenario with the correct initial
authority state, the specific perturbation, the intended rule and the intended outcome.

ALREADY CLOSED, mapped rather than duplicated:

- T3: MutationPopulationAdmissionCeremonyTest "authorized identity with a different raw status fails
  M32" is §7's scenario field for field - the authorized row, its own raw status, M32, with the M32
  message asserted verbatim.
- T2: "T2 an outcome change changes the authority projection" (digest sensitivity) plus "authorized
  row with a different population digest fails M34" (M34 on a mismatch) close both halves of the row.
- T8: "authorized row measured under different analyzer semantics fails M33" (refusal, digest minted
  against the same candidate so no earlier predicate refuses) plus "T8 analyzer semantics change the
  authority projection" close both halves.

Two T-numbered tests I wrote first turned out to duplicate that pre-existing coverage, so they were
REMOVED rather than shipped as T-labelled duplicates. Net diff is one file, one test.

Method correction worth recording: my first audit pass searched test bodies for rule ids and could not
see "T2 an outcome change..." or "T8 analyzer semantics change...", because neither contains one.
Naming-based T-row matching is required alongside rule-id matching.

T9 - the finding. Section 7 maps T9 to "M32 fail". M32 cannot fire for this attack and the
implementation is not at fault: MutationPopulationAdmission.admitsRow compares identity, status,
outcome, family and module, but a mutant's identity is a hash over the row's coordinates, so
re-homing an authorized row to another family produces a DIFFERENT identity. The
same-identity/different-row case that is M32's only precondition is unachievable, so the premise
does not hold. Measured with a temporary probe, the transition actually reaches:

  M06: the moved row is a NEW NON_KILLED identity absent from the base authority
  M37: the authorization that admitted the old identity disappears unconsumed

The re-homing is therefore refused - what the row's OUTCOME requires - with a different named rule.
The new test pins that chain and its KDoc states the divergence, so the row is never counted from a
rule-id hit. No production change is proposed: the transition is refused, and the family/module terms
in admitsRow remain correct defence in depth.

Verification: focused suites 180 tests / 0 failures (T9 new; T2/T8 pre-existing); spotless clean;
Detekt baseline 4792 -> 4792 with 0 added; verifyChangePolicy PASSED against base 514f61a, class
build-logic; exactly 1 file modified.

Authority invariants: 67 historical admissions untouched, P1M certificate untouched, no new
AdmissionAuthority production site, no new Valid construction path, M36/M37 unchanged, M34 unchanged,
T7 unchanged, T10 not started, no production change.

* authority(0.7.1g1P2-3c2): T9 re-tests the canonical transition - family-only re-homing fails M32

The first T9 test was testing the wrong transition. MutationRatchetTestSupport.row() derives BOTH
identity = identityOf(marker, family, module) and className = "dev.tramai.$family.Policy" from the
family argument, so row("target", family = retryFamily) changed an identity-bearing field and minted
a new mutant. It tested a className change wearing a T9 label, and the M06/M37 chain it reported was
evidence about that transition, not the canonical one. The "M32 cannot fire" conclusion was a fixture
artifact and is withdrawn.

MutationIdentity.stableKey() hashes module, className, method, descriptor, mutator, description,
block and index: family is NOT identity-bearing, module IS. Section 7 fuses the two, so the corrected
test separates them.

family - non-identity-bearing. The re-homing is applied to an existing row via
row("target").copy(family = retryFamily), so className and every other stableKey input survive. The
candidate topology is kept coherent with baseFamilies + (retryFamily to retryTarget); the earlier
"families [policy, retry] disagree with the governing targetFamilies [policy]" diagnostic was exactly
that gap. Identity preservation is asserted as an explicit precondition, so this test can never again
silently drift into a className discriminator. Result: M32, with the M32 message - section 7 as
written. No production defect.

module - identity-bearing, therefore a different attack. It cannot reach M32, because the mutant that
arrives is a new identity: refused as a new survivor (M06) while the authorization for the old
identity is orphaned (M37). Pinned separately so neither case can be read as evidence for the other.

Verification: MutationPopulationAdmissionCeremonyTest 25 tests / 0 failures at this head. Production
untouched.

* authority(0.7.1g1P2-3c2): pin M37 exactly in the supplemental module-move assertion

The module half of the T9 test asserted only MUTATION_RATCHET_ADMISSION_INVALID, which is broader
than the M37 the KDoc claims. Pin the named rule: the orphaned authorization must fail M37, with the
M37 message, so the assertion matches its own documentation and M37 cannot silently degrade into a
sibling rule under the same code.

The canonical family-only half is unchanged: identity preserved, family changed, M32 reached.

MutationPopulationAdmissionCeremonyTest: 25 tests / 0 failures. spotless, static analysis and
verifyChangePolicy (base 514f61a) all pass. Production untouched.
…ance and the T1-T19 ledger (#479)

Documentation only. No code, no tests, no production behaviour, no authority artifact changes.

Section 7's transport rule is amended by supersession, not deletion. Its original wording - "each
attack needs a pure-verifier test and a real-task authority-transport test" - is kept verbatim, with
an amendment recording that it is now read as boundary-level transport assurance rather than
per-discriminator duplication. Requiring all nineteen attacks to be re-proven through the Gradle
task asks the same fact to be established repeatedly at every layer. The amended rule:

  1. every attack needs semantic discriminator evidence at the verifier/ceremony level;
  2. every authority transport boundary needs independent real-task non-vacuity evidence, proven
     once per boundary;
  3. attacks whose security property depends on provenance need end-to-end evidence.

The warning that motivated the original wording still holds: a verifier-level discriminator cannot
detect a missing Loader.load(rootDir), so every boundary that performs a load is asserted through
the real task. What is no longer required is proving each individual attack twice.

New section 11 records the closure ledger: the six transport boundaries with their real-task
evidence, the T1-T19 rows with both their semantic evidence and their transport classification, and
the recorded numbers - 122 verifier/ceremony tests at 0 failures, 3 transport tests at 0 failures,
the pre-existing unrelated CanonicalProbeFunctionalTest:507 red, 67 admissions and the P1M
certificate unchanged, no production behaviour changed, no new AdmissionAuthority production site,
no new CertificateConsumption.Valid construction path.

Verdict recorded: 0.7.1g1P2 Step 3c COMPLETE.
…mentation (#480)

Documentation only. One file. No production code, no tests, no authority artifacts, no baselines, no
migration. 0.7.1i is not started here.

Status line corrected. It still read "Active - 0.7.1a audit recorded... 0.7.1b... 0.7.1c... 0.7.1f",
a stale line naming four of nine sub-tasks. It now records CONTENT FROZEN, links every completed
record (0.7.1a/b/c/d/d1/e/f/g1P2) and points at the closure section.

New section "0.7.1h - Integration closure & content freeze" carrying the Epic acceptance matrix:
six criteria, each mapped to tests that already existed, each PASS, with no implementation gap found
and no test created or renamed for this task.

The 0.7.1g1P2 row added to the task table - the authority-model reconciliation and its Step 3c closure
had no row at all.

Docs deliberately left alone: docs/modules/tramai-control-plane.md already states the responsibility,
the safe exposure model (a declaration input never reflected back), the CAS/stale-writer semantics,
the RETIRED-terminal lifecycle and the TCK inventory correctly. Checked, unchanged, no churn for
style. The invariant "candidate transitions cannot mint the authority they consume" stays in its own
record (TASK-0.7.1g1P2 7/11) rather than being duplicated into this Epic.

Verification at this base: :tramai-control-plane:test plus ServerGovernedRunAttributionTest - 75 tests,
0 failures (TCK 19, governed-run attribution 14, WorkloadControlPlaneContractTest 8,
WorkloadExposureModelTest 8, WorkloadRegistrationAuthorityTest 26). spotlessKotlinCheck,
verifyStaticAnalysis and verifyChangePolicy against base 5dc10f2 all pass;
verifyChangePolicy reported the same docs-diff misclassification as build-logic noted on the
previous record PR, with no policy violation.
@GionaGranchelli
GionaGranchelli changed the base branch from master to release/0.7.0 October 2, 2026 21:55
Merges release/0.7.0 (9ccbd0d) into the epic so the promotion PR stops conflicting.
Seven files conflicted because #404 put an early control-plane snapshot onto release/0.7.0
while the epic carries the finished version 98 commits later.

Resolution, per file:
- tramai-control-plane/src/main/kotlin/.../WorkloadRegistrationAuthority.kt (add/add) -> epic
- tramai-control-plane/src/test/kotlin/.../WorkloadRegistrationAuthorityTest.kt (add/add) -> epic
- tramai-control-plane/api/tramai-control-plane.api (add/add) -> epic
- docs/modules/tramai-control-plane.md (add/add) -> epic
- build-logic/src/test/kotlin/.../ResidualQualityVerifierTasksTest.kt (content) -> epic
  (keeps the 0.7.1b identity-vocabulary comment; the release side had no competing text)
- docs/roadmap/0.7.0/EPIC-0.7.1-CONTROL-PLANE-AUTHORITY.md (content) -> epic
  (drops the stale "Active - 0.7.1a/b/c" status line; the epic's CONTENT FROZEN line and the 0.7.1h
  section are preserved)
- docs/roadmap/0.7.0/README.md (content) -> hand-merged: BOTH completion conditions kept, because each
  branch had renumbered item 7 differently and both are valid. The epic's 0.7.1 integration-closure
  gate stays item 7; #468's XR1 external-runtime authority proof is preserved as item 8.

No production behaviour is changed by the resolution: every contested source file resolves to the
epic's version. release/0.7.0's own commits (#404, #406, #468) remain in history.
…lease-into-epic

merge: reconcile release/0.7.0 into the 0.7.1 epic so #440 stops conflicting
The api-architecture gate was red on the 0.7.1 -> release/0.7.0 promotion
(#440) with 13 failure-severity diagnostics: 6 modules changed without an
exact hash-bound migration entry, and 7 entries from a branch-only lineage
that is no longer authoritative.

Keep the 6 real 0.7.0 -> 0.7.1 transitions as ACTIVE entries (fromSha256 =
release/0.7.0's committed dump, toSha256 = this line's dump, targetVersion
0.7.0) and remove the 7 non-authoritative entries. Git preserves their
history; no registry redesign, no retire-with-record mechanism, no DAG.

ApiCompatibilityVerifier accepts an entry only when it is ACTIVE or its
toSha256 reaches the module's base dump by following entries as a sequence.
The removed targets are intermediate states of the epic lineage that never
landed in release/0.7.0, so no chain can reach that base: a linear successor
chain cannot express a merge of two divergent API lineages, and the release
lineage is authoritative.

Nothing else changes: no module architecture, no catalog reclassification,
no production code, no gate, no baseline. The 5 pre-existing stability
inversions need no exemption -- verifyStabilityInversions already reports
them as WARNING when the descriptor is present in the base dump.

Verified: verify060Architecture PASS 10/10 against release/0.7.0 and against
the PR base; spotlessCheck + verifyStaticAnalysis + verifyStaticSafetyGuards
green; verifyChangePolicy PASSED.

Co-authored-by: TramAI Test <tramai-test@invalid>
@GionaGranchelli GionaGranchelli added the baseline-migration PR changes both the static-analysis analyzer and a canonical baseline (bootstrap) label Oct 3, 2026
…motion boundary (#483)

The 0.7.1 -> release/0.7.0 promotion (#440) fails the mutation ratchet on
M35 x 67 admissions and M45 x 1 certificate: both rules bind a NEWLY
INTRODUCED row to the authority base it was minted against, and a promotion
cannot satisfy that by any legitimate means - re-minting admissions,
rewriting fromBaseSha, manufacturing replacement certificates and a
per-entry migration chain are all forbidden.

Minting and promotion are different operations. This adds the one concept
that distinguishes them: MutationPopulationPromotion, a declaration that an
already-certified evidence population is carried across a promotion boundary
and therefore was not minted by the transition that carries it.

It grants no authority. Four fields - promotionBase, populationDigest,
admissions, certificates - are each enforced against the ledgers themselves,
so a declaration cannot describe a population other than the one it names.
SHA-exact and candidate-side, because the base branch cannot name the commit
that will contain the declaration.

M35 and M45 each gain one narrow guard: a row is not bound to this base when
the exact declaration carried it. Everything else fails exactly as before -
a newly minted row carries a different digest and is still refused. The
declaration is computed once in MutationRatchetVerifier, so the admission and
certificate rules cannot drift apart.

Verified: the real verifyMutationRatchet against release/0.7.0 PASSES (the
exact check #440 fails); 12 discriminators green, covering every field of the
declaration plus its absence; spotlessCheck and verifyStaticAnalysis clean,
with the two findings this code raised (ReturnCount, ThrowsCount) fixed in
source - no suppression, no baseline entry. No fromBaseSha rewritten, no
admission re-minted, no gate weakened.

Co-authored-by: TramAI Test <tramai-test@invalid>
@GionaGranchelli
GionaGranchelli merged commit 6bb4830 into release/0.7.0 Oct 3, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

baseline-migration PR changes both the static-analysis analyzer and a canonical baseline (bootstrap)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants