Retire completed campaign artifacts and keep repository reports explicit

Remove completed campaign records, one-time adoption/transplant machinery and
incidental line floors. Keep current contracts and generated inventories with
their readers, and direct optional domain reports to stdout or an explicit file.
Document continuing-purpose review in the existing handbook and checklists.

No version bump; ordinary release gates and runtime behavior are preserved.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Anton Razzhigaev 2026-09-18 01:07:51 +03:00
parent 2cce3c0c16
commit 98ca8a13d5
90 changed files with 605 additions and 18348 deletions

View file

@ -51,7 +51,8 @@ Evidence:
every section relevant to this change in full.
- [ ] I updated tests and documentation where behavior or architecture changed.
- [ ] I did not include secrets, local settings, runtime state, logs, caches, or
generated build/review artifacts in the commit.
generated build/review artifacts in the commit; tracked material follows
DEVELOPMENT.md "Documentation contract" (including plans and optional reports).
- [ ] I did **not** bump `VERSION` or release-only version carriers; maintainers
assign the collision-free release version during final integration.

View file

@ -797,11 +797,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: v7next adoption release bar
# The campaign ledger's release gate (every transplanted delta done or an
# explicit post-release row with its authority) runs on the tag path itself,
# not only inside the pytest wrapper (batch №13 item 4).
run: python scripts/v7next_adoption.py --release
- name: Validate tag matches VERSION
id: release_meta
run: |

1
.gitignore vendored
View file

@ -60,7 +60,6 @@ MagicMock/
.final_combined_review.py
.review_*.py
/.review-drive/
/.adversarial-review/
# Accidentally vendored site-packages fragments — guard against recurrence of the
# pre-rc.7 "dump site-packages into the source tree" bug (see Version History

File diff suppressed because one or more lines are too long

View file

@ -64,7 +64,9 @@ direction-changing proposal. Small, well-understood fixes do not need
ceremonial design work.
Never commit local settings, credentials, runtime state, logs, caches,
benchmark runs, generated review runs, or build artifacts.
benchmark runs, generated review runs, or build artifacts. For tracked material,
including campaign plans and optional reports, follow the
[Documentation contract](docs/development/02-naming-and-boundaries.md#documentation-contract).
## 3. Branch from `ouroboros` and Do Not Bump the Version

View file

@ -7,7 +7,8 @@ Rules:
- Generated logs, datasets, run outputs, Docker layers, and secrets do not live
here.
- Default benchmark outputs go under `/Users/anton/Ouroboros/bench_runs/`.
- Choose a benchmark output root outside the source checkout and runtime data;
use each runner's documented output option or `OUROBOROS_BENCH_RUNS_ROOT`.
- Runtime modules must not import `devtools`.
- This is not an immune-system bypass: touched files are reviewed normally.
- Promote code out of `devtools` only through a separate reviewed runtime plan.

View file

@ -178,7 +178,7 @@ Used by `commit_reviewed` for all changes to the Ouroboros repository.
| # | item | what to check | severity when FAIL |
|---|------|---------------|--------------------|
| 1 | bible_compliance | Does the diff violate any BIBLE.md principle? | critical |
| 2 | development_compliance | Does it follow DEVELOPMENT.md patterns? Check explicitly: (a) naming conventions (snake_case modules/vars, PascalCase classes, UPPER_SNAKE_CASE constants); (b) entity type rules — Gateway classes contain ONLY transport, no business logic; Tool functions are thin wrappers; (c) Python everywhere (including `tests/`/`devtools/`) and first-party `web/**/*.js` (including `web/tests/`) target ~1000 lines; exact repo-relative module debt above the 1600-line hard gate, exact `(path, qualname)` Python-function debt above 300 lines, the exact-current 1001-1500 band (new/re-entered paths need a nonblank rationale), and exact byte debt above 200,000 canonical UTF-8/LF bytes are checked in to `ouroboros/size_ratchet_manifest.py`; the enforcing surface for all of these (and for `MAX_TOTAL_FUNCTIONS`) is the official repository CI's `size_ratchet` pytest lane — manifest exactness on the tip tree plus the pairwise base-vs-tip shrink-only transition — while local runs surface the same `validate_size_ratchet` findings as warnings (a stale or growing entry is therefore review debt to flag, not a local commit block); methods above 150 lines are a decomposition signal, runtime-code total Python function/method count stays under `ouroboros/review.py::MAX_TOTAL_FUNCTIONS`, and more than eight parameters is a decomposition signal, not a hard gate; (d) no gratuitous abstract layers, and any SOLID/minimalism finding names an exact symbol/authority, concrete duplication or coupling, and a smaller contract-preserving alternative rather than citing diff size (P7 Minimalism) — and when the diff ADDS a surface (a new module, state file, ledger, resolver, cache, retry path, tool, endpoint, or background loop), the reviewer consults the docs/ARCHITECTURE.md map and NAMES the existing mechanism that already covers the need when one exists (name it exactly — the reuse-first duty this checklist carries for a CHANGE; the plan-review checklist judges an intention and has no such generative duty); absence of a covering mechanism may be stated in one line; (e) new LLM calls go through the shared `LLMClient`/`llm.py` layer, not ad-hoc HTTP clients; (f) cognitive artifacts (identity.md, scratchpad, task reflections, review outputs, pattern register) must NOT use hardcoded `[:N]` truncation — when content must be shortened, summarize explicitly (attempts, changes, and conclusions survive) and disclose the omission with a resolvable reference, because an omission marker alone is disclosure, not sufficiency; (g) new `get_tools()` exports follow the ToolEntry pattern in registry.py; (h) provider independence — no change may make a core capability (agent loop, multi-model commit review, scope review, or memory/context flows) silently require a second provider or OpenRouter specifically, and every supported single direct provider (local, OpenAI, Anthropic, MiniMax, DeepSeek, Cloud.ru, GigaChat) must keep its model AND review/scope slots self-fillable (see DEVELOPMENT.md "Provider Independence"); (i) a claimed-complete visible UI change includes vision-inspected evidence from at least one relevant real consumer flow. A screenshot file without inspection is insufficient; states/viewports/additional engines are risk-selected, mobile/WebKit are not universal, and an unavailable optional engine alone is not degradation. | critical |
| 2 | development_compliance | Does it follow DEVELOPMENT.md patterns? Check explicitly: (a) naming conventions (snake_case modules/vars, PascalCase classes, UPPER_SNAKE_CASE constants); (b) entity type rules — Gateway classes contain ONLY transport, no business logic; Tool functions are thin wrappers; (c) Python everywhere (including `tests/`/`devtools/`) and first-party `web/**/*.js` (including `web/tests/`) target ~1000 lines; exact repo-relative module debt above the 1600-line hard gate, exact `(path, qualname)` Python-function debt above 300 lines, the exact-current 1001-1500 band (new/re-entered paths need a nonblank rationale), and exact byte debt above 200,000 canonical UTF-8/LF bytes are checked in to `ouroboros/size_ratchet_manifest.py`; the enforcing surface for all of these (and for `MAX_TOTAL_FUNCTIONS`) is the official repository CI's `size_ratchet` pytest lane — manifest exactness on the tip tree plus the pairwise base-vs-tip shrink-only transition — while local runs surface the same `validate_size_ratchet` findings as warnings (a stale or growing entry is therefore review debt to flag, not a local commit block); methods above 150 lines are a decomposition signal, runtime-code total Python function/method count stays under `ouroboros/review.py::MAX_TOTAL_FUNCTIONS`, and more than eight parameters is a decomposition signal, not a hard gate; (d) no gratuitous abstract layers, and any SOLID/minimalism finding names an exact symbol/authority, concrete duplication or coupling, and a smaller contract-preserving alternative rather than citing diff size (P7 Minimalism) — and when the diff ADDS a surface (a new module, state file, ledger, resolver, cache, retry path, tool, endpoint, or background loop), the reviewer consults the docs/ARCHITECTURE.md map and NAMES the existing mechanism that already covers the need when one exists (name it exactly — the reuse-first duty this checklist carries for a CHANGE; the plan-review checklist judges an intention and has no such generative duty); absence of a covering mechanism may be stated in one line; added tracked material needs a continuing purpose under DEVELOPMENT.md "Documentation contract", and completed campaign machinery is retired rather than preserved by presence-only tests; (e) new LLM calls go through the shared `LLMClient`/`llm.py` layer, not ad-hoc HTTP clients; (f) cognitive artifacts (identity.md, scratchpad, task reflections, review outputs, pattern register) must NOT use hardcoded `[:N]` truncation — when content must be shortened, summarize explicitly (attempts, changes, and conclusions survive) and disclose the omission with a resolvable reference, because an omission marker alone is disclosure, not sufficiency; (g) new `get_tools()` exports follow the ToolEntry pattern in registry.py; (h) provider independence — no change may make a core capability (agent loop, multi-model commit review, scope review, or memory/context flows) silently require a second provider or OpenRouter specifically, and every supported single direct provider (local, OpenAI, Anthropic, MiniMax, DeepSeek, Cloud.ru, GigaChat) must keep its model AND review/scope slots self-fillable (see DEVELOPMENT.md "Provider Independence"); (i) a claimed-complete visible UI change includes vision-inspected evidence from at least one relevant real consumer flow. A screenshot file without inspection is insufficient; states/viewports/additional engines are risk-selected, mobile/WebKit are not universal, and an unavailable optional engine alone is not degradation. | critical |
| 3 | secrets_check | Are secrets, API keys, .env files, credentials present in the diff? | critical |
| 4 | code_quality | Careful code review: bugs, logic errors, crashes, regressions, race conditions, resource leaks? | critical |
| 5 | security_issues | Security vulnerabilities: injection, path traversal, secret leakage, unsafe operations? | critical |
@ -940,7 +940,7 @@ clean response.
| 3 | cross_surface_consistency | If behavior changed, are adjacent surfaces still consistent: prompts, docs, comments, tool descriptions, automation, or user-visible workflow? Apply the shared `Critical surface whitelist` — only release metadata, tool schema, module map, behavioural documentation, or safety contracts count as critical; commentary and prose mismatches are advisory. | critical if the mismatch is in a whitelisted surface AND concrete; otherwise advisory |
| 4 | regression_surface | Does wider repository context show a concrete sibling path, migration edge, or parallel flow that remains broken or incomplete after this change? | critical if it leaves a concrete broken/incomplete path; otherwise advisory |
| 5 | prompt_doc_sync | If prompts or docs are relevant to the changed behavior, are they still accurate and mutually consistent? Apply the shared `Critical surface whitelist` — behavioural documentation describing what a tool/command DOES at runtime is critical; wording/style of comments is advisory. | critical if a whitelisted prompt/doc artifact becomes false; otherwise advisory |
| 6 | architecture_fit | Does the change solve the class of problem, or is it a narrow patch that leaves the underlying pattern unresolved? | advisory |
| 6 | architecture_fit | Does the change solve the class of problem, or is it a narrow patch that leaves the underlying pattern unresolved? Check tracked material and completed campaign machinery against DEVELOPMENT.md "Documentation contract" for a continuing purpose. | advisory |
| 7 | cross_module_bugs | Does this change break something in a different module through implicit coupling, shared state, or assumed call/return patterns? Name the exact module, symbol, or call site. Follow DEVELOPMENT.md "Shared behavior and data-flow changes" for affected consumer paths. | critical if a concrete cross-module breakage can be cited; otherwise advisory |
| 8 | implicit_contracts | Are there constants, data format assumptions, expected function signatures, or protocol invariants relied upon by OTHER modules that this change violates without updating those callers? Name the exact symbol or file. Apply DEVELOPMENT.md "Shared behavior and data-flow changes" to authority, scope, freshness, and preservation evidence. | critical if a concrete violated contract can be cited; otherwise advisory |

View file

@ -1,10 +1,11 @@
# Delegated-run admission — threat model
Status: **schema floor enforced at admission; the boundary is read back per attempt and
**Schema floor is enforced at admission; the boundary is read back per attempt and
DISCLOSED, never required.** Owner: `ouroboros/config.py` (the two floors),
`ouroboros/subagents.route_health` (the admission decision),
`ouroboros/subagent_route_health.route_health` (the admission decision),
`ouroboros/gateways/claudexor.attempt_containment` (the applied-fact reader) and
`ouroboros/tools/delegate.py` (the three-place disclosure). This document is the reason
`ouroboros/tools/delegate_terminal_evidence.py` and
`ouroboros/delegate_start_instructions.py` (the three-place disclosure). This document is the reason
those numbers and that predicate are what they are; change it in the same commit as the code.
Claudexor owns the mirror document, `docs/DELEGATED_CONFINEMENT.md` in its own tree, which
@ -13,32 +14,41 @@ the outside, and what it says when the answer is "nothing was applied".
## 1. The asset
`~/.claudexor/v3/daemon/token` is a bearer for the ENTIRE `/v2` control API. A process that
reads it can start runs at any access level on any registered project. Every authority
derivation Ouroboros performs — the access profile, the run shape, the write-surface
predicate — is decoration downstream of a child that holds it.
The token named by the selected daemon's `control-api.json` is a bearer for
the entire `/v2` control API. A process that reads it can request runs at other
access levels on registered projects. Child-scope instructions do not restrict
what a holder of the daemon token can ask the control API to do.
The daemon runs as the operator, so a daemon Ouroboros did not start keeps its token in the
operator's own home, at an absolute path a scoped `HOME` does not redirect. Ouroboros's OWN
daemon (D30) is the exception: it is spawned under `CLAUDEXOR_CONFIG_DIR`, and that override IS
the complete relocatable root — its token lives under `data/claudexor/`, not in the operator's
home. Either way the token is an absolute path the child does not have to guess.
Ouroboros owns a daemon under `DATA_DIR/claudexor/`:
`CLAUDEXOR_CONFIG_DIR` relocates its complete config, credential, run and daemon
state. A separately configured daemon may use its own runtime home instead.
In either case, changing a child's `HOME` redirects home-relative lookups; it
does not make the actual token path inaccessible.
## 2. The actor
A delegated MUTATING child: `mode: agent`, `access: workspace_write`,
`execution.isolation: live`. Claudexor hands the harness a real shell in the nanny's own
worktree. The command stream is model output and the host cannot review each command before it
runs. Not assumed hostile — assumed uncontrolled.
A delegated mutating child uses the shape from
`subagents.delegated_run_shape`: `mode: agent`, `access: workspace_write`,
`execution.isolation: live`, with the delegated marker. For Git and skill
payload work, `tools/delegate.py` provisions a private execution snapshot
before dispatch. `live` means that Claudexor works in that supplied execution
root; it does not mean direct edits to the authoritative target. The host
captures the result and applies it through its existing integration path.
Ordinary folders use the separate supported direct/copy work-product shape in
`delegate_directory.py`.
A delegated READ-ONLY child (`mode: ask`, `access: readonly`) is not this actor. It gets no
shell that can mutate, and it stays inside Claudexor's ordinary envelope.
The harness can execute model-generated commands under the operator's OS
identity. It is not assumed hostile, but the host cannot review each command
before it runs.
## 2a. Stable project identity and the persistent registration (#362)
A read-only child requests `mode: ask`, `access: readonly` under Claudexor's
ordinary envelope. The host reads effective access back for both shapes;
the delegated HOME/boundary checks below apply only to marker-carrying runs.
Fresh mutating delegated starts on an engine satisfying the workspace-root release
contract (`CLAUDEXOR_DELEGATED_WORKSPACE_ROOT_MIN_VERSION = "3.8.1"`, the next
compatible release carrying Claudexor PR216 after the pinned 3.8.0) register and retain
## 2a. Stable project identity and persistent registration
For Git and skill payload work, fresh mutating starts on an engine satisfying the workspace-root release
contract (`CLAUDEXOR_DELEGATED_WORKSPACE_ROOT_MIN_VERSION = "3.8.1"`) register and retain
the user's actual target project in `scope.root`, while the child's writable filesystem
rides separately as the private snapshot in `execution.workspaceRoot`. That registration
is the USER'S identity, not a disposable snapshot: it is marked `project_persistent`
@ -55,75 +65,56 @@ shape and retire their one-shot registration as before.
## 3. What Ouroboros actually controls
Only ADMISSION and REPORTING. Ouroboros is an HTTP client of a daemon it does not build, ship,
or version. It cannot confine the child; it can decline to start a run, and it can state
afterwards what the run actually got.
Ouroboros selects and delivers an immutable Claudexor runtime through
`claudexor_runtime_pin.json` and `claudexor_runtime.py`. Executable bytes live
under `DATA_DIR/state/cx`; credentials and daemon state remain separately under
`DATA_DIR/claudexor`. The reviewed pin selects the next spawn, while the serving
process may still run an earlier pin until its lifecycle ends. Admission uses
the engine version returned by the connected daemon's handshake.
The marginal escalation is worth naming before any defence is priced against it (AGENTS.md
"Name the marginal escalation, not the scary noun"): this child already holds a shell in the
nanny's worktree, running the operator's own code as the operator. The step from "shell" to
"shell plus the daemon token" is real but small, and it does not buy a lane-wide refusal.
Claudexor implements the harness boundary. Ouroboros controls admission,
execution-root preparation, custody and reporting through the control API; it
cannot infer an applied boundary merely from having delivered a particular
engine build.
So the question is NOT "against which engines is this an acceptable act?" but **"what did this
run actually get, and does everyone downstream know?"**
The marginal escalation matters: this child already holds a shell in its
assigned worktree, running the operator's code as the operator. Access to the
daemon token adds control-plane authority, but withholding the whole lane
because a host has no boundary mechanism would also remove useful delegated
execution. The contract therefore checks required request support and reports
what each attempt actually received.
## 4. The version bands (measured 2026-08-03, not assumed)
## 4. Compatibility floors and applied evidence
Probed live against the operator's running daemon, and read out of the Claudexor tree at
`/Users/anton/Clawdexor` for the bands no local daemon runs.
The constants in `ouroboros/config.py` answer request-compatibility questions:
| Engine | `execution.delegated` | What the child actually gets | Verdict |
| --- | --- | --- | --- |
| ≤ 3.2.x | **400** `invalid_request`, `fieldErrors: {"/execution/delegated": ["Unexpected field; not part of this request."]}` | run never starts | below the marker floor — refused, because it cannot run |
| 3.3.0 – 3.3.1 | accepted | a scoped `HOME` — a CONVENTION. `~`-relative lookups redirect; `/Users/<op>/.claudexor/v3/daemon/token` is read with an absolute path and is READABLE. No confinement fields exist on the attempt record at all | admitted, and reported as UNCONFINED |
| ≥ 3.3.2, macOS | accepted | Seatbelt profile denying the Claudexor runtime tree and the operator credential stores, PROVEN against a denied path before the harness spawns; recorded as `confinement_mechanism` + `confinement_verified_denied_path` | admitted, and reported as CONFINED |
| 3.3.2, elsewhere | accepted | nothing, and the run does not proceed: `applyConfinement` threw `ConfinementUnavailableError` off darwin and the evidence gate refused to terminalize | REFUSED by the engine (`delegated_confinement_unavailable`) |
| ≥ 3.3.3, elsewhere | accepted | nothing enforced. `confinementMechanism()` returns null off darwin and the engine works anyway, disclosing the absence — `docs/DELEGATED_CONFINEMENT.md` §7: "There is no second policy. On every other platform `confinement_mechanism` is null, `confinement_verified_denied_path` is null, and `confinement_unavailable_reason` says why." | admitted, and reported as UNCONFINED |
| Engine version | Request support used by Ouroboros | Consequence |
| --- | --- | --- |
| Below `CLAUDEXOR_MIN_VERSION` (3.2.0) | Below the supported control transport | Handshake refuses the route |
| From 3.2.0, below `CLAUDEXOR_DELEGATED_MARKER_MIN_VERSION` (3.3.0) | Read-only shape is supported; `execution.delegated` is not | Read-only delegation remains available; a mutating shape gets `engine_rejects_delegated_marker` |
| From 3.3.0 | Delegated marker is schema-compatible | Admission can proceed subject to route readiness; confinement is read from attempt evidence |
| From `CLAUDEXOR_DELEGATED_WORKSPACE_ROOT_MIN_VERSION` (3.8.1) | Separate `execution.workspaceRoot` is supported | Stable target registration and private execution root stay distinct (§2a) |
3.3.3 is where proceed-and-disclose replaced the refusal, not 3.3.6. Between them, 3.3.3–3.3.5
did ship a real Linux bubblewrap boundary; 3.3.6 removed it as an owner decision, leaving the
scoped `HOME` plus a disclosed absence as the whole non-macOS design. None of 3.3.3–3.3.5 was
ever tagged or published, so the band above is what any reachable engine does.
The live 3.2.0 daemon answers the read-only body with nothing but the fake-root error
(`project root does not exist`), i.e. it schema-accepts every field that lane sends. The
mutating body is rejected on the field, before the root is even looked at.
### Why the floor is the MARKER release and not the boundary release
Both were tried. The floor sat at 3.3.2 — the release that added the boundary — on the
reasoning that 3.3.0–3.3.1 write `harness_home_isolated: true` while the token stays readable,
so admitting them would produce a receipt for a confinement that is not there.
That reasoning was right about the receipt and wrong about the remedy. **The last row of the
table is the same defect the floor was supposed to prevent, and the floor cannot see it:** a
3.3.2 build declares 3.3.2 on every host and applies a boundary on one of them. A version
describes a BUILD; it never describes what THIS attempt did. Using it as a proxy for "a
boundary was applied" is false in both directions — it refuses engines that would have been
honestly reported, and it passes hosts where nothing was applied.
The receipt is fixed where the receipt is written (§8), not by narrowing admission. Once the
report tells the truth, the whole band from 3.3.0 up is admissible, and the floor means the
one thing a version can honestly mean: **below 3.3.0 the request is a 400 and no run exists.**
These floors are not a platform-support matrix. A version describes a build,
not what a particular attempt applied. Raising the marker floor to a release
that contains a boundary would still not prove that boundary exists on every
host; it would also refuse older engines that can execute with honest
unconfined disclosure. The report must instead follow the attempt evidence
in §8. The floors are compatibility minima, not a claim that the managed pin
or serving engine currently equals one of them.
## 5. Why a version at all, and why not a capability probe
For the SCHEMA question a version is the only answer available. Verified rather than assumed:
The marker floor prevents a known request-schema failure before dispatch.
The capability catalog's top-level `runControlKeys` does not establish support
for the nested `execution.delegated` field. Its per-harness `delegation` object
describes MCP injection for Claudexor's own delegation strategy, which is a
different capability. `subagent_route_health.route_health` therefore does not
use that field as proof of marker support.
- `POST /v2/handshake` returns `{protocolMajor, compatible, operationsPath, engine: {version,
sha, entry}}`. There is no capability list of any kind — checked live, and checked in the
3.3.2 source, where the handshake responder is unchanged.
- `GET /v2/agent-capabilities` publishes `runControlKeys` derived from **top-level** request
keys only. `execution` appears; `execution.delegated` is nested and therefore invisible.
The catalog SCHEMA is field-identical between 3.2.0 and 3.3.2 — the only diff is one
`.describe()` string, so it gained nothing a probe could read.
- The per-harness `delegation` object in that catalog is about **Claudexor MCP injection** —
whether the harness can be handed sub-agent tools. It is `available: true` on the live 3.2.0
daemon, which rejects the marker outright, so reading it as a delegation signal would admit
precisely the engines that cannot serve the lane.
- A probe by BEHAVIOUR is unavailable: `RunExecution` is `.strict()`, so the only way to learn
whether the field is accepted is to send it, and sending it on an engine that accepts it
STARTS THE RUN. There is no dry-run key in `runControlKeys`. The probe and the act are one.
A behavioral test of the start endpoint would be the operation itself: sending
the field to an engine that accepts it starts a run. Admission uses the
compatibility constant instead of spending a model run to probe that schema.
For the BOUNDARY question no probe is needed, because the engine already answers it — after
the fact, on the attempt record (§8). That answer is a fact about the run rather than a
@ -141,12 +132,12 @@ one question left that a floor cannot answer:
`route_health` against the run SHAPE, before a token is spent. An engine below it would
reject the request with a 400, so the lane refuses it with a typed reason
(`engine_rejects_delegated_marker`) instead of spending a dispatch on a certain failure.
- `attempt_containment` — the applied-evidence reader. Not a gate: it decides what is SAID,
never whether the run happens.
- `attempt_containment` — the applied-evidence reader. Its boundary evidence
feeds disclosure, never a boundary-required admission gate. Its HOME facts
also feed the separate breach check in §8.
An engine between the two floors serves read-only delegation and refuses mutating delegation.
That is the owner's explicit decision, and it is why the marker floor is not simply raised into
the transport floor.
Keeping the marker floor separate preserves that serving read-only lane.
Both floors fail CLOSED: `engine_at_least` compares an absent or unparsable version as `(0,)`,
below every floor.
@ -161,21 +152,21 @@ never produce. An `auto` request becomes an ordinary native subagent with a visi
Stated plainly, because a floor described as total is worse than a narrow one.
- **Not the enforcement.** Ouroboros admits; the engine confines. The floor is a claim about a
build, checked against a self-reported number, and it is now used only for the schema
build, checked against a self-reported number, and it is used only for the schema
question, where that is enough.
- **Not a lying or downgraded daemon.** The version is self-reported over loopback, and so are
the applied facts on the attempt record. Anything that can forge either already runs as the
operator and has the token.
- **Not the gap between two repos.** Ouroboros and Claudexor have no shared build. That the
release carrying the marker declares ≥ 3.3.0 is a RELEASE GATE on the engine side, not
something this pin can enforce. It holds without an edit for every bump above the floor and
fails closed if a release ever breaks it.
- **Not the engine implementation.** Claudexor is built separately and selected
by an exact reviewed runtime pin. Its release must actually implement the
request shape its version promises. The compatibility floor checks that
declared contract; it does not inspect the engine's code at dispatch.
- **Not a promise that anything is confined.** A delegated mutating run is allowed on a host
with no boundary mechanism at all. What is guaranteed is that the run is not DESCRIBED as
confined when it is not — the disclosure, not the boundary, is the invariant.
- **Not what the boundary itself leaves open where it does exist.** The vendor credential root
stays readable to the child and the network is not fenced. Those are the engine's to state
and it states them in `docs/DELEGATED_CONFINEMENT.md` §8. Ouroboros must not re-describe
and it states them in `docs/DELEGATED_CONFINEMENT.md`. Ouroboros must not re-describe
them as covered.
- **Not the read-only lane's confinement.** A read-only child is scoped by Claudexor's ordinary
envelope. Ouroboros asks for no marker and verifies no boundary there.
@ -183,24 +174,13 @@ Stated plainly, because a floor described as total is worse than a narrow one.
boundary", so such a run is disclosed as unconfined when it was in fact confined. That is
the honest limit of an applied-fact reader, and it is the safe direction: the consequence is
a disclosure, never a refusal.
- **Not free of every harness NAME.** One named residual, disclosed rather than removed:
`gateway/claudexor_accounts.py::_build_login_request` branches on `harness == "codex"` in
three places (login setup only — never admission, routing or confinement). The branch is
load-bearing: `loginFlow` exists only for codex and is a 400 elsewhere, and a non-codex
login with no explicit transport would default daemon-side to `transport=daemon`, the
macOS Terminal.app handoff D30 forbids — so `client_pty` is forced instead. It mirrors
Claudexor's own setup-transport rule, not Ouroboros policy, and deleting it breaks D30.
It is the ONLY harness-name branch in the core (`ouroboros/`, `supervisor/`, `server.py`,
`launcher.py`). Removal condition: when the engine makes non-codex logins daemon-hosted,
the branch goes and this bullet with it.
## 8. Evidence, not intention — and the disclosure it feeds
What the run actually got is read back from the run's own artifacts
(`<runDir>/attempts/<id>/attempt.yaml`). The HOME pair is artifact-only — the engine projects it
onto no `/v2` response — while the boundary is also on the run detail, as
`candidates[].confinement` (`proven` / `mechanism` / `verifiedDeniedPath` / `unavailableReason`,
since 3.3.6); the artifact stays the one reader here because it answers both halves at once.
`candidates[].confinement` (`proven` / `mechanism` / `verifiedDeniedPath` / `unavailableReason`); the artifact stays the one reader here because it answers both halves at once.
Two facts, one reader (`gateways.claudexor.attempt_containment`):
- the HOME pair, `harness_home_isolated` / `harness_home_dir`;
@ -220,32 +200,32 @@ branch would have gone on reporting "no boundary" forever after that day.
**The two halves take different rules about silence, on purpose.** A missing HOME fact stays
UNPROVEN rather than false, because the consequence of "false" there is a CANCELLATION, and an
attempt can legitimately record no `harness_home_isolated` — it is the one optional member of
the applied facts, omitted when the attempt died before its home was decided (and an engine
older than 3.3.2 put no applied facts on `attemptFailureRecord` at all). A missing mechanism
the applied facts, omitted when the attempt died before its home was decided (and an older engine may omit
those facts from `attemptFailureRecord`). A missing mechanism
collapses to "no boundary", because the consequence there is a DISCLOSURE. Each silence is read
in the direction whose failure mode is recoverable.
**A breach is exactly two facts** (simplified 2026-08-11, Poltergeist phase A3; the
2026-08-07 refinement went one step further): a recorded `harness_home_isolated: false`,
or an applied home EQUAL to the operator's own (the claim is the lie, whatever boundary
sits beside it). A scoped home NESTED under `$HOME` is NOT a breach — with or without a
recorded boundary. The engine roots every scoped home under its own runtime dir, which
lives under `$HOME` on every host it supports, and on a host with no boundary mechanism
(every non-macOS host today) it CANNOT record one — so the former nested-without-mechanism
rule cancelled every mutating Linux run post-factum while the work was already done and
healthy. The boundary-less nested shape flows to the existing disclosed-unconfined path
below instead: the token stays reachable by a relative walk and the disclosure SAYS so,
but the child already holds a shell in this worktree, and cutting the lane on every
boundary-less host costs more than the marginal step it prevents (AGENTS.md "Disclose
instead of forbid"). The engine's typed `confinement_unavailable_reason` — read from the
SAME attempt artifact — rides the disclosure as an amplifier (why this host has no
mechanism); it is telemetry, never an admission token, and its presence never excuses a
recorded FALSE.
**Confirmed HOME failures are distinct from missing evidence.** For attempts
that record the HOME isolation flag, `_home_isolation_breach` reports a breach
when that flag is false, or when the claimed isolated home resolves to the
operator's own home. A missing flag is skipped by this enforcement check and
remains unproven in the report. A scoped home nested under the operator's home
is not a breach, with or without an OS boundary: nesting is the engine's
ordinary layout and its absence of a boundary is disclosed rather than used
to cancel useful work. The engine's `confinement_unavailable_reason` amplifies
that disclosure; it never excuses a recorded false.
Where no boundary was applied, the fact is written LOUDLY into three places (AGENTS.md
"Disclose instead of forbid"):
The run-level report also preserves partial evidence. `verified` remains false
unless every recorded attempt discloses its HOME fact, no HOME breach exists,
the HOME is not nested under the operator's, and all attempts name the same
proven boundary mechanism. `nested_under_operator_home` stays visible even if
a boundary was applied: the boundary is evidence of confinement; the HOME
redirect alone is not.
1. **the durable record** — a `delegate_run_unconfined` event, once per run, carrying the
The disclosure reaches three places:
1. **the durable record** — a `delegate_run_unconfined` event when no boundary
is reported or the HOME is nested under the operator's, once per run, carrying the
note the parent was given, so the forensic trail of an integrated patch says where the
work came from;
2. **the child's own prompt** — its instructions state that the boundary is a REQUEST and not

View file

@ -1,4 +1,4 @@
# Domain map — v7next
# Domain map
Generated from `ouroboros/domains.toml` by `python scripts/check_domains.py --write`. Do not edit — edit the manifest and regenerate; `tests/test_domain_manifest.py` pins byte-identity.
@ -59,7 +59,7 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
## Cycle status
1 pinned cycle group(s) — the SCC ceiling; the target is zero. Witness-level detail lives in `docs/v7next/DOMAIN_QUOTIENT_REPORT.md`.
1 pinned cycle group(s) — the SCC ceiling; the target is zero. Generate witness-level detail with `python scripts/domain_report.py`.
- group 1 (20 domains): D01 ⇄ D02 ⇄ D03 ⇄ D04 ⇄ D05 ⇄ D06 ⇄ D07 ⇄ D08 ⇄ D09 ⇄ D10 ⇄ D11 ⇄ D12 ⇄ D13 ⇄ D14 ⇄ D15 ⇄ D16 ⇄ D17 ⇄ D18 ⇄ D19 ⇄ D20

View file

@ -1,15 +1,12 @@
# Design note — runtime invariant `model-visible ⟺ logged` (CPL-5)
# Model-send observability: `model-visible ⟺ logged`
Status: LANDED (plan §7 item 5; batch-1 Q8=A confirmed; narrowed per roast
finding F15). The code is `ouroboros/model_send_seal.py`, wired at
The physical-send observability contract is implemented in
`ouroboros/model_send_seal.py`, wired at
`llm_attempt._candidate_before_dispatch` and swept from `server_maintenance`;
the pins are `tests/test_model_send_seal.py`. This note remains the contract,
kept narrow so neither the code nor a later reader drifts into a broader —
unprovable — claim. ONE clause changed between design and landing, and it is
marked in §3.2: a reconstruction mismatch is an OBSERVABILITY fact, not a
dispatch gate.
`tests/test_model_send_seal.py` verifies it. The claim is deliberately narrow:
a reconstruction mismatch is an observability fact, not a dispatch gate.
## 1. The claim, narrowed (F15)
## 1. Scope of the claim
The invariant binds exactly one object: **`model_send` — the physical
candidate payload at the last host-controlled pre-transport seam**. That seam
@ -22,8 +19,8 @@ after the cache-marker finalizer produced the final send copy
durable record of its exact send copy BEFORE dispatch.
- **Reverse (`logged ⟹ sent`)**: every sealed `model_send` record joins
exactly one accounting attempt (dispatched, refused, or released). The
reverse direction holds for `model_send` records ONLY — F15 explicitly does
not claim it for any other log plane (events, chat, progress are narrations,
reverse direction is asserted only for `model_send` records, not
for any other log plane (events, chat, progress are narrations,
not send truth).
- **Everything else is out of the byte domain by typed exclusion, never by
silence** (§4).
@ -35,21 +32,20 @@ its own previous answers is whatever the host replays into the NEXT send, so
response-assembly truth is covered transitively by the next round's
`model_send` record (§5.2).
## 2. What already exists (reuse-first — the note extends, it does not mint)
## 2. Shared mechanisms
| Existing mechanism | Where | Role in the invariant |
|---|---|---|
| Canonical digest of the exact send copy (`canonical_json_v1`: sort_keys, compact separators, `ensure_ascii=False`, `allow_nan=False`, `default=str`) | `llm_attempt._attempt_request` / `_canonical_candidate_bytes` | The canonical form and its versioned basis (`candidate_measurement_kind`) |
| Pre-dispatch identity re-check: digests re-derived from the closed-over candidate and compared with the reservation's expected identity; drift refuses dispatch (`PhysicalAttemptPreparationFailed: physical candidate changed before dispatch`) | `llm_attempt._candidate_before_dispatch` | The forward gate's skeleton — today a digest compare of two in-memory copies |
| Pre-dispatch identity re-check: digests re-derived from the closed-over candidate and compared with the reservation's expected identity; drift refuses dispatch (`PhysicalAttemptPreparationFailed: physical candidate changed before dispatch`) | `llm_attempt._candidate_before_dispatch` | The forward gate's skeleton — a digest compare of two in-memory copies |
| Durable candidate manifest + redacted CAS blob, written before dispatch, with two labelled digest domains (`canonical_json_v1_pre_redaction` facts vs `observability_json_v1_post_default_redaction_cas` blob) | `observability.persist_physical_candidate` / `persist_call` | The sealed record carrier |
| Attempt lifecycle `reserved → dispatched → settled|unresolved` / `reserved → released`, short-lock append + sequence replay | `usage_accounting` | The join target for the reverse direction |
| Anthropic native custody projection (opaque provider-native content replaced before persistence; disclosed as `anthropic_native_custody_projected`) | `anthropic_native_custody.physical_custody_projection` | Prototype of a typed exclusion |
| Secret redaction with per-hit `RedactionRecord`s | `observability._redact_text` + rules | Prototype of a typed exclusion |
The gap this note was written to close: the pre-existing gate compared two
**in-memory** serializations, so a bug between "what we persisted" and "what we
believe we persisted" was assumed away rather than caught, and a mismatch was
only a raised exception. `model_send_seal.verify_sealed_candidate` closes it by
An identity gate that compares only two **in-memory** serializations cannot
establish whether the durable record matches either copy.
`model_send_seal.verify_sealed_candidate` checks that separate question by
**reconstructing from the durable record** and byte-comparing that
reconstruction against the wire-bound serialization, emitting a **typed durable
fact** on any inequality (§3.2 — a fact, not a refusal).
@ -58,7 +54,7 @@ fact** on any inequality (§3.2 — a fact, not a refusal).
### 3.1 Sealed record (`model_send` seal, v1)
Extend the existing physical-candidate manifest (no new plane) with a
The existing physical-candidate manifest carries a
`model_send_seal` block:
- `seal_version: 1`
@ -66,18 +62,18 @@ Extend the existing physical-candidate manifest (no new plane) with a
the serializer is a NEW basis string; a reader never re-interprets bytes
under a different basis.
- `pre_redaction_sha256` / `size_bytes` — digest of the canonical bytes of the
exact wire payload (exists today as `candidate_raw_sha256`).
exact wire payload (`candidate_raw_sha256`).
- `exclusions: [...]` — every applied exclusion instance: `{class, path,
opaque_sha256?}` (§4). An empty list is an explicit claim that the CAS blob
reconstructs the wire bytes exactly (modulo nothing).
- `attempt_id` — the accounting join key (exists).
- `attempt_id` — the accounting join key.
### 3.2 Verification on call (forward)
At the seam, in this order:
1. Serialize the wire-bound candidate to canonical bytes `W`.
2. Persist the sealed record (already the order today: persist, then gate).
2. Persist the sealed record (persist, then gate).
3. **Reconstruct** `R` from the durable record just written: read back the
blob, undo nothing — instead apply the SAME exclusion map to `W` (redaction
and custody projection are not invertible; §5.1) — and compare byte-for-byte
@ -87,10 +83,7 @@ At the seam, in this order:
NOT blocked, and the verification never raises: this invariant is
observability, and `verify_sealed_candidate` is fail-soft by contract.
That last step is the one place the landed contract differs from the first
draft of this note, which asked for a fail-closed refusal through
`PhysicalAttemptPreparationFailed`. It was rejected on its own merits, not for
convenience:
Verification remains fail-soft for two reasons:
- The refusal it would add is not the same question as the existing gate. The
in-memory identity re-check above this call still refuses dispatch when the
@ -106,7 +99,7 @@ convenience:
disclosure, and a refusal path that can itself fail (write error, unreadable
root) would have to decide between a silent skip and a dead runtime.
So the landed rule is: the fact is mandatory, the block is not.
The mismatch must be disclosed without blocking dispatch.
`tests/test_model_send_seal.py` pins exactly this — a corrupted blob, a
tampered seal digest, a dropped seal block, an undisclosed exclusion class and
a foreign basis each produce their typed fact while the attempt still settles.
@ -152,7 +145,7 @@ violation.
| `secret_redaction` | Secret VALUES masked in the CAS blob by the observability redaction rules | The durable copy must not carry live credentials; equality is digest-anchored instead (pre-redaction sha256) | existing `RedactionRecord`s → `{class, path}` rows |
| `provider_native_custody` | Provider-owned opaque content (e.g. encrypted reasoning replay items) projected before persistence | Bytes are provider property; replay semantics are server-side | existing `anthropic_native_custody_projected` flag → per-item `{class, path, opaque_sha256}` |
| `transport_envelope` | HTTP headers, auth, SDK-added transport fields (user-agent, idempotency keys, `stream` flag where the SDK owns it) | Below the seam by construction; carries secrets and transport identity, not model-visible content | class-level row (no per-call enumeration) |
| `provider_side_transform` | Server-side effects the host cannot observe pre-flight: prompt-cache application, provider truncation/normalization | Not host-controlled; the seam is the LAST host-controlled point, not the last point | class-level row; conformance suite (CPL-6) owns per-provider characterization |
| `provider_side_transform` | Server-side effects the host cannot observe pre-flight: prompt-cache application, provider truncation/normalization | Not host-controlled; the seam is the LAST host-controlled point, not the last point | class-level row |
Delegated/harness model calls (`agent_session` executor lanes) are a
lane-level instance of `provider_side_transform`: the host never holds the
@ -217,24 +210,19 @@ from "our two copies agree" to "the durable record agrees with the wire".
narration planes (they remain projections; reverse-⟺ is `model_send` only).
- No logical-call identity across retry rungs (§5.3).
- No global "every log line reconstructs" framework — one seam, one record
kind, one sweep (plan: local decisions, no generic framework).
kind, one sweep.
- No new persistence plane: the seal extends the existing physical-candidate
manifest; facts ride `events.jsonl` + the seal's own directory.
## 7. Implementation sketch for the next lane (not this one)
## 7. Implementation owners
1. `llm_attempt.py`: extend `_candidate_before_dispatch` with read-back +
projection compare; thread the typed fact writer (small; the seam is one
closure).
2. `observability.py`: `model_send_seal` block in
`persist_physical_candidate` manifests (schema_version bump of the
manifest payload is NOT needed — additive key under the existing
`SCHEMA_VERSION` object; readers ignore unknown keys).
3. `server_maintenance.py`: reconciliation sweep behind the existing startup
sweep guardrails (fail-soft, bounded batch, UNKNOWN accounting state skips
destructive conclusions — there are none to skip: the sweep only writes
facts).
4. Tests: seal round-trip (write → reconstruct → equal); each §5 class forced
(mutating fake SDK, redaction-rule flip, double-assembly guard, per-rung
seals); reverse sweep on a synthetic orphan both ways; delegated-lane
`unobserved` disclosure.
- `llm_attempt.py` owns the pre-dispatch seam and its candidate identity gate.
- `model_send_seal.py` stamps the physical-candidate manifest, reads back the
durable projection, writes typed mismatch facts and reconciles both join
directions. The seal is an additive key under the existing manifest schema.
- `server_maintenance.py` runs the bounded reconciliation through the existing
startup sweep. Unknown accounting evidence does not become an orphan claim;
the sweep records facts without deleting records or fabricating attempts.
- `tests/test_model_send_seal.py` covers reconstruction, typed divergence,
non-blocking dispatch and reverse joins. Compacted history is resolved through
the live/archive union described in [Usage compaction](USAGE_COMPACTION.md).

View file

@ -1,21 +1,14 @@
# PERSISTENCE.md — durable data-plane inventory (CPL-4)
# PERSISTENCE.md — durable data-plane inventory
Every durable entity under the runtime data root (`DATA_DIR`, default
`~/Ouroboros/data/`), with four decisions per entity (plan §7 item 4):
**schema_version** (present / not needed / needed→candidate),
**migration** path, **retention** (bounded / rotated / unbounded-accepted /
unbounded→candidate), and **reset** semantics (what deleting the entity while
the server is stopped does). Decisions are LOCAL per entity — there is no
generic persistence framework, by design.
`~/Ouroboros/data/`), with its schema, migration path, retention and reset
semantics (what deleting the entity while the server is stopped does).
These contracts are local to each entity; there is no generic persistence
framework.
Provenance disclosure: the plan references "§16 findings" (undocumented
planes, unbounded ledgers, mismatched temp); that findings document is not
recoverable in the plan, spec, or campaign archives, so this inventory was
built from scratch by an AST scan of every `data/`-path constructor in
`ouroboros/`, `supervisor/`, `server.py` and `launcher.py`, cross-checked by
manual reads of every writer. The scan is pinned as a verify test:
`tests/test_persistence_inventory.py` re-runs it and requires every scanned
data-relative path to be covered by a row here (count-anchored both ways).
`tests/test_persistence_inventory.py` scans data-path constructors in
`ouroboros/`, `supervisor/`, `server.py` and `launcher.py` and requires every
scanned data-relative path to be covered by a row here (count-anchored both ways).
## Shared idioms (the vocabulary the rows use)
@ -27,13 +20,13 @@ data-relative path to be covered by a row here (count-anchored both ways).
- **GC retention** — `ouroboros/retention.py`: one owner knob
`OUROBOROS_GC_RETENTION_DAYS` (default 7, clamped 1–365; legacy per-subsystem
keys migrate). Governs subagent worktrees, headless/task drives, task trees,
service logs, and — since the CPL4 train — consumed schedule receipts,
service logs, consumed schedule receipts,
confirmed capability probes, delegate recovery/supervision sweeps, code_intel
and reconcile-failed prunes, memory-journal digesting and agent media.
- **Rotation** — `supervisor/state.py::rotate_jsonl_log_if_needed`: >800 KB →
atomic rename to `archive/<prefix>_<ts>.jsonl` under the append lock.
Applied on the supervisor tick to `chat.jsonl`, `progress.jsonl` and — since
the CPL4-C1..C4 train — `events.jsonl`, `tools.jsonl`, `supervisor.jsonl`,
Applied on the supervisor tick to `chat.jsonl`, `progress.jsonl`,
`events.jsonl`, `tools.jsonl`, `supervisor.jsonl`,
`task_reflections.jsonl`. Chain readers enumerate
`archive/<stem>_*.jsonl` name-sorted (chronological by construction);
`utils.jsonl_chain_handles` is the rotation-race-safe traversal
@ -44,11 +37,6 @@ data-relative path to be covered by a row here (count-anchored both ways).
- **Atomic writes** — `atomic_write_json`/`atomic_write_text` (tmp+rename) and
`update_json_locked` (sidecar `<file>.lock`); JSONL appends go through
`append_jsonl` (O_APPEND + sidecar lock) unless noted.
- **Candidate fixes** — the CPL4-C1..C23 candidate table lives in the campaign
ledger (`docs/v7next/LEDGER_CORRECTIONS.md`, F5 lane B section). The
mechanical train (owner №9=A) plus owner batch №8 closed every row except
CPL4-C6 (usage-ledger compaction — its own reviewed lane, monetary
authority); rows above cite their CPL4-Cn as provenance, not as open gaps.
## 1. Root files
@ -56,7 +44,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
|---|---|---|---|---|---|
| `settings.json` | `ouroboros/config.py` save_settings (lock `settings.json.lock`, integrity guard) | JSON env-key map | none — not needed: migration is per-key inside `load_settings` (legacy keys migrate on read); external sha256 pin `OUROBOROS_SETTINGS_SHA256` makes it immutable when set | fixed-size overwrite | recreated from defaults; ALL owner secrets/modes lost — never delete casually |
| `.ouroboros_isolated_benchmark` | EXTERNAL writer — the benchmark launchers (`devtools/benchmarks/evolve_smoke.py`, `devtools/benchmarks/editbench/run_editbench.py`, `devtools/benchmarks/cybergym/cybergym_server.py`) stamp it into their throwaway data root; the runtime only READS it (`supervisor/state.py` rotate suppression, `ouroboros/agent_startup_checks.py`) | one-line marker text | none — not needed: presence IS the fact | write-once per benchmark drive; never GC'd | the drive stops declaring itself synthetic: JSONL rotation and the benchmark-only startup carve-outs switch back to live-root behaviour on a throwaway root |
| `settings.json.lock`, `*.lock` sidecars, `locks/**` | `ouroboros/platform_layer.py` lock family (+ `supervisor/state.py`, `supervisor/update_merge.py`, `ouroboros/skill_lifecycle_queue.py`) | O_EXCL lockfiles (unlinked on release) or flock files (persistent); plus one transient kernel-lock capability probe file per lock directory (`.kernel-lock-probe.<pid>.<ns>`, created, locked and unlinked once per process by `kernel_file_locks_enforced`) | none — not needed | self-healing (mtime/pid staleness) — except a lock whose owner died and whose pid was REUSED by a live process: it reads as alive whoever owns the pid (`kill(0)` succeeds for a same-uid impostor and answers EPERM for another user's — both alive since round 5.4), is never reclaimed by age while the impostor lives, and needs a hand repair meanwhile | zero durable state; deleting while stopped is a no-op |
| `settings.json.lock`, `*.lock` sidecars, `locks/**` | `ouroboros/platform_layer.py` lock family (+ `supervisor/state.py`, `supervisor/update_merge.py`, `ouroboros/skill_lifecycle_queue.py`) | O_EXCL lockfiles (unlinked on release) or flock files (persistent); plus one transient kernel-lock capability probe file per lock directory (`.kernel-lock-probe.<pid>.<ns>`, created, locked and unlinked once per process by `kernel_file_locks_enforced`) | none — not needed | self-healing (mtime/pid staleness) — except a lock whose owner died and whose pid was REUSED by a live process: it reads as alive whoever owns the pid (`kill(0)` succeeds for a same-uid impostor and answers EPERM for another user's — both mean alive), is never reclaimed by age while the impostor lives, and needs a hand repair meanwhile | zero durable state; deleting while stopped is a no-op |
## 2. `state/` — singletons (fixed-size overwrite)
@ -66,11 +54,11 @@ data-relative path to be covered by a row here (count-anchored both ways).
| `state/queue_snapshot.json` | `supervisor/queue_snapshot.py` | `_schema_version: 1` (ABI-2) | overwrite; self-expires (max_age 900 s) | absent = 0 restored; queued-unstarted tasks silently dropped |
| `state/direct_roots.json` | `supervisor/direct_roots.py` — the main loop's off-lock projection of live direct-chat turns, written beside the snapshot on every tick (`publish_direct_roots`, `server.py`) and emptied when the queue initializes or restores (`clear_direct_roots`, `supervisor/queue.py`) so a dead process's turns never outlive it | none — not needed: the whole file is re-derived every tick, and an unreadable one reads as no direct roots | overwrite; the rows are whatever the actor registry holds this tick, and a turn mid-admission (its actor lock only ever TRIED) is skipped behind ONE aggregate `incomplete` fact rather than blocking the loop | absent/empty = no live direct-chat roots are known: a task's peer roster (`ouroboros/peer_roster.py`) lists only the pooled roots from `state/queue_snapshot.json` until the next tick rewrites it |
| `state/advisory_review.json` | `ouroboros/review_state.py` (lock `locks/advisory_review.lock`) | own pre-ABI-2 spelling `state_version: 3` (+duplicate `schema_version`) — kept; `_schema_version` deliberately avoids this key | bounded on write: runs 10, attempts 50, debts 50; `open_obligations` coalesced | recreated empty; recorded blocking obligations forgiven, commit gate demands fresh review (fail-closed) |
| `state/scheduled_tasks.json` | `supervisor/queue_schedules.py` | `schema_version: 1` — authored at the write seam (`_write_scheduled_tasks`, CPL4-C7); legacy files gain it on their next write | consumed `once` receipts age out past GC retention on the scheduler tick (CPL4-C7; `prune_consumed_once_records`); 2 MB WARN = prune broken or live set huge | owner cron/once schedules lost; skill-manifest schedules resync automatically |
| `state/scheduled_tasks.json` | `supervisor/queue_schedules.py` | `schema_version: 1` — authored at the write seam (`_write_scheduled_tasks`); legacy files gain it on their next write | consumed `once` receipts age out past GC retention on the scheduler tick (`prune_consumed_once_records`); 2 MB WARN = prune broken or live set huge | owner cron/once schedules lost; skill-manifest schedules resync automatically |
| `state/terminal_deliveries.json` | `supervisor/terminal_delivery.py` | own `schema_version: 2` | bounded: delivered 512, pending 64, replays 5 | dedupe + owed-outbox lost: possible double- or never-delivery of one buffered terminal answer |
| `state/cancel_intents.json` | `ouroboros/cancel_intents.py` | own `schema_version: 1` | self-draining (settled rows leave) | in-flight cancels lost: cancelled-unsettled task revives as pending; forensics survive in supervisor.jsonl |
| `state/update_letter.json` | `ouroboros/update_letter.py` (`refresh_after_check` — the ONE writer, synchronous inside a FETCHING update check: boot and the Updates panel's check button) | own record shape keyed by the update range (`base_sha`, `target_sha`, channel, target ref) — no `_schema_version` | overwrite per check; the letter is never deleted after the update lands (it outlives its range; `project_letter` reads `applied`/`other` by SHA equality + the recorded `target_in_head` fact) | absent = no letter: the Updates panel and the agent's Runtime context (`official_update_projection`) show the check's status alone; the next FETCHING check rewrites it |
| `state/capability_evidence.json` | `ouroboros/capability_evidence.py` | none — accepted (self-healing cache; TTLs on read) | expired probe keys drop at the write seam (CPL4-C8): failed/unprobeable past their read TTL, confirmed past GC retention (blip-keep evidence survives inside retention); owner acks never expire | recreated; ≥1M-context gates fail closed to `unknown`, owner acks must be re-given |
| `state/capability_evidence.json` | `ouroboros/capability_evidence.py` | none — accepted (self-healing cache; TTLs on read) | expired probe keys drop at the write seam: failed/unprobeable past their read TTL, confirmed past GC retention (blip-keep evidence survives inside retention); owner acks never expire | recreated; ≥1M-context gates fail closed to `unknown`, owner acks must be re-given |
| `state/evolution_campaign.json` | `supervisor/evolution_lifecycle.py` (CAS under state.lock) | own `schema_version: 1` (campaign) / 2 (active_transaction); the active transaction carries the pre-commit `commit_intent` (reviewed tree + parents) written BEFORE `git commit`, which boot recovery turns into the `commit_receipt` a crash never wrote | bounded histories (50) | in-flight self-modification transaction unabsorbable; anti-repeat fingerprints lost |
| `state/evolution_metrics_cache.json` | `ouroboros/utils.py` | own `schema: 1` (strictly validated) | one point per git tag, no prune — accepted (derived cache) | pure cache; recomputed from git |
| `state/projects.json`, `state/project_task_bindings.json` | `ouroboros/projects_registry.py` (sidecar locks) | `_schema_version: 2` / `1` (ABI-2) | never age-pruned (owner curates); deletes are durable tombstones | tombstones live here: losing it can resurrect deleted project rooms (marker unlink mitigates) |
@ -99,7 +87,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
| Path | Writer | schema/record marker | Retention | Reset |
|---|---|---|---|---|
| `state/usage_attempts.jsonl` (+ `state/usage_attempts.quarantine.jsonl`, `state/usage_attempts.lock`) | `ouroboros/usage_ledger.py` single chokepoint (own O_APPEND+fsync under named lock — NOT append_jsonl); `ouroboros/usage_compaction.py` rewrites it whole (verified candidate, atomic swap) under the same lock | no `_schema_version`; its own validated contract: dense `seq`, `kind` discriminator, `state` machine, per-row `candidate_measurement_kind`, attribution `physical_attempt_v1`; compacted files lead with a stamped `usage_baseline` header + `usage_baseline_group` rows — accepted | bounded by CPL4-C6 compaction (config `USAGE_LEDGER_COMPACT_BYTES`, 8 MB): terminal non-review attempt chains fold into the baseline block, raw segment archived first (fsync'd) — see docs/v7next/DESIGN_USAGE_COMPACTION.md; 20 MB WARN can reflect broken compaction, a large unfoldable residue, or a refused/skipped pass: the name-tier refusal emits `usage_ledger_compaction_refused` once per process per data root; a policy abort (`_Abort`) emits `usage_ledger_compaction_skipped` once per process per (data root, reason); the two snapshot-race exits before archive/swap only log warnings, without a typed event; torn tails — and a charge a compaction swap erased inside its last syscall, read back from the old inode (POSIX, round 5.4) — quarantined, never GC'd | monetary history destroyed, `seq` restarts, budget fences read $0; watermark survives so legacy import will NOT re-run — deleting the ledger alone is unrecoverable; and a ledger reset while `archive/usage_ledger/` survives leaves every history question a permanent `generation newer` corruption verdict (the stamp-less anchor) — move/delete the archive with it, or keep both |
| `state/usage_attempts.jsonl` (+ `state/usage_attempts.quarantine.jsonl`, `state/usage_attempts.lock`) | `ouroboros/usage_ledger.py` single chokepoint (own O_APPEND+fsync under named lock — NOT append_jsonl); `ouroboros/usage_compaction.py` rewrites it whole (verified candidate, atomic swap) under the same lock | no `_schema_version`; its own validated contract: dense `seq`, `kind` discriminator, `state` machine, per-row `candidate_measurement_kind`, attribution `physical_attempt_v1`; compacted files lead with a stamped `usage_baseline` header + `usage_baseline_group` rows — accepted | bounded by compaction (config `USAGE_LEDGER_COMPACT_BYTES`, 8 MB): terminal non-review attempt chains fold into the baseline block, raw segment archived first (fsync'd) — see docs/USAGE_COMPACTION.md; 20 MB WARN can reflect broken compaction, a large unfoldable residue, or a refused/skipped pass: the name-tier refusal emits `usage_ledger_compaction_refused` once per process per data root; a policy abort (`_Abort`) emits `usage_ledger_compaction_skipped` once per process per (data root, reason); the two snapshot-race exits before archive/swap only log warnings, without a typed event; torn tails — and a charge a compaction swap erased inside its last syscall, read back from the old inode (POSIX) — quarantined, never GC'd | monetary history destroyed, `seq` restarts, budget fences read $0; watermark survives so legacy import will NOT re-run — deleting the ledger alone is unrecoverable; a ledger reset beside `archive/usage_ledger/` makes history queries raise `generation newer` while an unreferenced segment represents a newer generation and is not a byte-prefix of the live file; after fresh compactions reach the surviving archive generations, those old unreferenced segments are skipped and their attempt IDs remain absent — see [history readers](USAGE_COMPACTION.md#10-history-readers-model-send-reconciliation-and-audits); keep or reset the ledger and archive together |
| `state/skill_review_root_tasks.jsonl` (+ `state/skill_review_root_tasks.gaps.jsonl`) | `ouroboros/skill_review_history.py` (`_append_root_task_projection_once`: one compact row per root task appended when its skill review lands; a row the projection could not attribute goes to the `.gaps.jsonl` ledger via `_record_root_task_projection_gap`) | rows carry `usage_attribution_schema: physical_attempt_v1`; no version key — accepted (derived index over the per-skill `review_history.jsonl`, P7) | unbounded append; reads are BOUNDED (`skill_readiness.py`: 1 MiB / 512 records tail, a truncated or gapped read is disclosed as `projection_incomplete`) and the startup hot-store check warns past `SKILL_REVIEW_ROOT_TASKS_WARN_BYTES` | delete with `state/`; the index is not rebuilt — readiness and the acceptance packet read `projection_incomplete` until new rows accrue (the per-skill histories keep the truth) |
| `state/process_ledger.jsonl` | `ouroboros/process_custody.py` (spawn chokepoint) | none — downgrade-safe field split (`start_time`/`start_time_boot`) is the versioning device — accepted | self-compacting: reapers rewrite survivors-only | prior-generation supervised processes permanently orphaned (fingerprint index lost) |
| `state/evolution_checkpoints.jsonl` | `ouroboros/evolution_checkpoints.py` | `schema_version: 1` on every row (+`kind` on outcome rows) | unbounded append; read bounded (last 200) — accepted (structured solve-capability history is the product) | absorbed/abandoned objectives can be re-proposed (BUG3 regression); the outcome row is DERIVABLE from the resolved campaign transaction, so a row lost to a crash between the two writes is replayed at boot (`source: boot_backfill`) |
@ -109,14 +97,14 @@ data-relative path to be covered by a row here (count-anchored both ways).
| Path | Writer | schema_version | Retention | Reset |
|---|---|---|---|---|
| `state/skills/<name>/` owner state (`review.json`, `review_job.json`, `grants.json`, `enabled.json`, `deps.json`, `self_authored.json`, `owner_attestation.json`, `accepted_rebuttals.json`, `health.json`, `uninstalled.json`, provenance sidecars, `auto_repair.json`, `presence_profile_state.json`) | `ouroboros/skill_loader.py`, `skill_review_runner.py`, `skill_owner_attestation.py`, `skill_review_cycles.py`, `skill_uninstall_state.py`, `extension_health.py`, `marketplace/*`, `ouroboros/gateway/marketplace.py`; allowlist SSOT `contracts/skill_payload_policy.py` | `deps.json`/`self_authored.json`/provenance: `schema_version: 1`; `review.json`/`enabled.json`/`grants.json`/`review_job.json`/`owner_attestation.json`/`accepted_rebuttals.json`: `_schema_version: 1` (ABI-2, stamp-on-write — CPL4-C10; readers keep legacy-0 tolerance, unstamped files never retrofitted); verdict/grant staleness stays pinned by `content_hash` | no age GC; hub uninstalls write an `uninstalled.json` tombstone and the startup sweep clears the dead state BY that mark (CPL4-C11, owner 3A) — `grants.json` survives as owner authority, a reinstall self-heals the tombstone; the gateway's local delete removes the whole state dir | absent state = disabled + pending review + grants revoked (fail-closed); `owner_attestation` absence invalidates its verdict |
| `state/skills/<name>/review_history.jsonl` + `review_dispatch/` (legacy `review_dispatch.json`) | `ouroboros/skill_review_history.py` | rows carry `usage_attribution_schema: physical_attempt_v1`; no version key — accepted (derived-counter SSOT, P7) | history unbounded per skill — accepted with BOUNDED reads (CPL4-C12): every reader windows the 4 MB tail (`find_history_job_bounded` idiom); lifecycle terminal rows persist their ordinals so counters stay exact inside the window (a group aged past it restarts low — under-counts, never over-blocks); per-skill archive rotation declined (no per-skill archive plane; disclosed) | review-cycle ceiling resets to zero; paid dispatches become free again |
| `state/skills/<name>/` owner state (`review.json`, `review_job.json`, `grants.json`, `enabled.json`, `deps.json`, `self_authored.json`, `owner_attestation.json`, `accepted_rebuttals.json`, `health.json`, `uninstalled.json`, provenance sidecars, `auto_repair.json`, `presence_profile_state.json`) | `ouroboros/skill_loader.py`, `skill_review_runner.py`, `skill_owner_attestation.py`, `skill_review_cycles.py`, `skill_uninstall_state.py`, `extension_health.py`, `marketplace/*`, `ouroboros/gateway/marketplace.py`; allowlist SSOT `contracts/skill_payload_policy.py` | `deps.json`/`self_authored.json`/provenance: `schema_version: 1`; `review.json`/`enabled.json`/`grants.json`/`review_job.json`/`owner_attestation.json`/`accepted_rebuttals.json`: `_schema_version: 1` (ABI-2, stamp-on-write — readers keep legacy-0 tolerance, unstamped files never retrofitted); verdict/grant staleness stays pinned by `content_hash` | no age GC; hub uninstalls write an `uninstalled.json` tombstone and the startup sweep clears the dead state BY that mark — `grants.json` survives as owner authority, a reinstall self-heals the tombstone; the gateway's local delete removes the whole state dir | absent state = disabled + pending review + grants revoked (fail-closed); `owner_attestation` absence invalidates its verdict |
| `state/skills/<name>/review_history.jsonl` + `review_dispatch/` (legacy `review_dispatch.json`) | `ouroboros/skill_review_history.py` | rows carry `usage_attribution_schema: physical_attempt_v1`; no version key — accepted (derived-counter SSOT, P7) | history unbounded per skill — accepted with BOUNDED reads: every reader windows the 4 MB tail (`find_history_job_bounded` idiom); lifecycle terminal rows persist their ordinals so counters stay exact inside the window (a group aged past it restarts low — under-counts, never over-blocks); per-skill archive rotation declined (no per-skill archive plane; disclosed) | review-cycle ceiling resets to zero; paid dispatches become free again |
| `state/delegate_project_retirements/<sha256[:24]>.lock` | `ouroboros/delegate_custody_usage.py` (`project_retirement_lock`: exclusive file lock around one project's settlement/retirement decision; stale after 120 s, owner-aware) | none — not needed (lock file, no payload) | one file per project ever settled; reclaimed as stale by the next holder | delete freely; a live holder re-creates its lock |
| `state/skills/<name>/` transport dirs: `extension_calls/`, `__extension_imports/` | `ouroboros/extension_process_runner.py`, `extension_import_staging.py` | none — not needed (per-call transport files, staged import trees) | per-call files consumed; import leaves reaped owner-dead+grace | transient; recreated per call |
| `state/delegate_recovery/`, `state/delegate_recovery_transactions/` (+`active.json`), `state/delegate_supervision/` | `ouroboros/delegate_recovery.py`, `delegate_supervision.py` (+ startup sweep `delegate_state_sweep.py`) | own `schema: 1` on supervision/transactions; recovery rows fingerprinted, unversioned | terminal+age startup sweep (CPL4-C13): terminal-status recovery rows (`vetoed`/`adopted`), unreferenced transactions and settled-task supervision files past GC retention; live/resumable rows, `active.json`, no-result tasks and unreadables kept fail-closed; unreadable custody log skips the sweep | interrupted delegated runs cancelled instead of adopted; duplicate wake replay; planned handoffs vetoed |
| `state/delegate_recovery/`, `state/delegate_recovery_transactions/` (+`active.json`), `state/delegate_supervision/` | `ouroboros/delegate_recovery.py`, `delegate_supervision.py` (+ startup sweep `delegate_state_sweep.py`) | own `schema: 1` on supervision/transactions; recovery rows fingerprinted, unversioned | terminal+age startup sweep: terminal-status recovery rows (`vetoed`/`adopted`), unreferenced transactions and settled-task supervision files past GC retention; live/resumable rows, `active.json`, no-result tasks and unreadables kept fail-closed; unreadable custody log skips the sweep | interrupted delegated runs cancelled instead of adopted; duplicate wake replay; planned handoffs vetoed |
| `state/delegate_actor_claims/*.lock`, `state/.payload_delegation_claim.lock` | `ouroboros/delegate_custody.py`, `delegate_start_claims.py` | none — locks | unlinked on release | no durable state |
| `state/code_intel/<root-sha>/inventory.json` | `ouroboros/code_intelligence.py` | own `schema_version: 2` (older/malformed rebuilt silently) | per-repo rewrite in place; stale roots age-pruned at startup by `inventory.json` mtime past GC retention (CPL4-C14, pure cache) | pure derived cache; one full re-index |
| `state/extension_reconcile/` (+`failed/`) | `ouroboros/extension_reconcile_queue.py` | none — not needed (one-shot markers) | consumed by server loop; after 5 attempts moved to `failed/`, where markers age-prune past GC retention (CPL4-C15; the failure fact stays durable in events.jsonl) | pending worker→server reconciles lost; re-toggle heals |
| `state/code_intel/<root-sha>/inventory.json` | `ouroboros/code_intelligence.py` | own `schema_version: 2` (older/malformed rebuilt silently) | per-repo rewrite in place; stale roots age-pruned at startup by `inventory.json` mtime past GC retention (pure cache) | pure derived cache; one full re-index |
| `state/extension_reconcile/` (+`failed/`) | `ouroboros/extension_reconcile_queue.py` | none — not needed (one-shot markers) | consumed by server loop; after 5 attempts moved to `failed/`, where markers age-prune past GC retention (the failure fact stays durable in events.jsonl) | pending worker→server reconciles lost; re-toggle heals |
| `state/workspace_executor_processes/` | `ouroboros/workspace_executor.py` | own `schema_version: 1` + owner tag | unlink on stop; stale rows filtered at read (pid/cmd-sha) | service processes survive unreaped |
| `state/acceptance_fence_acks/` | `supervisor/events_worker_reports.py` (writer), `ouroboros/agent.py` (read+unlink) | none — transport ack | inline GC on write (255 newest / 3600 s) | waiting worker fails closed (TimeoutError) |
| `state/headless_tasks/<id>/` (child data drives) | `ouroboros/headless.py` | child `state/state.json`: `schema_version: 1` | GC-retention prune at startup (terminal + age; skips artifacts-not-terminal / refs-unpromoted) | in-flight child drives and unpromoted child refs lost |
@ -126,7 +114,7 @@ data-relative path to be covered by a row here (count-anchored both ways).
| `state/review_continuations/<task>.json` (+ `state/review_continuations/archived/**`, `state/review_continuations/corrupt/**` with its `.txt` reason notes) | `ouroboros/task_continuation.py` (atomic write; `rename` to `archived/` on retire, `replace_atomic` to `corrupt/` on quarantine) | none — accepted: a typed dataclass with an ownership check (a `task_id` mismatch raises) and malformed JSON quarantined, never migrated | live payload per blocked task, cleared on resume; a settled, un-resumed continuation is retired to `archived/` past `RETIRE_SETTLED_CONTINUATION_AFTER_DAYS` (7) so history stops riding into every new prompt; `archived/` and `corrupt/` unbounded — accepted (small typed payloads; the quarantine IS the corruption evidence) | blocked tasks lose their resume pointer: findings, obligations and the commit intent must be re-derived by re-running review |
| `state/skills/<name>/auth_token.json` | `ouroboros/extension_plugin_api.py` `mint_skill_token` (atomic write, chmod 0600) | none — not needed: the token is bound to `content_hash`, which IS its staleness contract | one token per skill, rotated on a content-hash change; a transient hash failure reuses or fails closed, never rotates | the next mint issues a new token, so a companion still holding the old one is de-authorized until respawn — do not delete while companions run |
| `state/skills/<name>/repair_admission.json` | `ouroboros/skill_repair_admission.py` `record_repair_admission` (atomic; the newest admission owns the record) | own `schema_version: 1` | one record per skill, superseded by the next admitted repair | the repair CAS has nothing to check against and its writes are refused — fail-closed by design, since this record exists to remove exactly that fail-open |
| `state/skills/<name>/jobs/<job>/` (`assets/`, `output/`, `tmp/`) | `ouroboros/extension_plugin_api.py` `skill_job_dir` (creates the three children on request; the extension owns the contents) | none — not needed: a workspace, not a record | unbounded — candidate: nothing sweeps it, and the gateway's local skill delete is the only path that removes it with the state dir (disclosed by the stage-2 fix wave in `docs/v7next/LEDGER_CORRECTIONS.md`) | in-flight extension jobs lose their assets and output; the next call recreates the tree |
| `state/skills/<name>/jobs/<job>/` (`assets/`, `output/`, `tmp/`) | `ouroboros/extension_plugin_api.py` `skill_job_dir` (creates the three children on request; the extension owns the contents) | none — not needed: a workspace, not a record | unbounded: nothing sweeps it, and the gateway's local skill delete is the only path that removes it with the state dir | in-flight extension jobs lose their assets and output; the next call recreates the tree |
| `state/skills/<name>/chat_id_counter.json` | `ouroboros/gateway/host_service.py` `allocate_internal_chat_id` (atomic, under the in-process counter lock) | none — not needed: `range_name` plus last/next id | one file per skill; ids descend from `A2A_CHAT_ID_MAX` | allocation restarts at the top of the range, so a fresh A2A room can reuse a chat id already present in history |
| `state/project_source_locks/` | none in this tree — orphan plane seen in live layouts (removed feature leftover) | none | n/a | harmless; nothing reads or recreates it |
@ -136,14 +124,14 @@ data-relative path to be covered by a row here (count-anchored both ways).
|---|---|---|---|---|
| `logs/chat.jsonl` | `supervisor/message_bus.py` (+presence, project summaries) | `direction` + optional `type`; no version — accepted (projection replayed by chain-aware readers) | rotated 800 KB → `archive/chat_*.jsonl`; archive chain WARN at 100 MB | newest generation lost; consolidation cursor reports gap (recoverable) |
| `logs/progress.jsonl` | `supervisor/message_bus.py` (+plan review) | `type: send_message`, `is_progress` | rotated 800 KB → `archive/progress_*.jsonl`; 8 MB WARN = rotation broken | current segment lost; readers archive-chain-aware |
| `logs/events.jsonl` | ~60 modules via `append_jsonl` (+`delegate_custody.emit`) | universal `type` discriminator — accepted (per-type payloads owned by emitters) | rotated 800 KB → `archive/events_*.jsonl` (CPL4-C1); custody readers (replay, fault tail-scan, `complete_custody_rows`, settled-terminal chain cursor, legacy-usage import, swarm rollup, worker-boot verify) are chain-aware; 8 MB live WARN = rotation broken; 100 MB chain WARN = replay degradation | delegated-run custody destroyed (chain incl. archive segments): open runs invisible/unreapable; lineage, citations, legacy-usage source lost |
| `logs/tools.jsonl` | `ouroboros/loop_tool_execution.py` (+budget-drive mirror) | `type: tool_call`, untruncated args | rotated 800 KB → `archive/tools_*.jsonl` (CPL4-C2); tail readers (api_logs_tail, task_events) archive-backfill; 8 MB WARN = rotation broken | untruncated tool record + `result_ref` pointers lost |
| `logs/supervisor.jsonl` | supervisor family, `process_custody`, gateway control, server shutdown | `type` (+secondary `event_type`) | rotated 800 KB → `archive/supervisor_*.jsonl` (CPL4-C3) + 8 MB tripwire; tail readers (`memory.read_jsonl_tail`, api_logs_tail) archive-backfill | reap receipts, rescue disclosures, shutdown causes lost |
| `logs/task_reflections.jsonl` | `ouroboros/reflection.py` (+ project-scoped copy under `projects/<id>/logs/`) | full rows unversioned; pointer rows `type: project_reflection_pointer` | rotated 800 KB → `archive/task_reflections_*.jsonl` (CPL4-C4) + 8 MB tripwire; tail-20 read archive-backfills; project-scoped copies follow project retention (never age-pruned) | inter-task memory-carry signal lost |
| `logs/events.jsonl` | ~60 modules via `append_jsonl` (+`delegate_custody.emit`) | universal `type` discriminator — accepted (per-type payloads owned by emitters) | rotated 800 KB → `archive/events_*.jsonl`; custody readers (replay, fault tail-scan, `complete_custody_rows`, settled-terminal chain cursor, legacy-usage import, swarm rollup, worker-boot verify) are chain-aware; 8 MB live WARN = rotation broken; 100 MB chain WARN = replay degradation | delegated-run custody destroyed (chain incl. archive segments): open runs invisible/unreapable; lineage, citations, legacy-usage source lost |
| `logs/tools.jsonl` | `ouroboros/loop_tool_execution.py` (+budget-drive mirror) | `type: tool_call`, untruncated args | rotated 800 KB → `archive/tools_*.jsonl`; tail readers (api_logs_tail, task_events) archive-backfill; 8 MB WARN = rotation broken | untruncated tool record + `result_ref` pointers lost |
| `logs/supervisor.jsonl` | supervisor family, `process_custody`, gateway control, server shutdown | `type` (+secondary `event_type`) | rotated 800 KB → `archive/supervisor_*.jsonl` + 8 MB tripwire; tail readers (`memory.read_jsonl_tail`, api_logs_tail) archive-backfill | reap receipts, rescue disclosures, shutdown causes lost |
| `logs/task_reflections.jsonl` | `ouroboros/reflection.py` (+ project-scoped copy under `projects/<id>/logs/`) | full rows unversioned; pointer rows `type: project_reflection_pointer` | rotated 800 KB → `archive/task_reflections_*.jsonl` + 8 MB tripwire; tail-20 read archive-backfills; project-scoped copies follow project retention (never age-pruned) | inter-task memory-carry signal lost |
| `logs/containment_faults.jsonl` | `ouroboros/delegate_custody.py` (mirrored to events.jsonl) | `type` ∈ CONTAINMENT_FAULT/RESOLVED joined on run_id | unbounded BY DESIGN — read whole so an open fault never ages out — accepted | health invariant degrades to the 4 MB events tail scan (the regression this file fixed) |
| `logs/chat_annotations.jsonl` | `ouroboros/project_dialogue.py` (`append_jsonl` under the shared sidecar append lock) | `type: chat_annotation` keyed by `(client_message_id, routing_token)`; latest row per key wins (an owner message keeps one row per routing act; task-authored acts ride synthetic `agent-steer:<token>` ids) | self-compacting at 800 KB under the append lock: the latest row of every message still present in the chat chain (live `chat.jsonl` + the 3 newest `archive/chat_*.jsonl`) is rewritten, the rest is DROPPED — presentation state, so it is not rotated into `archive/` | annotation cards fall back to their plain chat rows; the one named exception (#198) also loses the durable picker decision-card token, so a pending manual routing choice must be made again |
| `logs/tasks/task_<id>.txt` | `ouroboros/utils.py` log sanitization (`write_text`, best-effort) | raw oversized task text, no envelope; the log row keeps `text_full_path` | unbounded → candidate: no retention sweep names `logs/tasks/` (disclosed by the stage-2 fix wave in `docs/v7next/LEDGER_CORRECTIONS.md`) | the truncated text in the log row stays; only the spilled full text of oversized task prompts is lost |
| `logs/agent_stdout.log` | `launcher.py` pipe-copy thread | unstructured text | bounded ~8 MB (2 MB × `.1..3` backups, rotated by the copy thread — CPL4-C5) | pre-logging crash output lost; nothing parses it |
| `logs/tasks/task_<id>.txt` | `ouroboros/utils.py` log sanitization (`write_text`, best-effort) | raw oversized task text, no envelope; the log row keeps `text_full_path` | unbounded: no retention sweep names `logs/tasks/` | the truncated text in the log row stays; only the spilled full text of oversized task prompts is lost |
| `logs/agent_stdout.log` | `launcher.py` pipe-copy thread | unstructured text | bounded ~8 MB (2 MB × `.1..3` backups, rotated by the copy thread) | pre-logging crash output lost; nothing parses it |
| `logs/server.log` (+`.1..3`), `logs/launcher.log` | stdlib `RotatingFileHandler` (`server.py`, `launcher.py`) with secret-redacting filter | text | bounded ~8 MB (2 MB × 4) — the model citizen | stdlib log history lost; nothing parses it |
## 6. `memory/` (Ouroboros cognition — operator read-only)
@ -157,30 +145,30 @@ data-relative path to be covered by a row here (count-anchored both ways).
| `memory/dialogue_blocks.json` + `dialogue_meta.json` | `ouroboros/consolidator.py` (locked atomic) | none | bounded by era compression (10 blocks, oldest 4 compressed) | blocks: compressed biography irreproducible; meta: full re-consolidation (cost, not loss) |
| `memory/dialogue_summary.md` | none — legacy read-only (reader in context.py) | none | frozen | legacy artifact; nothing writes it |
| `memory/knowledge/**` (topic .md + `index-full.md` + `patterns.md`) | `ouroboros/tools/knowledge.py`, `consolidator.py` (index rebuild), `reflection.py` (patterns CAS rewrite) | none | topic files unbounded — accepted (curated by consolidation); backlog topic merge-only fail-closed | recreated lazily; knowledge lost |
| `memory/*_journal.jsonl`, `memory/knowledge_history.jsonl`, `memory/knowledge/patterns_history.jsonl` | `ouroboros/memory.py`, `tools/control_runtime.py`, `tools/knowledge.py`, `reflection.py` — every append through the `append_jsonl` sidecar-lock seam (CPL4-C17) | scratchpad journal: `type` rows; others unversioned full-text snapshots; digested rows carry `content_digested: true` | full old+new text only inside GC retention (CPL4-C16, owner 4A): older identity/knowledge/patterns rows go digest-only (sha256+len) at startup (`memory_journal_compaction.py`, under the append lock, unreadable lines byte-preserved); scratchpad journal keeps its own eviction contract | undo/provenance record lost (live .md survives); eviction/rewrite paths fail closed when journal append fails; digested history is irreversible by design |
| `memory/owner_mailbox/<task>.jsonl` + `.acks.jsonl` | `ouroboros/owner_mailbox.py` (append-only; revocation appends, reader resolves) | `kind` discriminator | lifecycle-bounded: unlinked at task terminal; a startup sweep unlinks mailboxes whose task has a SETTLED durable result (CPL4-C18; no result / non-terminal keeps the mailbox fail-closed) | undelivered owner directives + restart-surviving hurry latch lost; acks lost ⇒ re-delivery |
| `memory/*_journal.jsonl`, `memory/knowledge_history.jsonl`, `memory/knowledge/patterns_history.jsonl` | `ouroboros/memory.py`, `tools/control_runtime.py`, `tools/knowledge.py`, `reflection.py` — every append through the `append_jsonl` sidecar-lock seam | scratchpad journal: `type` rows; others unversioned full-text snapshots; digested rows carry `content_digested: true` | full old+new text only inside GC retention: older identity/knowledge/patterns rows go digest-only (sha256+len) at startup (`memory_journal_compaction.py`, under the append lock, unreadable lines byte-preserved); scratchpad journal keeps its own eviction contract | undo/provenance record lost (live .md survives); eviction/rewrite paths fail closed when journal append fails; digested history is irreversible by design |
| `memory/owner_mailbox/<task>.jsonl` + `.acks.jsonl` | `ouroboros/owner_mailbox.py` (append-only; revocation appends, reader resolves) | `kind` discriminator | lifecycle-bounded: unlinked at task terminal; a startup sweep unlinks mailboxes whose task has a SETTLED durable result (no result / non-terminal keeps the mailbox fail-closed) | undelivered owner directives + restart-surviving hurry latch lost; acks lost ⇒ re-delivery |
## 7. Skills payloads, tasks, uploads, projects, services
| Path | Writer | Marker | Retention | Reset |
|---|---|---|---|---|
| `skills/{native,clawhub,ouroboroshub,external}/<name>/**` (+`.staging/`, `.ouroboros_env/` with `cache/ tmp/ home/`) | `ouroboros/marketplace/*`, `launcher_bootstrap.py` seed, agent self-authoring | provenance sidecars `schema_version: 1`; env `fingerprint.json: 1` | no age GC (payloads are installed software); staging rmtree'd per install, crash orphans recognized by name fragments; package caches live with the skill | bucket recreated empty; native seeds NOT re-seeded (deletion intent preserved) except post-bootstrap set; orphaned `state/skills/` rows keep grants + tombstone only after the CPL4-C11 sweep |
| `skills/{native,clawhub,ouroboroshub,external}/<name>/**` (+`.staging/`, `.ouroboros_env/` with `cache/ tmp/ home/`) | `ouroboros/marketplace/*`, `launcher_bootstrap.py` seed, agent self-authoring | provenance sidecars `schema_version: 1`; env `fingerprint.json: 1` | no age GC (payloads are installed software); staging rmtree'd per install, crash orphans recognized by name fragments; package caches live with the skill | bucket recreated empty; native seeds NOT re-seeded (deletion intent preserved) except post-bootstrap set; orphaned `state/skills/` rows keep grants + tombstone after the startup sweep |
| `state/skills/<name>/dependency_cache/` | `marketplace/isolated_deps.py` via existing installer subprocesses and verified resource downloads | downloaded resources keyed by sha256; package-manager cache formats | reused across payload/environment replacement; follows existing skill-state cleanup | resources are verified/downloaded again and package caches rebuilt |
| `state/skills/<name>/go/`, `state/skills/<name>/go-cache/` | Go compiler launched by `tools/skill_exec.py:_run_go_skill`, with GOPATH/GOCACHE bound to skill_state_dir | Go-owned cache formats | reused across script runs; follows existing skill-state cleanup | compiler recreates caches; skill payload and review remain unchanged |
| `task_results/<id>.json` | `ouroboros/task_results.py` (locked merge) | `_schema_version: 1` (ABI-2); unstamped/future/malformed → quarantine, no conversion (Q8=B) — with the ONE carve-out (owner 4A): the boot latch migration re-stamps unstamped rows still at `cancel_requested` in place (same status, one typed `task_result_cancel_latch_admitted` event) so a wedged task still reaches its `cancelled` terminal | UNBOUNDED — one file per task forever, no GC — RATIFIED for 7.0 (CPL4-C19, owner batch №8 5A: lifecycle authority stays eternal deliberately; any future prune needs a fresh owner decision) | lifecycle authority lost; drive prunes degrade to age-only; strict authority reads break |
| `task_results/<id>.json` | `ouroboros/task_results.py` (locked merge) | `_schema_version: 1` (ABI-2); unstamped/future/malformed → quarantine, no conversion — with one carve-out: the boot latch migration re-stamps unstamped rows still at `cancel_requested` in place (same status, one typed `task_result_cancel_latch_admitted` event) so a wedged task still reaches its `cancelled` terminal | UNBOUNDED — one file per task forever, no GC; lifecycle authority is retained deliberately | lifecycle authority lost; drive prunes degrade to age-only; strict authority reads break |
| `task_results/quarantine/` | `ouroboros/task_result_schema.py` (same-dir rename) | quarantined bytes unchanged | NEVER GC'd (pinned); recovery is manual owner re-stamp | quarantined evidence lost |
| `task_results/artifacts/<id>/**` (+`verification_receipts.jsonl`, `.directory.*.tmp`, `.directory.*.json.tmp`), `task_results/artifact_versions/` | `ouroboros/artifacts.py`, `headless.py`, `outcome_receipt_store.py` | artifact and complete directory manifests `schema_version: 1`; scratch manifest 2 | artifact versions bounded (5 per name); artifacts live with their result; directory capture stages ZIP and manifest beside the result, removing owned temporaries on caught failures | deliverable bytes lost; results keep dangling manifests |
| `task_drives/<id>/**` (+`tmp_scripts/`) | `ouroboros/headless.py`, `tools/tool_context.py`, `tools/shell.py` | child stamps as above | GC-retention prune at startup (terminal + age, default 7 d); the `data/tmp_scripts` fallback's hard-kill orphans are in `sweep_stale_temp_files` scope (CPL4-C20, startup-only when no script can be live) | scratch lost; canonical artifacts survive |
| `task_drives/<id>/**` (+`tmp_scripts/`) | `ouroboros/headless.py`, `tools/tool_context.py`, `tools/shell.py` | child stamps as above | GC-retention prune at startup (terminal + age, default 7 d); the `data/tmp_scripts` fallback's hard-kill orphans are in `sweep_stale_temp_files` scope (startup-only when no script can be live) | scratch lost; canonical artifacts survive |
| `task_trees/<root>/blackboard.jsonl` | `ouroboros/task_tree_ledger.py` | rows unversioned; snapshot digest `schema_version: 1` | GC-retention prune at startup (root terminal + age) | swarm coordination facts lost for live trees |
| `state/subagent_worktrees.json` (registry; checkouts live OUTSIDE data root) | `ouroboros/subagent_worktrees.py` | none — malformed → typed refusal (absent = empty is the designed asymmetry) — accepted; file_baseline retains copied binary input identities | prune_orphans (age + missing checkout; skips delegated_exec) + custody-cross-checked snapshot prune (fail-closed on unreadable custody) | permanent leak of checkouts + pinned refs (nothing else names them) |
| `task_results/artifacts/<task>/delegated_runs/<run>/engine-files-manifest.json`, `task_results/artifacts/<task>/delegated_runs/<run>/*-*` | `ouroboros/delegate_directory.py`, existing streamed task-artifact owner | exact engine manifest SHA and per-file before/after digests | canonical result artifacts retained; engine keeps unapplied copy results under its existing disposition/retention owner | loss of complete binary results, baseline references and evidence after execution copy cleanup |
| `uploads/**` (+`screenshots/`, `views/`, atomic-copy `.tmp` files) | `ouroboros/gateway/files.py` through `artifacts.copy_artifact_file` for chat uploads; `tools/browser.py`, `tools/vision.py`, `server_owner_routing.py` | raw owner bytes; chat ingestion measures size and SHA256 while copying | owner attachments in the `uploads/` root: NO retention, owner-explicit delete only; chat upload no longer applies the former 50 MiB cap, while Files-browser and downstream transport limits retain their own contracts; agent-generated `screenshots/`/`views/` age out past GC retention at startup (CPL4-C21, owner 6A) | chat attachments dangle (readers skip missing); staged task copies survive |
| `uploads/**` (+`screenshots/`, `views/`, atomic-copy `.tmp` files) | `ouroboros/gateway/files.py` through `artifacts.copy_artifact_file` for chat uploads; `tools/browser.py`, `tools/vision.py`, `server_owner_routing.py` | raw owner bytes; chat ingestion measures size and SHA256 while copying | owner attachments in the `uploads/` root: NO retention, owner-explicit delete only; chat upload no longer applies the former 50 MiB cap, while Files-browser and downstream transport limits retain their own contracts; agent-generated `screenshots/`/`views/` age out past GC retention at startup | chat attachments dangle (readers skip missing); staged task copies survive |
| `services/<task>/*.log` | `ouroboros/workspace_executor.py`, `tools/services.py` | none — raw text; archived content becomes observability blob with events.jsonl receipt | GC-retention prune at startup (archive-then-unlink); per-task terminal archive; oversize logs retained live | live tails lost; archived blobs survive |
| `projects/<id>/**` (knowledge, journal, workpad, reflections, `.project.json` marker) | `ouroboros/project_facts.py`, `projects_registry.py` | marker unversioned; registry stamped (§2) | NEVER age-pruned (owner curates; delete = durable tombstone) | per-project memory lost; registry row survives, room reappears empty |
| `projects/<id>/knowledge_history.jsonl`, `projects/<id>/knowledge_journal.jsonl` | `ouroboros/knowledge.py` through the shared knowledge write lock | append rows retain source topic, revision and operation facts | follows the owning project shelf; retained with the project until explicit deletion | history/provenance lost while authored project notes remain |
| `archive/**` (rotated segments, `rescue/`, `usage_import/`, `managed_repo/`) | rotation + `supervisor/git_ops_rescue.py`, `usage_legacy_import.py`, `launcher_bootstrap.py` | segments inherit source shape; usage_import carries sha256 sidecar | UNBOUNDED BY DESIGN — durable history, never GC'd (P1) — accepted | memory horizon truncated; rescue copies of uncommitted work destroyed |
| `archive/usage_ledger/segment_*.jsonl` | `ouroboros/usage_compaction.py` (CPL4-C6: exact pre-compaction ledger bytes, written + fsync'd BEFORE the live swap) | each segment is a whole valid ledger generation; hash-pinned by the live `usage_baseline` header (`source_sha256`), chained recursively through each segment's own leading header | UNBOUNDED BY DESIGN — the folded monetary history, never GC'd (P1); read via `archived_attempt_ids` (tamper-evident, per-attempt joins for the CPL-5 reverse sweep) | folded per-attempt monetary history unrecoverable; live aggregates (baseline block) survive, but seal/attempt joins for folded ids break — the mirror of the ledger row: deleting the ARCHIVE alone under a stamped ledger is a typed chain break on every history question, deleting the LEDGER alone beside a surviving archive a permanent `generation newer` verdict; reset both together |
| `observability/{calls,blobs,salvaged}/**` | `ouroboros/observability.py` (private 0700/0600, CAS gzip); CPL-5 twins beside the call manifests: `model_send_seal` block + write-once `<attempt>.model_send_violation.json` typed facts (`ouroboros/model_send_seal.py`) | call manifests `schema_version: 1` + custody/redaction honesty markers; blob refs sha-verified on read; `model_send_seal.seal_version: 1` with the `canonical_json_v1` basis string | preserved indefinitely BY CONTRACT (the startup census counts, never deletes); the inert `OUROBOROS_OBSERVABILITY_RETENTION_DAYS` knob is RETIRED (CPL4-C22, owner 7A; key in `RETIRED_SETTING_KEYS`) | every recorded `result_ref`/`manifest_ref` dangles (strict readers raise); salvaged outputs unrecoverable; a lost seal on a seam-dispatched attempt surfaces as a typed `unlogged_attempt` fact at the next startup sweep |
| `archive/usage_ledger/segment_*.jsonl` | `ouroboros/usage_compaction.py` (exact pre-compaction ledger bytes, written + fsync'd BEFORE the live swap) | each segment is a whole valid ledger generation; hash-pinned by the live `usage_baseline` header (`source_sha256`), chained recursively through each segment's own leading header | UNBOUNDED BY DESIGN — the folded monetary history, never GC'd (P1); read via `archived_attempt_ids` (tamper-evident, per-attempt joins for the model-send reverse sweep) | folded per-attempt monetary history unrecoverable; live aggregates (baseline block) survive, but seal/attempt joins for folded ids break — the mirror of the ledger row: deleting the ARCHIVE alone under a stamped ledger is a typed chain break on every history question, deleting the LEDGER alone raises `generation newer` for surviving newer, non-prefix segments; once fresh compactions reach those generations, old unreferenced segments are skipped and their attempt IDs are absent ([history readers](USAGE_COMPACTION.md#10-history-readers-model-send-reconciliation-and-audits)); reset both together |
| `observability/{calls,blobs,salvaged}/**` | `ouroboros/observability.py` (private 0700/0600, CAS gzip); model-send records beside the call manifests: `model_send_seal` block + write-once `<attempt>.model_send_violation.json` typed facts (`ouroboros/model_send_seal.py`) | call manifests `schema_version: 1` + custody/redaction honesty markers; blob refs sha-verified on read; `model_send_seal.seal_version: 1` with the `canonical_json_v1` basis string | preserved indefinitely BY CONTRACT (the startup census counts, never deletes); the inert `OUROBOROS_OBSERVABILITY_RETENTION_DAYS` knob is RETIRED (key in `RETIRED_SETTING_KEYS`) | every recorded `result_ref`/`manifest_ref` dangles (strict readers raise); salvaged outputs unrecoverable; a lost seal on a seam-dispatched attempt surfaces as a typed `unlogged_attempt` fact at the next startup sweep |
| `claudexor/**` | EXTERNAL writer — the claudexord daemon (Ouroboros only mkdirs, appends `daemon.log`, writes `ouroboros-owned.json` marker) | marker unversioned | daemon-owned; grows unbounded under our root — disclosed external plane | owner harness logins/profiles lost (fresh device-auth required) |
| `playwright-browsers/` | `ouroboros/tools/browser.py` (vendor install) | none — vendor tree | no GC — accepted (vendor cache) | re-downloaded on next browser use |

View file

@ -1,23 +1,23 @@
# Design note — usage-ledger compaction (CPL4-C6, monetary authority)
# Usage-ledger compaction and monetary authority
Owner sanction: batch №8 item 1A (2026-09-01) — "seq-preserving compaction
snapshot (settled rows folded into a stamped baseline row + archive of the raw
segment)", excised from the CPL-4 persistence train into its own reviewed lane
because the ledger is the monetary authority.
Terminal usage history is compacted into a stamped baseline block while the
exact original ledger bytes remain in an append-only archive. The ledger is
the monetary authority, so compaction preserves exact money, attribution,
in-flight attempts and historical joins.
## 1. Problem
`state/usage_attempts.jsonl` is the append-only monetary authority
(`ouroboros/usage_ledger.py`). Every reservation re-reads it under the
cross-process monetary lock; a ~20 MB ledger costs ~0.5 s per full re-read
under that lock (the 2026-07-23 lock-timeout incident;
`USAGE_LEDGER_WARN_BYTES` in `ouroboros/context_budget.py` warns at exactly
that point). The in-process warm caches (#129) bound the *steady-state* cost,
under that lock. `USAGE_LEDGER_WARN_BYTES` in
`ouroboros/context_budget.py` warns at that measured degradation point.
The in-process warm caches bound the *steady-state* cost,
but every cold read (process start, refold on any doubt) still replays the
whole file, and the file grows without bound: each physical attempt appends a
2–4 row lifecycle chain that stays forever after it is terminal.
whole file. Without compaction the live file grows without bound: each
physical attempt appends a 2–4 row lifecycle chain that remains after it is terminal.
## 2. Sanctioned shape
## 2. Compacted representation
Fold the terminal history into a **stamped baseline block** at the head of the
ledger and move the raw pre-compaction bytes, verbatim, into an append-only
@ -32,7 +32,7 @@ validated aggregate plus every row that is still live.
| `kind="attempt"`, final state `settled` / `unresolved` / `released`, and **no review attribution** (`review_skill`/`review_wave_id`/`review_slot_id` all empty) | folded (their whole seq chain) | terminal, id never re-asserted by any writer (`attempt_id` is a one-shot uuid4 minted at reserve time); aggregation-complete under §5 |
| `kind="attempt"`, final state `reserved` / `dispatched` (in-flight) | **retained verbatim** | INVARIANT: in-flight/unsettled rows are never folded — their terminal transition still has to join them by `attempt_id` in the live replay |
| `usage_baseline` / `usage_baseline_group` from a previous compaction | re-folded (header replaced, groups merged by key, exact-decimal sums added) | baselines must not accumulate per epoch |
| `kind="subscription_session"`, `"external_unmetered"` | **retained** | their `attempt_id` is deterministically re-derived from a stable external id and re-asserted on replay: `_append_single_settled_row` dedups and conflict-checks against the LIVE replay. Folding them would turn an idempotent replay into a silent double charge. Disclosed residual: these rows keep growing (slowly — one row per delegated run / external dispatch); a future lane may fold them behind an archived-identity membership check. |
| `kind="subscription_session"`, `"external_unmetered"` | **retained** | their `attempt_id` is deterministically re-derived from a stable external id and re-asserted on replay: `_append_single_settled_row` dedups and conflict-checks against the LIVE replay. Folding them would turn an idempotent replay into a silent double charge. Disclosed residual: these rows keep growing (slowly — one row per delegated run / external dispatch). |
| `kind="legacy_*"` | **retained** | same idempotency argument: `ensure_legacy_imported` dedups candidate rows against live `attempt_id`s if the completion watermark is ever lost mid-history. Bounded one-time set. |
| attempts with review attribution | **retained** | `skill_review_usage` projects historical waves per-attempt (`attempt_ids`, `attempts` lists) for durable review receipts; folding would erase that projection. Disclosed residual (skill-review waves only; ordinary task/review traffic carries no `review_*` attribution). |
| unknown future kinds | **retained** | fail-safe default: fold only what this design proves aggregation-complete |
@ -83,7 +83,7 @@ carrying: the key fields verbatim; `folded_attempt_count` (int ≥ 1);
`root_limit_usd` = min over the group's known values (else absent);
`baseline_id` joining the header; empty `review_*` attribution.
Why per-group rows and not the literally single row of the sanction sketch:
Why per-group rows rather than a single global aggregate:
budget enforcement is **per-root** (`reserve_attempt` filters finals by
`root_task_id`; `usage_projection` takes `min` of row `root_limit_usd`), and
`usage_breakdown` groups by model/provider/category/task/root. A single global
@ -148,7 +148,7 @@ so instead the compacted file starts a fresh dense epoch:
- the header records `source_first_seq`/`source_last_seq`, and the archive
segment holds every original row with its original `seq` untouched.
Monotonicity and density are preserved (the lane invariant); the original seq
Monotonicity and density are preserved; the original seq
values are never lost (archive + `pre_compaction_seq`). Nothing durable
references ledger rows by `seq` (cross-references are `attempt_id`s); resume
fingerprints are invalidated structurally by the inode change (§8).
@ -187,7 +187,7 @@ thing that decides what a well-formed row IS — checks it:
## 7. Aggregation contract (`_usage_rows`)
`_summary` and `_physical_call_count`/`_breakdown_bucket` become
`_summary` and `_physical_call_count`/`_breakdown_bucket` are
baseline-aware in the narrowest way:
- `usage_baseline` header: skipped (no money, no counts);
@ -195,7 +195,7 @@ baseline-aware in the narrowest way:
`unknown_unmetered`, `non_final_rows`, physical calls, `prompt_cache_ttls`)
uses `weight = folded_attempt_count`; every **sum** adds the row's carried
aggregate once. For all existing kinds `weight == 1` and the code path is
byte-equivalent to today's.
unchanged.
The group key (§4) makes each group homogeneous in every branch predicate
`_summary` evaluates per row (`cost is None`, `cost_final`,
@ -221,14 +221,12 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
tier structurally unreachable there: a Windows volume without byte-range
locks would fail EVERY monetary append closed instead of degrading to it.
`ENOLCK` ("no locks available" — a filesystem without a lock daemon, or an
exhausted kernel lock table) is the third answer (round 5.4 close-out; round
5.4 proper made it fail EVERY caller closed, which the lenses showed to be
product-wide: the same primitive locks state singletons, task results and
custody, so a lockd-less NFS `state/` would have stopped every locked write
and every model dispatch — a capability the name protocol had always
provided there): it selects the **name tier** like a filesystem that cannot,
but the probe RECORDS the errno beside the verdict, and a caller may refuse
that tier by errno (`acquire_exclusive_file_lock(refuse_name_tier_errnos=…)`).
exhausted kernel lock table) selects the **name tier** too. Making this
failure close every caller would stop locked state writes, task-result and
custody updates, and model dispatch on a lockd-less filesystem, although
those non-monetary consumers can use the name protocol. The probe records
the errno beside its verdict, and each caller can refuse that tier by errno
(`acquire_exclusive_file_lock(refuse_name_tier_errnos=…)`).
Only the monetary lock does: `usage_ledger._named_lock` names `ENOLCK`, so on
such an install every monetary write refuses typed (`UsageAccountingError` —
no lock, no append, no pass; money never runs the name protocol where locks
@ -239,21 +237,14 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
probes that could disagree — except a directory where the scratch probe
cannot be created, which answers enforced for that call and is probed again
next time (not cached).
**Windows takes the ENFORCED tier in 7.0, on a byte range beyond the stamp.**
Its first shape could not ship. The 3-OS matrix on `bf8b6549`
(run 33654743857) locked the WHOLE file, and a Windows byte-range lock is
MANDATORY: a contender that opened the held lock file to read the owner's
stamp was refused the READ, could never judge the hold and waited out its
timeout — eight concurrent monetary writers all answered «lock unavailable»,
`update_json_locked` timed out, a concurrent chat append was lost.
`kernel_file_locks_enforced` was made to answer False there (abea91ec), which
moved the defect rather than closing it: this design's name tier probes
identity and stamp on every poll (the pre-C6 protocol only `stat`ed), and on
Windows that contender handle made the owner's release unlink fail with a
sharing violation (no FILE_SHARE_DELETE), orphaning the lock with a live pid
until `_unlink_lock_path` retried the transient refusal for a bounded window
(run 33663258606). The owner then made the working tier a release condition
(batch №13 item 1, 2026-09-02), and it is back: the hold is ONE byte at
**Windows takes the enforced tier on a byte range beyond the stamp.**
Windows byte-range locks are mandatory: locking the whole file prevents a
contender from reading the owner's stamp, so it cannot judge the hold and
times out. Falling back to the name tier would expose another constraint:
the contender's open handle can prevent the owner's release unlink through
a sharing violation (no `FILE_SHARE_DELETE`). `_unlink_lock_path` retries
that transient refusal for a bounded window. The enforced-tier hold is one
byte at
`platform_layer._WIN32_LOCK_OFFSET` (`0x7FFFFFFF00000000`, length 1 — the
common Win32 idiom; a lock beyond end-of-file is legal there and no lock
file's one-line stamp can reach that far), so the bytes a contender reads,
@ -277,9 +268,8 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
retries a contender's transient refusal). What Windows still does not have on
this tier is named elsewhere in this section and unchanged: no directory
fsync, and no old-inode witness across `os.replace`, so a charge landed in
the swap's last syscall is lost silently there. The Windows-EXECUTED proof is
the CI matrix, which is the only Windows host this work has: the Linux-side
pins (the range constant and its two wrappers, an emulated LockFileEx that
the swap's last syscall is lost silently there. Windows execution is checked
by the CI matrix; host-side pins (the range constant and its two wrappers, an emulated LockFileEx that
refuses the same range, the delete-semantics simulator) stand in for the
mechanism, never for the platform.
*Enforced tier* (POSIX `fcntl.flock`, Windows `LockFileEx` —
@ -339,15 +329,11 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
inode. It fails closed, and the file we stamped with our LIVE pid is removed
with it when its bytes are still exactly the ones we wrote — left behind, no
owner-aware reclaimer could ever evict it and the lock wedges for good.
**Residual, disclosed (mechanism corrected in round 5.4):** the owner-aware
rule asks `pid_is_alive(owner_pid)`, and a RECYCLED pid — one a live
process now owns — reads as alive whoever owns it: `kill(0)` succeeds for
a same-uid impostor and answers EPERM for another user's, which round 5.4
made "alive" too (it read as "dead" before, so another user's recycle was
reclaimed through the age path — the probe flock guarding it on the
enforced tier — while only a same-uid recycle wedged; this note named the
opposite mechanism). So a lock whose owner died and whose pid was reused
is never reclaimed by age while the impostor lives (`pid_is_alive` is the
**Residual, disclosed:** the owner-aware rule asks
`pid_is_alive(owner_pid)`, and a recycled PID reads as alive whoever now
owns it: `kill(0)` succeeds for the same user and answers `EPERM` for
another user's process; both mean alive. A lock whose owner died and whose
PID was reused is never reclaimed by age while the impostor lives (`pid_is_alive` is the
ONE liveness primitive, shared by every consumer — custody settlements,
claim reclaims, staging reaps — so a pid recycled onto another user's
process reads alive everywhere and those defer while the impostor lives,
@ -368,11 +354,9 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
checkpoint, immediately before every rename attempt and once more AFTER
the in-swap snapshot look, so the irreducible residual is the interval
between that last proof and the rename syscall: a charge the robber lands
inside it is ERASED by the swap. Until round 5.4 this note claimed the
post-swap re-read or the next read's seq quarantine would surface it;
neither can — the re-read compares the NEW inode against the candidate and
the archive segment is the pre-row snapshot — so the loss was silent and
the pass returned a success receipt. Now, on POSIX, the swap holds the OLD
inside it is erased by the swap. A post-swap re-read cannot detect that
loss: it compares the new inode against the candidate, while the archive
contains the pre-row snapshot. On POSIX the swap therefore holds the old
inode open across the rename (the only witness left) and reads whatever
landed beyond the proven snapshot's length AFTER the fact: those bytes go
to `state/usage_attempts.quarantine.jsonl` (`raw_base64`, the shape a torn
@ -492,11 +476,13 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
mechanism, exactly like the rotation-bounded log warns: it now fires only if
compaction is broken or the unfoldable residue itself reaches 20 MB.
## 10. History readers: CPL-5 reconcile sweep, audits
## 10. History readers: model-send reconciliation and audits
CPL-5 (`DESIGN_MODEL_VISIBLE_LOGGED.md` §3.3, implementation landed on the integration tip as `ouroboros/model_send_seal.py`, wired and swept) reconciles `model_send` seals against "an attempt row in the
usage-accounting replay". After compaction a folded attempt is no longer in
the live replay, so this lane ships the join surface the sweep must use:
The reverse reconciliation in
[Model-send observability](MODEL_SEND_OBSERVABILITY.md#33-reverse-direction-audit-model_send-only)
(`ouroboros/model_send_seal.py`) joins model-send seals to usage-accounting
attempts. A folded attempt is absent from the live replay, so the sweep uses
the archive-aware join:
- `usage_compaction.archived_attempt_ids(root)` — the `attempt_id` set of
every archived segment, walked through the tamper-evident header chain
@ -560,7 +546,7 @@ the live replay, so this lane ships the join surface the sweep must use:
open is the step a directory refuses and a writer-less FIFO blocks on. A
first row that reads but does not parse is a torn segment from a crashed
write: no evidence of any generation, left to the walk. Every path
inspection the reader makes is typed the same way (round 5.4): `pathlib`
inspection the reader makes is typed the same way: `pathlib`
re-raises every `OSError` but `ENOENT`/`ENOTDIR`/`EBADF`/`ELOOP` from
`is_symlink`/`is_dir`, so the symlink bounds on both archive levels and
on the named segment (an `archive/usage_ledger` readable but not
@ -639,7 +625,7 @@ the live replay, so this lane ships the join surface the sweep must use:
stamp-less file too, and the wrong answer there turns a folded attempt into
a reported orphan seal.
Contract for the CPL-5 lane (recorded here and in the review packet): the
The model-send reconciliation contract is that the
reverse sweep's "no attempt row" verdict (`orphan_seal`) must consult this
union, not the live replay alone; an unreadable/mismatched segment is the
sweep's existing UNKNOWN → skip-pass case (fail-soft, the API raises typed
@ -651,11 +637,11 @@ existing live-replay dedup keeps working under a lost watermark.
## 11. Module placement
New leaf `ouroboros/usage_compaction.py` (domain D16): fold policy +
`ouroboros/usage_compaction.py` (domain D16) owns fold policy +
archive/verify/swap + history readers. It imports FROM `usage_ledger`
(substrate) and `_usage_rows` (aggregation leaf); `usage_accounting` calls
INTO it from `reserve_attempt`. The substrate stays policy-free (it learns
only the new row kinds' validation), the one-way seam
only the baseline row kinds' validation), the one-way seam
`usage_ledger ← usage_accounting` is unchanged, and the compactor — which must
know the aggregation semantics — lives beside the aggregation, not inside the
byte authority.
@ -671,7 +657,7 @@ tests/fixtures_usage_compaction.py)
limits) and of `usage_breakdown` (all axes) renders equal dicts: state
counts and folded weights, physical calls, token sums, finality,
subscription sessions, per-root limits, every axis shape. The float dollars
those renders carry are deliberately NOT part of that equality (R2-37).
those renders carry are deliberately NOT part of that equality.
Readers round money at six places, so one history summed per row and summed
per group can land on either side of that boundary. The regression fixture
in `tests/test_usage_compaction_fingerprint.py` observes a 1e-6 USD shift,
@ -696,7 +682,7 @@ tests/fixtures_usage_compaction.py)
and the ledger's directory after it.
4. **Budget limits are preserved**: root/global enforcement thresholds are
unchanged across compaction.
5. **CPL-5 join survives**: every pre-compaction `attempt_id` remains
5. **Model-send join survives**: every pre-compaction `attempt_id` remains
resolvable through live ∪ archive, across chained compactions; a tampered
segment, a re-hashed but structurally broken segment, a deleted segment
behind a warm cache, a same-size rewrite once the cache window closes, an
@ -777,11 +763,3 @@ tests/fixtures_usage_compaction.py)
than the live file's own (bar an uncommitted orphan of it, proven by
still being a prefix of that file), whether or not the live file carries
a stamp.
## 13. Explicitly out of scope
- Folding subscription/external/legacy/review-attributed rows (disclosed
residuals, §3).
- Any GC of archive segments or the quarantine file (append-only, never).
- Changes to the CPL-5 sweep beyond the archive-aware membership join in §10.
- Changing `USAGE_LEDGER_WARN_BYTES` or the lock timeouts.

View file

@ -130,7 +130,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
├── _outcome_receipts.py ← Receipt parsing and the ONE canonical receipt identity (`receipt_canonical_identity` → `ReceiptIdentity`; invariant in §10): three independent components — `criterion_id`; structurally canonical `check` text PAIRED with its `check_rendering` stamp (quoted shell punctuation is data, not syntax, and receipts from different renderings are never the same verification — the stored string alone cannot say which renderer wrote it); and the raw-sorted `canonical_path_set` (whitespace untouched — a leading space is a legal filename byte); `ReceiptIdentity.key` selects ONE typed (kind, value) and sameness is that key's equality, never a match across kinds — the parts are disclosures, never the comparison; the per-kind normalization answer lives in the closed `IDENTITY_KINDS`/`KIND_NORMALIZES_COMMAND_TEXT` table, so a fourth kind must state its own answer in its own row rather than inherit a default; the outstanding sets `unreconciled_failed`/`unreconciled_masked` scan every candidate against ALL later reconcilers and collapse repeated failures of one check onto the freshest receipt; the shared disclosed projections (`receipt_identity_projection`, `disclosed_list_projection`) make every bound explicit — exact omitted counts plus a hash over the injective serialization, string bounding via the SSOT `utils.truncate_review_artifact`, never a hand-rolled slice; `verification_receipt_ledger_row` splats that projection, so a new receipt key is dropped unless added there
├── _outcome_tool_errors.py ← Leaf SSOT for tool-trace status vocabularies and execution-axis classification; outcomes.py re-exports
├── code_intelligence.py ← Internal code inventory: derived-only file facts, hashes, polyglot symbol/import/call extraction via tree-sitter with Python on stdlib `ast`, a visible `structural_unavailable` fallback when a grammar is missing, and an incremental JSON cache (no raw source)
├── code_intelligence_architecture.py ← Architecture facts over the pinned domain/contract/persistence carriers: `owner_of`, the domain quotient, and the facade inventory (`docs/v7next/FACADE_INVENTORY.md`)
├── code_intelligence_architecture.py ← Architecture facts over the pinned domain/contract/persistence carriers: `owner_of`, the domain quotient, and the facade inventory (`docs/inventories/FACADE_INVENTORY.md`)
├── code_search_rg.py ← Optional ripgrep-backed search for search_code; every match is post-filtered through the protected/secret gates
├── pricing.py ← Exact-route best-effort provider-catalog lookup with nullable estimates; no static model tariffs (they go stale) and not the monetary ledger
├── usage_accounting.py ← Append-only physical-model-attempt monetary authority: reserved→dispatched→settled|unresolved (or reserved→released), short cross-process check+append+fsync lock, conservative global/root admission, validated replay/torn-tail quarantine, compatibility projections, resumable legacy import; application candidates carry exact raw/context identities + a pre-dispatch manifest on the same attempt id
@ -651,7 +651,7 @@ Bundled resources use the §1 CLI/headless lookup order rather than assuming the
└── ouroboros.pid ← launcher PID lock; platform lock auto-released on crash
```
Every entry of this tree is probed against the tree and the runtime sources by the generated `docs/v7next/DATA_LAYOUT_INVENTORY.md`, so a durable file renamed in code while its row here survives is red, not silent.
Every entry of this tree is probed against the tree and the runtime sources by the generated `docs/inventories/DATA_LAYOUT_INVENTORY.md`, so a durable file renamed in code while its row here survives is red, not silent.
---

View file

@ -653,7 +653,7 @@ Rationale: diff reviewers catch line-level mistakes; the scope reviewer catches
Structural smoke gates are a deterministic BIBLE P3 codebase-size component. `ouroboros/review.py::iter_gated_modules` is the one source inventory for smoke, `codebase_health`, census, and the UTF-8 byte gate (Python everywhere plus first-party `web/**/*.js`, vendored/minified excluded). `ouroboros/size_ratchet_manifest.py` is a generated, data-only debt register consumed through AST literals: exact module debt above 1600 lines, exact function debt above 300 lines, the 1001–1500 band with rationale authority, and exact byte debt above 200,000 UTF-8 bytes. `validate_size_ratchet` proves the live and staged manifests exact against their trees and shrink-only against the merge-aware committed authority — no first-parent history replay: the previous manifest resolves merge-aware from `HEAD` or any of its parents, and a checkout with no committed manifest anywhere bootstraps from its own tree, so a fork whose local line predates the manifest is never condemned by inherited topology. Official-repository CI runs the blocking `size_ratchet` pytest lane while every local surface reports the same findings as warnings; two disclosed residuals — pairwise validation covers only the base→HEAD interval, and the official block presupposes branch protection. Within a validated pair, debt can shrink but cannot be swapped, re-entered, grow on the byte axis, or survive stale. `MAX_TOTAL_FUNCTIONS` (`ouroboros/review.py`) remains the coarse runtime ceiling. A deterministic hot-store growth invariant sits beside these gates: `agent_startup_checks.py::hot_store_growth_notes` (surfaced by `context_health.py::build_health_invariants` and once per worker boot) stats eight hot stores plus the `archive/chat_*.jsonl` aggregate — `state/usage_attempts.jsonl`, `logs/events.jsonl`, `logs/tools.jsonl`, `logs/supervisor.jsonl`, `logs/task_reflections.jsonl`, `logs/progress.jsonl`, `state/scheduled_tasks.json`, and `state/skill_review_root_tasks.jsonl` — against justified byte thresholds in `ouroboros/context_budget.py` and emits a WARNING with a remediation pointer.
Three gen/verify inventories ride the same discipline as the size manifest (generator `scripts/regenerate_inventories.py`, verify `tests/test_generated_inventories.py`, staleness = red): the frozen-contracts inventory (`docs/v7next/FROZEN_CONTRACTS_INVENTORY.md`, a machine extraction of §11.1 with every owner/anchor path resolved against the tree and the `ouroboros/contracts/` package-coverage gap pinned), the data-layout inventory (`docs/v7next/DATA_LAYOUT_INVENTORY.md`, every entry of the §1 "Data layout" tree probed as a tracked repo path or a runtime-source literal, so a durable file renamed in code while its tree row survives turns red), and the facade inventory (`docs/v7next/FACADE_INVENTORY.md`, the AST-derived `noqa: F401` re-export surface with per-leaf domains from `ouroboros/domains.toml`).
Three gen/verify inventories ride the same discipline as the size manifest (generator `scripts/regenerate_inventories.py`, verify `tests/test_generated_inventories.py`, staleness = red): the frozen-contracts inventory (`docs/inventories/FROZEN_CONTRACTS_INVENTORY.md`, a machine extraction of §11.1 with every owner/anchor path resolved against the tree and the `ouroboros/contracts/` package-coverage gap pinned), the data-layout inventory (`docs/inventories/DATA_LAYOUT_INVENTORY.md`, every entry of the §1 "Data layout" tree probed as a tracked repo path or a runtime-source literal, so a durable file renamed in code while its tree row survives turns red), and the facade inventory (`docs/inventories/FACADE_INVENTORY.md`, the AST-derived `noqa: F401` re-export surface with per-leaf domains from `ouroboros/domains.toml`).
The shared hard prompt-size SSOT is `REVIEW_PROMPT_TOKEN_BUDGET = 920_000` in `review_helpers.py`. `review_context_atlas.py` targets 850K estimated prompt tokens for scope review and deep self-review, and the final 920K gate stays in each caller as the hard stop (plan review builds no Atlas: its packet is sized per slot by `plan_review_runtime.plan_slot_fit`). Scope review additionally reserves output headroom inside the reviewer's window (`_SCOPE_MAX_TOKENS` 100K plus a tokenizer margin), because provider accounting can exceed the local chars/4 estimator on atlas-heavy prompts and a physically rejected oversize prompt has no authoritative verdict; `scope_review.py` gates assembled input on `_SCOPE_INPUT_TOKEN_LIMIT = min(920K, window − _SCOPE_MAX_TOKENS − margin)`.

View file

@ -84,11 +84,10 @@ Add the field to the active frozen owner — `ouroboros/contracts/` for the pack
install that emits a machine-readable scope document naming every incompatibility it finds across the five
frozen classes (gateway alias, retired setting, comma list, plugin API, schema stamp), snapping the
retired-key lists at execution time instead of hardcoding them.
- *Deliberately NOT in this window:* the handler ABI — tool handlers returning `ToolResult` instead of `str` —
is backlog, so handler signatures are unchanged. The external-executor family is the one INTERIOR
exception and does not open that window: its producers, decorators and host consumers pass a native
`ToolResult`, while its four REGISTERED entries still publish a `str` projection through
`tool_result._publish_tool_result`. No handler signature and no other family changed.
- *Handler ABI.* Registered tool handlers return `str`. The external-executor family uses a native
`ToolResult` internally across its producers, decorators and host consumers, while its four
registered entries publish a `str` projection through `tool_result._publish_tool_result`.
This preserves the public handler signature.
- `5.25.0-rc.4` retired the native skill upgrade migration banner API (`GET /api/migrations`,
`POST /api/migrations/{key}/dismiss`, and `MigrationsResponse`). The migration note is the release row itself:
dismissed banner state in `data/state/migrations.json` is intentionally ignored by current runtimes.

View file

@ -1,944 +0,0 @@
# C6 review packet — monetary usage-ledger compaction (CPL4-C6, owner 1A)
Lane: `v7next_c6`, base `74a03082`. Owner sanction: batch №8 item 1A
(2026-09-01) — compaction of `state/usage_attempts.jsonl` in its own reviewed
lane (monetary authority). Design note ratified before code:
`docs/v7next/DESIGN_USAGE_COMPACTION.md` (commit `a1063124`); implementation
+ pins in the follow-up commit; this packet + ledger section close the lane.
Round 2 (§6) is the fix-round for the external adversarial wave against
`e2801c52`. Round 3 (§7) is the fix-round for the second wave, against
`830aa35a`: five findings were re-opened as still-open (1, 2, 3, 4, 6) and
all five are fixed here; four (5, 7, 8, 9) the wave confirmed closed.
Round 4 (§8) is the fix-round for the third wave, against `d7b487ab`: the
lock's exclusion becomes kernel-enforced, ownership is proven adjacent to
every irreversible decision, the swap re-proves its snapshot inside the
atomic replace, and the archive symlink bound moves from check-then-use to
the open itself (dir-fd `O_NOFOLLOW`). Round 5 (§9) is the fix-round for the
fourth wave (gpt-5.6-sol, read-only), against `13af62c5`: the lock's tier
becomes an explicit capability predicate with a fail-closed enforced tier,
the swap re-proves ownership and snapshot before EVERY rename attempt, the
epoch anchor scans through the handle the chain walk held, the two surviving
mutations are pinned, and the doc absolutes are stated per tier. Round 5.2
(§9, second block) is the fix-round for the adversarial lenses over round 5,
against `2dd3e017`: the acquisition itself is identity-checked, the
name-tier refusal becomes a durable typed event, the swap proves ownership
on both sides of its last look, the anchor scan classifies non-regular
entries and cannot hang, the anchor-swap pin covers the open-through-fd
half, LockFileEx refusals classify by their Win32 error, and the remaining
absolutes are bounded per tier.
## 1. Diff map (what to read, in review order)
| surface | change | why |
|---|---|---|
| `docs/v7next/DESIGN_USAGE_COMPACTION.md` | NEW — the ratified contract | invariants, fold scope, decimal rule, seq policy, crash order, trigger, CPL-5 join |
| `ouroboros/usage_compaction.py` | NEW leaf (D16, ~490 lines; round 5.2: `owned_and_intact` beats on both sides of its look, the typed durable `usage_ledger_compaction_refused` event, non-regular archive entries classified — `O_NONBLOCK` open, `S_ISREG` — and typed `_load_segment` failures; round 5.3: the heartbeat REQUIRED by both entry points, the orphan exemption a byte-prefix proof read from the classified descriptor, the anchor running on a stamp-less live file, the root handle typed, the name-tier mark following the landed row, the path-shape scan classifying before the open, the chain-union cache bounded — 1197 lines; round 5.4: the refusal mark following a True append only, every reader path inspection typed and the stamp-less check exact on ENOENT, the swap's old-inode witness quarantining an erased charge and raising instead of receipting, `_build_candidate`'s beat required — 1245 lines) | fold policy + prove-then-swap + archive + history readers; imports FROM `usage_ledger`/`_usage_rows`, called INTO by `usage_accounting` — the one-way substrate seam is unchanged |
| `ouroboros/usage_ledger.py` | `_validate_records` learns the two baseline kinds; round 5: `LOCK_REL` (lock-path SSOT) and the atomic writer routing its precondition through `replace_atomic` | head-only baseline block, exactly one header at seq 1, group rows joined by `baseline_id` + positive `folded_attempt_count`; a baseline row in an appended tail or after any non-baseline row = corrupt. Everything else (locking, append arithmetic, quarantine, resume fingerprints) is untouched |
| `ouroboros/_usage_rows.py` | `_summary` + `_physical_call_count` baseline-aware | header skipped; group rows: count axes × `folded_attempt_count`, sums added once. Weight-1 paths are byte-equivalent to the previous code (pure refactor for existing kinds) |
| `ouroboros/usage_accounting.py` | +5 lines in `reserve_attempt` | the opportunistic trigger under the already-held monetary lock; contained (never raises into the reservation) |
| `ouroboros/config.py` | `USAGE_LEDGER_COMPACT_BYTES` (8 MB), `USAGE_LEDGER_COMPACT_RETRY_GROWTH_BYTES` (1 MB) | trigger policy from config SSOT, no env knob |
| `ouroboros/agent_startup_checks.py`, `ouroboros/context_budget.py` | warn-text updates only; round 5.2: the tripwire names the name-tier refusal as a third cause and points at its event | the 20 MB WARN becomes the broken-compaction tripwire — and can tell the tiers apart |
| `ouroboros/domains.toml`, `docs/DOMAIN_MAP.md` | new module seated D16; graph regenerated via `check_domains.py --write` | manifest completeness gate |
| `docs/PERSISTENCE.md` | usage-ledger row rewritten (bounded by compaction); NEW `archive/usage_ledger/segment_*.jsonl` row | inventory truth; scan pin 123→124 in `tests/test_persistence_inventory.py` |
| `tests/test_gateway_abi3_removals.py` | one per-site allowlist row | `_build_candidate` writes the internal ledger-plane `cost_usd` key (same class as every existing ledger writer row there) |
| `tests/test_usage_compaction.py` | NEW pin suite (61 test items after round 5.3; the suite entered the 1001-1500 size band with a recorded rationale — it stood at 1512 lines for one commit, `79a1b9fb`, with the manifest stale, and the round-5.2 fold `6ad110e9` returned it inside the band without a new abstraction; 1492 after round 5.2 — and LEFT the band in round 5.3 at `e08a0392`: 1597 at the tip, in the ungated 1501-1600 zone, the manifest regenerated with it; the owner decision is stated in §9 "Round 5.3"; round 5.4: 66 items, back at 1597 after five new/extended pins — paid for by a `compacted` fixture folding twenty-one seed-then-compact preambles and by argument-list/data-literal reflows, no claim dropped) | see §3, §6, §7, §8 and §9 |
| `tests/test_lockfile_helpers.py` | +5 lock-ownership pins in round 3, +2 in round 4, +5 in round 5 (one Windows-only), +2 in round 5.2 (the lock-less creator; LockFileEx classification), +2 in round 5.3 (an unreadable own identity is never a hold; the design note's refusal sets) with the classification pin extended to the unsupported set, +3 in round 5.4 (a pid answering EPERM is alive and its aged lock is not reclaimed; ENOLCK keeps the enforced tier and the acquisition fails closed; two threads racing the first probe run one probe) with the probe pin's name-tier selector moved to EOPNOTSUPP and an ENOLCK clause, and the identity pin extended with the heartbeat's refresh clauses | the finding-1 fixes are platform primitives, so they are pinned where those primitives live |
| `ouroboros/platform_layer.py` | lock ownership: `_lock_identity`, inode-guarded stale eviction and release, ownership-reporting heartbeat; round 5: `kernel_file_locks_enforced` capability predicate (enforced vs name tier), fail-closed kernel refusal, LockFileEx error classification; round 5.2: the owner pid written BEFORE the kernel lock and a won lock returned only while the path still names it (an evicted creator re-contends), `_win32_lock_error` classifying ERROR_LOCK_VIOLATION alone as busy; round 5.3: a descriptor whose own identity cannot be read is never a hold (its live-pid stamp removed with it), `_WIN32_LOCK_ERRNOS` mapping ERROR_INVALID_FUNCTION / ERROR_NOT_SUPPORTED onto the unsupported set, both kill-tree sweeps on `force_kill_pid` (exactly 1500 lines); round 5.4: ENOLCK out of the unsupported set (fails closed), winerror 1 onto ENOSYS, the tier cache decided once under `_KERNEL_LOCK_TIER_LOCK`, `pid_is_alive` reading EPERM as alive with `pid_provably_gone` its one-line negation (1497 lines) | round 3, finding 1; round 5, finding 1; round 5.2, findings 1 and W; round 5.3, findings 1 and L5; round 5.4, R1 and R7 |
| `ouroboros/utils.py` | `replace_atomic(precondition=)`: the precondition is asked before EVERY attempt, the Windows sharing-violation retries included; returns False without replacing when refused | round 5, finding 2 |
| `ADOPTION_v7next.md` | CPL-4 row: C6 landed + verification hook | adoption gate |
| `docs/v7next/LEDGER_CORRECTIONS.md` | append-only C6 lane section | provenance |
## 2. Invariants to verify adversarially
1. **Money is decimal-exact.** Group sums are `Decimal`s of the exact JSON
literals, carried as exact-decimal strings (`_number` accepts strings at
every reader: validator, `_summary`, projections). Retained rows are
verified Decimal-identical across re-serialization; a non-round-trippable
foreign literal aborts the pass (never approximates).
2. **Prove-then-swap.** Commit happens only after the candidate bytes
(a) re-validate structurally and (b) render EQUAL dicts through the
production aggregation on every consumed surface (global summary, per-root
summaries + min `root_limit_usd`, breakdown buckets on all five axes) and
(c) match decimal money totals. Any inequality → abort → ledger
byte-identical. Compaction is an optimization; it can only decline.
3. **In-flight rows never fold** (reserved/dispatched finals keep their whole
chain, verbatim modulo seq) and their later transitions work unchanged.
4. **Idempotency-bearing kinds never fold** (subscription/external/legacy):
their replay dedup + conflict checks read the live replay only. This is
why their exclusion is structural, not an optimization choice.
5. **Crash-safety order**: archive segment written + fsync'd (file and every
directory entry the chain created; POSIX failure is fatal, Windows is a
disclosed no-op) BEFORE the atomic ledger swap, and the swap is refused if
the live file changed since the snapshot. Crash anywhere = valid ledger
(old or new generation); orphan segments harmless.
6. **seq policy**: dense-seq validation authority preserved by starting a
fresh epoch; original seqs survive in the archive and as
`pre_compaction_seq` on retained rows. Substrate append/resume arithmetic
(`len(records)`-based) is deliberately UNCHANGED — check this holds.
7. **Concurrency**: everything under the existing monetary lock (`_locked`),
which is owner-aware and heartbeaten so a long pass cannot be evicted by
elapsed time; cache coherence is structural (atomic swap → new inode →
every resume fingerprint refolds). No cooperative invalidation.
8. **CPL-5 join**: every pre-compaction `attempt_id` resolves through
live ∪ `archived_attempt_ids` (hash-chained, tamper-evident, each segment
bounded to the archive directory, revalidated as a ledger, cached per
immutable segment BY FINGERPRINT, chain epochs stepping down to 1). The
CPL-5 reverse sweep (NOT on this base — only its design note is) must
consult this union and treat typed corruption as UNKNOWN/skip; contract
recorded in the design note §10 and the ledger section.
## 3. Pins (tests/test_usage_compaction.py, 31 tests; round-2 pins in §6)
- exact money + whole-projection equality (incl. `skill_review_usage` waves)
- global + root budget refusal thresholds identical across compaction
- in-flight survival + post-compaction settle/release
- crash injection between archive and swap → byte-identical ledger, retry OK
- chained compactions: id resolution live ∪ archive; tampered segment raises
- subscription/external replay dedup + identity-conflict still enforced
- legacy import: rows retained; watermark-loss replay appends nothing
- trigger: config threshold gates the reserve path; growth-throttle after an
unprofitable pass; verify-abort on a foreign non-canonical literal
- structure: baseline rows only at head (tail-smuggled row = corrupt; group
without header = corrupt); quarantine + `integrity_degraded` on a compacted
file unchanged; archive segment = exact source bytes, sha-pinned
- round 2 adds fifteen pins for lock ownership/heartbeat, the snapshot
re-check, the archive directory chain and its fsync failure, header
provenance and counts, bounded archive references, epoch-chain steps,
segment revalidation, warm-cache integrity, typed corruption of an
unreadable header, union caching, and decimal precision (§6)
- round 3 adds sixteen more (five of them in `tests/test_lockfile_helpers.py`)
for lock-file ownership on eviction/release/renewal, the pass abandoning a
lost hold, heartbeats inside the long span, writer exclusion at the swap and
the absence of any unlocked fallback, the post-swap re-read, retry
durability of the directory chain, the archive epoch anchor and its orphan
tolerance, source-range provenance, the segment-cache windows, and archive
symlink bounds (§7)
- round 4 and its verification add eight (two of them in
`tests/test_lockfile_helpers.py`): two racing reclaimers yield at most one
holder, a heartbeat after an atomic replacement of the lock file answers
False, an append between the pre-swap re-check and the rename aborts
without loss, a hold lost at the archive is seen before the snapshot
re-check is even asked, a hold lost after the re-check aborts before the
swap, a hold lost WHILE the candidate temp is written refuses the replace
(the verification round's panel fix), and a link planted after the bound
check can neither receive (writer) nor serve (reader) history (§8)
- round 5 adds ten (five of them in `tests/test_lockfile_helpers.py`, one
Windows-only): a non-contention kernel refusal fails the acquisition
closed; a stale lock is never evicted without the kernel hold; the name
tier is chosen by the predicate and makes no kernel call; the capability
probe decides once per directory and leaves no residue; LockFileEx
contention reads as busy (Windows); the pass refuses the name tier while
appends continue; a refused rename re-proves the hold and the snapshot
before retrying (append / hold-lost variants); the epoch anchor scans the
directory the chain was walked in; an entry the anchor cannot open is
typed corruption; a hold lost before the first commit look writes no
orphan (§9)
- round 5.2 adds two in `tests/test_lockfile_helpers.py` (a creator evicted
while still lock-less never returns a descriptor; LockFileEx refusals
classify by their Win32 error — runs on POSIX too) and, in this suite, one
new pin plus five strengthened ones: a hold lost the instant the last
snapshot look answered True refuses the rename; the after-recheck pin also
requires that the in-swap look is never asked once the hold is gone; the
anchor-swap pin carries the epoch-3 NAME with the forged live header and
requires `generation newer`; the cannot-open pin also plants a directory
and a FIFO (skipped, no hang) and requires `could not complete`; the
warm-cache pin adds the directory-in-place-of-segment shape (`not a
regular file`); the name-tier pin requires exactly one durable
`usage_ledger_compaction_refused` row and the tripwire text naming the
tier (§9, second block)
- round 5.4 adds four in `tests/test_lockfile_helpers.py` and, in this
suite, two new pins plus three strengthened ones: a stamp-less ledger still
inspects its archive fail-closed (a regular file where the directory
belongs, an uninspectable archive); a path inspection the reader cannot
make is typed corruption (the not-searchable segment directory, an
`is_symlink` the kernel refuses); the swap-lie pin gains the `erased`
variant (a charge landed inside the rename syscall is quarantined, flags
integrity, and the pass raises instead of receipting); the name-tier pin
gains the append-returned-False shape; the reserve-path pin proves the
heartbeat it is handed is THAT lock's (aged lock renewed), not a callable
(§9 "Round 5.4")
Mutation-probed red (not just green-once): `_summary` weight math, group-sum
rounding, folding of dispatched rows — each flips at least one pin.
## 4. Gate evidence (this host, isolated env roots)
- targeted: usage family (7 files) green; budget family (5 files) green;
persistence inventory + domain manifest + rotation train green;
test_usage_compaction 16/16 green
- full CI-shape non-serial battery (`-m "not serial and not integration and
not browser and not ui_browser and not ui_browser_docker and not
portable_detail and not skill_smoke and not size_ratchet" -n 16 --dist
loadscope --max-worker-restart=0 --timeout=300 --timeout-method=thread`):
EXIT=0, ~13.4k outcomes, 0 failed (first run had exactly one red —
the ABI-3 alias sweep discovering the new `cost_usd` emission site — fixed
by the per-site allowlist row, battery relaunched whole and green)
- serial pass: EXIT=0 (622 passed / 39 skipped); size_ratchet: 5/5, exit 0
(PIPESTATUS-preserved); `ruff check . --select F` clean;
`scripts/v7next_adoption.py` OK; `git diff --check` clean;
`git rev-parse HEAD` verified after every pytest run
- scale smoke: 24,000-row / 11.9 MB synthetic ledger → 183 KB (65×),
280 groups, 1.16 s pass; projections byte-equal; post-compaction reserve
correctly refused over the folded money (accounted $1228 > $200 limit)
## 5. Known residuals (disclosed, not defects)
1. Subscription/external/legacy/review-attributed rows never fold → slow
residual growth on delegation- or skill-review-heavy installs; the 20 MB
WARN now names exactly this case. A future lane may fold them behind an
archived-identity membership check (design note §3).
2. The in-compactor render fingerprint mirrors the COMPOSITION of
`usage_projection`/`usage_breakdown` (using the same production `_summary`
/ `_breakdown_bucket` primitives). A future divergence in that composition
would weaken the self-check, not correctness (worst case: a lawful pass
aborts); the end-to-end pin compares the real projection functions.
3. Directory fsync is a disclosed no-op on Windows (POSIX guaranteed and now
FATAL on failure, round 2 / finding 2); worst case there is a lost archive
dir entry AND a swap in the same crash window — mitigated by archive-first
ordering, disclosed in the design note.
4. A float-boundary rounding coincidence can make the rounded projections
differ pre/post → the pass aborts and the ledger simply stays uncompacted
(correctness over availability; disclosed in design note §5).
5. CPL-5's sweep is not on this base; its contract (consult live ∪ archive;
corrupt chain = UNKNOWN/skip) is recorded in the design note §10 for the
lane that lands it. `model_send_seal`-targeted gates therefore do not
exist on this base to run.
6. **Orphan archive segments** (round 2, widened in round 3): a pass that
loses the snapshot race, dies at its swap, or is abandoned by a lost lock
can leave a written-but-never-referenced segment. It carries no money and
no chain authority — readers start at the live header and follow only what
it names, and the epoch anchor recognises an orphan of the live generation
as legal because its bytes are still a PREFIX of the live file (round 5.3;
matching its leading row alone admitted a restored generation too). Repeated lost races nevertheless accumulate disk: LOW
availability / forensic clutter, not correctness. No GC by design (§13 of
the note).
7. **Warm segment-cache window** (round 3): the per-segment cache hit needs a
matching fingerprint, an mtime settled for > 2 s and an entry younger than
60 s. An in-place same-size rewrite that ALSO restores `mtime_ns` exactly
can therefore still be answered from a warm entry for up to a minute.
Closing it means re-hashing every segment on every question — the
quadratic cost the cache exists to remove — for an attacker who already
has write access to the data root and can be caught a minute later, by any
other process, and by the chain hash on every segment an answer depends
on.
8. **Ownership is defended, not guaranteed — per tier, and bounded** (round
3, stated per tier in round 5, bounded in round 5.2, identity-complete in
round 5.3): on the enforced tier the lock primitives are ownership-exact
and kernel-guarded (the acquisition itself included: neither an evicted,
still lock-less creator nor a descriptor whose own identity the kernel
cannot read ever returns a hold), and the pass heartbeats through its long
span,
immediately before every rename attempt and again after the in-swap
snapshot look. No claim is made that a pass can never be robbed of the
lock. The bounded claim: a concurrent holder can exist only after the lock
file is removed by an actor outside the lock protocol (a hand repair, a
foreign helper, a name-tier process of a mixed-tier install — in-protocol
eviction is impossible under the held flock and the heartbeat-fresh
mtime); such a robbery is caught at the next proof, and the irreducible
residual is the interval between the final ownership proof and the rename
syscall, in which a charge landed by that holder IS erased by the rename.
*(Correction, round 5.4: this packet, DESIGN §8/§12.9 and the round-5.2
ledger line said the loss was "then surfaced by the post-swap re-read or
quarantined seq-misnumbered on the next read" — neither could ever see it:
the re-read compares the NEW inode against the candidate and the archive
segment is the pre-row snapshot, so the loss was SILENT and a success
receipt was returned. Now, on POSIX, the swap holds the old inode open
across the rename and reads back what landed beyond the proven snapshot:
those bytes are quarantined — `state/usage_attempts.quarantine.jsonl`,
which flips `integrity_degraded` — and the pass raises typed instead of
receipting; the charge is not re-appended. Windows cannot hold the
destination open through `os.replace`: silent there, disclosed. §9 "Round
5.4", R5.)* In-protocol, no writer can append in the
compare→replace window, because every writer of this ledger takes the same
owner-aware lock and has no unlocked fallback. On the name tier no such
claim is made at all: the pass does not run there (§5.10). The round-5
sentence "it cannot finish while robbed" was an absolute the round-5.2
probe refuted (PROBE-1: a row appended after the third look answered True
and before `os.replace` was erased, receipt returned); corrected here.
9. **Epoch anchoring reads content, not names** (round 3, narrowed in round
5, classified in round 5.2): a garbage REGULAR file whose first row reads
but does not parse (a torn segment from a crashed write) is no evidence of
any generation rather than corruption, so it cannot deny service to the
whole history; a directory or special file is not a segment (segments are
regular files by construction) and is skipped — a FIFO is opened
`O_NONBLOCK`, so it cannot hang the question either *(qualified, round 5.4:
an entry that OPENS but is not a regular file is skipped; one the kernel
refuses to open at all — a UNIX socket, ENXIO — is corruption like any
other unopenable entry on the dir-fd shape; the path shape's stat-before-open
classifies a socket as not regular and skips it)*; an entry the scan
cannot list, open or read IS corruption — the scan did not complete, the
data root's own handle included since round 5.3, and since round 5.4 every
path inspection the reader makes (the symlink bounds on both archive levels
and on the named segment; the stamp-less archive check, which ends the
question early only on an exact ENOENT) is typed the same way instead of
escaping as a bare `OSError` or a silent empty answer. Disclosed since
round 5.4: with a stamp-less live file every parsable regular file in the
archive that is not its byte-prefix is `generation newer`, so a ledger
reset beside a surviving archive is a `generation newer` corruption verdict
on every history question until the fresh ledger's epoch passes the
surviving segments, which are then silently ignored (reset both together)
and an operator's stray JSON file
there is corruption on a stamp-less ledger only; and the readers being
lock-free, a compaction that commits between a question's live-header read
and its anchor scan makes that ONE question UNKNOWN (`generation newer`),
the next question walking the new chain — and the scan runs through
the very handle the chain walk held, entries opened relative to it (and
without a dir-fd the classification happens BEFORE the open, which is the
step a directory refuses on Windows and a FIFO blocks on: round 5.3). The round-5 wording ("an entry the scan cannot list, open
or read" beside "a garbage file cannot deny service") contradicted itself
for a directory/FIFO/unopenable file and was false for the first two:
round 5 made a stray `backup/` directory typed-corrupt for every history
question (reproduced on `2dd3e017`; `13af62c5` answered) — an availability
regression with no correctness gain, corrected here. Round 5.3 then replaced
the orphan exemption itself: recognising an orphan by its leading row also
admitted a restored previous generation, which is NOT indistinguishable and
does hide ids — the attempts the rolled-back compaction folded exist
nowhere else. An orphan is the pre-swap copy of the live file, so its bytes
are still a prefix of it; that is the test now, and it runs on a stamp-less
live file too. Every
segment an answer actually depends on is still fully verified by the
chain walk, and a named segment that is not a regular file or whose read
fails is typed corruption, never a bare `OSError`.
10. **The lock has two tiers, by capability predicate** (round 4, made
explicit in round 5): `platform_layer.kernel_file_locks_enforced` locks a
scratch file in the lock directory once per process; only the kernel's
own "this filesystem cannot" selects the name tier — exactly
EOPNOTSUPP/ENOTSUP/ENOSYS *(correction, round 5.4: ENOLCK was in the set
until then — "no locks available" is a missing lock daemon OR an exhausted
kernel lock table, not a capability answer, and it selected the tier where
the round-3 race returns; round 5.4 made it keep the enforced tier and fail
EVERY live acquisition closed — product-wide, the lenses showed, since the
primitive is shared — so the close-out made ENOLCK the name tier with its
errno RECORDED and a per-caller refusal: only the monetary lock names it
(`refuse_name_tier_errnos={ENOLCK}`), so a lockd-less NFS refuses every
monetary write with `UsageAccountingError` instead of running the name
protocol while every other lock keeps the protocol it always ran there;
the per-directory verdict is decided ONCE under a module lock, so racing
threads share one probe; an unprobeable directory answers enforced for
that call, uncached — §9 "Round 5.4" and §10, R1)*, and since
round 5.3 the two Win32 answers of a volume without byte-range locks —
ERROR_INVALID_FUNCTION, which LockFileEx answers on `\\wsl$`
(microsoft/WSL#5762), and ERROR_NOT_SUPPORTED, error 50 on a Samba share —
map onto ENOSYS and EOPNOTSUPP (onto ENOLCK and EOPNOTSUPP before round 5.4):
CPython's winerror→errno table lands both on EINVAL, which left the name
tier structurally unreachable on Windows, so a lock-less volume there
failed EVERY monetary append closed instead of degrading to it.
*Enforced tier* — POSIX flock and Windows LockFileEx, both held on
the lock fd — a refusal that is not contention fails the acquisition
closed (no descriptor, our own file removed, a stale lock never evicted
without the hold); a live-but-WEDGED holder can no longer be evicted by
age — the deliberate trade of an availability incident (the wedged writer
must die first) for the correctness incident (age-evicting a live
monetary writer). Windows cannot unlink an open file: its eviction and
release re-check the path after the close (release included — the POSIX
"unlink under the still-held flock" is POSIX only, stated so since round
5.4), and a freshly won lock — held open by its owner — is undeletable
there. Disclosed since round 5.4, both tiers: a contention answer on the
creator's OWN fresh file (a foreign flock holder that never unlinks it)
leaves that live-pid-stamped file on the path — the creator re-contends
against it until its timeout and owner-aware acquirers never age it out
while the process lives; no in-protocol holder produces that shape.
Also since round 5.4 the recycled-pid disclosure names its real
mechanism: `pid_is_alive` read EPERM as DEAD, so another user's recycled
pid was reclaimed through the age path (flock-guarded on the enforced
tier) and only a same-uid recycle wedged — the opposite of what round 5.3
wrote; EPERM now reads alive (the process exists) — in `pid_is_alive`, the
one liveness primitive shared by every consumer (custody settlements, claim
reclaims, staging reaps defer for such a pid too) — so ANY live impostor —
same uid or another — wedges the lock from the 90 s staleness window
(`usage_ledger._locked`, `stale_sec=90.0`) until it exits, the probe
flock deliberately unconsulted while the pid reads alive (a mixed-tier
name-tier holder has none); Windows also has no
`dir_fd`/`O_DIRECTORY`, so its archive bound and anchor scan stay
path-based (fail-closed on any OSError since round 5). *Name tier* —
kernel-lockless filesystems only — keeps the O_EXCL name protocol with
re-check-then-unlink eviction, a disclosed best effort with no kernel
exclusion: the compaction pass refuses to run there
(`usage_compaction.NAME_TIER_REFUSAL`: logged, throttled by the growth
guard, and since round 5.2 written ONCE per process per data root as a
typed `usage_ledger_compaction_refused` event — the cause the 20 MB
tripwire now names; the round-5 claim that the tripwire "names the case"
was false until then) while ordinary appends continue under the name
protocol. Residual, disclosed: the tier is decided per process per
directory, so a lockd that dies mid-run can leave one process on each
tier until restart — the name-tier process never compacts, and it also
evicts by NAME with no kernel hold, so in that mixed mode the round-3
two-writer class returns for the enforced-tier process's heartbeat-less
APPENDS, not only for compaction. Also since round 5.2: the acquisition
is identity-checked on both tiers (an evicted, still lock-less creator
re-contends instead of returning a descriptor; the owner pid is written
before the lock), and on Windows only ERROR_LOCK_VIOLATION reads as busy
— access-denied and sharing-violation fail the acquisition closed at
once instead of re-contending until the 45 s timeout (unexecuted here,
owed to the 3-OS matrix). The round-4 claim that the
anchor's path-based reads "can only ever ADD a corruption verdict" was
false: a directory swapped after the walk made the path-based scan FAIL
to add the verdict it owed; corrected in round 5 (§9, finding 3).
## 6. Round 2 — adversarial wave disposition (fix-round base `e2801c52`)
Verdict of the wave: NEEDS FIXES, nine findings. **All nine accepted and
fixed** — this is the monetary authority, so nothing was argued away as
theoretical. Every fix carries a pin that was verified RED against the exact
mutation it claims to catch (the mutation harness reverts one behaviour and
reruns the suite), and the three pins the wave called weak were rebuilt.
| # | wave finding | disposition | fix | red-first pin |
|---|---|---|---|---|
| 1 | HIGH — a long pass can be robbed of the lock (`stale_sec=90`, no `owner_aware_stale`), and a prior owner can unlink the new owner's lockfile; the swap then replays a stale snapshot over a concurrently appended charge | **accepted, fixed both ways** | `_named_lock` acquires owner-aware (a live PID is never evicted by age) and yields a heartbeat (`platform_layer.refresh_exclusive_file_lock`, descriptor-targeted so a stolen lock is never refreshed for the thief) that the pass beats at each checkpoint; **and** the swap is refused unless the live bytes still equal the snapshot, re-read under the same held lock right before the rename | `test_monetary_lock_is_owner_aware_and_the_pass_heartbeats_it`; `test_append_between_snapshot_and_swap_aborts_instead_of_erasing_it` (injected append → pass returns `None`, the row survives, money = before + that row) |
| 2 | HIGH — archive durability: only the segment's own parent is fsync'd, and `_fsync_dir` swallows every error, including on POSIX | **accepted, fixed** | `_mkdir_fsync_chain` syncs every directory entry the chain creates (segment parent, `archive/`, data root); `_fsync_dir` raises on POSIX and is a no-op on Windows *by the platform predicate*, not by a bare `except` | `test_archive_directory_chain_is_durable_before_the_swap` (fsync'd inodes recorded and required BEFORE the swap); `test_posix_directory_fsync_failure_aborts_before_the_swap` |
| 3 | HIGH — the baseline validator accepts a rolled-back hash chain and forged seq/epoch provenance; the archive reader scrapes ids instead of validating | **accepted, fixed** | substrate validates the stamp: epoch, bounded `archive_rel`, 64-hex sha, closing counts (`folded + retained == source`, first seq 1, last seq == source rows), block↔header agreement (`group_count`, summed `folded_attempt_count`), and `pre_compaction_seq` uniqueness/monotonicity under a stamp only. The reader runs each segment through `_validate_records` and requires the chain's epochs to step down by one to a header-less epoch 1 | `test_repointing_the_header_at_an_older_segment_is_corrupt` (three epochs; both skip shapes); `test_baseline_header_counts_must_close`; `test_pre_compaction_seq_is_a_checked_provenance_claim`; `test_rehashed_segment_still_fails_the_ledger_structure`; `test_a_group_row_cannot_rejoin_the_block_after_it_closed` |
| 4 | MEDIUM — a warm segment cache hides a deleted or replaced segment | **accepted, fixed** | the cache hit additionally requires the file's `(ino, dev, size, mtime_ns)` fingerprint; a miss re-reads, re-hashes and re-validates | `test_warm_segment_cache_revalidates_the_file_it_cached` (delete, then rewrite, both after a warm read) |
| 5 | MEDIUM — "decimal exactness" is bounded by the ambient 28-digit context, and the self-check rounds the same way | **accepted, fixed** | sums run under `_exact_money` (`prec=60`, `Inexact` trapped), so a loss past even that aborts instead of approximating; the pin's oracle sums in its own wider context | `test_group_sums_survive_beyond_the_default_decimal_precision` (10²⁸ + 1 keeps its last digit; red-first showed the dollar vanishing) |
| 6 | MEDIUM — `archive_rel` is not bounded to the archive directory | **accepted, fixed with 3** | `usage_ledger.valid_archive_rel` (textual bound, substrate-owned) plus a resolved-path bound in the reader (defeats a planted symlink) | `test_archive_reference_is_bounded_to_the_archive_directory` (six shapes rejected by the validator; an existing, correctly hashed file outside the archive rejected by the reader) |
| 7 | MEDIUM — a corrupt live header reads as "never compacted" | **accepted, fixed** | `_live_baseline_header` raises `UsageLedgerCorrupt` on an unreadable or non-object first row; `None` now means only "a readable row that is not a stamp" | `test_unreadable_leading_row_is_typed_corruption_not_absence` (the CPL-5 join raises → UNKNOWN, never an orphan verdict) |
| 8 | LOW — the join primitive re-unions the whole archived id set per question | **accepted, fixed** | the union is cached by chain identity ((`archive_rel`, sha) per hop); the stat-checked walk still runs, so finding 4's guarantee is not traded for the cache | `test_archived_id_union_is_built_once_per_chain` (H questions → exactly one union build) |
| 9 | MEDIUM — three pins do not pin what they claim | **accepted, all three rebuilt** | crash pin injects at `os.replace` itself and asserts the segment is already on disk with the exact source bytes (a swap-before-archive reorder now fails it); the threshold pin proves the lock is HELD at the call rather than trusting the call site; the head-only pin contrasts one unmodified baseline block that validates at the head with the same rows rejected purely for position | `test_crash_at_the_ledger_rename_leaves_ledger_intact`; `test_reserve_path_compacts_only_past_config_threshold`; `test_baseline_header_is_rejected_by_POSITION_not_by_shape` |
Not changed by round 2, and deliberately so: the fold scope (§3 of the
design note), the per-group baseline shape the wave independently confirmed
preserves per-root enforcement and all five breakdown axes, the trigger
thresholds, and the ABI-3 allowlist row the wave found correctly scoped.
New residual disclosed by finding 1's fix: a pass that loses the snapshot
race leaves an orphan archive segment (already written, never referenced).
Orphan segments were disclosed as harmless before, and the archive is
append-only by design (§13); the alternative — swapping anyway — is the
defect being fixed.
Round-2 code commits (author `ouroboros-agent`, single-intent):
`9e99eb55` (findings 1, 2, 9-crash, 9-threshold), `0ed2dc2c` (findings 3, 4,
6, 7, 8, 9-position), `6b03212e` (finding 5 + the ARCHITECTURE ownership
line).
## 7. Round 3 — second adversarial wave disposition (fix-round base `830aa35a`)
Verdict of the second wave: NEEDS FIXES. It re-read the round-2 fixes and
judged five of the nine findings still OPEN (1, 2, 3, 4, 6), closing 5, 7, 8
and 9. **All five accepted and fixed**; nothing was argued away. Each fix carries a pin verified RED against the exact mutation it
claims to catch, on this base, before the fix landed.
| # | round-2 verdict | what was still open | fix | red-first pin |
|---|---|---|---|---|
| 1 | HIGH, OPEN | ownership was never actually proven: stale inspection judged the path and then unlinked the path; release unlinked whatever now occupied the name; the POSIX heartbeat renewed the descriptor and answered success after it had been unlinked; `_beat` ignored the answer; nothing beat during the long build/verify span; and the snapshot compare→replace stayed a TOCTOU window | `platform_layer` now compares descriptor identity with path identity everywhere: the eviction removes only the exact file it judged (re-checked immediately before the unlink), the release removes only the file it still holds, and `refresh_exclusive_file_lock` returns an OWNERSHIP verdict. `_beat` aborts the pass on a lost or unanswerable hold and runs inside both candidate row walks and between every verification stage. The window is closed structurally — every ledger writer takes this same owner-aware lock with no unlocked fallback — and the swap re-reads what landed | `test_stale_eviction_never_removes_a_lock_re_created_under_it`, `test_release_never_unlinks_a_lock_that_was_stolen`, `test_heartbeat_reports_lost_ownership_instead_of_renewing` (+ deleted-lock variant) in `tests/test_lockfile_helpers.py`; `test_a_lost_lock_aborts_the_pass_instead_of_swapping`, `test_the_long_build_and_verification_section_beats_the_lock`, `test_no_writer_can_append_between_the_snapshot_check_and_the_swap`, `test_every_ledger_writer_refuses_when_the_lock_cannot_be_taken`, `test_a_swap_that_did_not_land_is_a_typed_failure_not_a_receipt` |
| 2 | HIGH, OPEN | durability was established only for the levels a pass CREATED, so the retry after a pass that died on its own fsync skipped directories that already existed but were not yet durable | `_mkdir_fsync_chain(path, root)` fsyncs the whole chain up to the data root unconditionally, every pass | `test_the_directory_chain_is_re_synced_on_the_retry_after_a_failed_pass` (fails the first pass on the directory fsync, then requires all three inodes fsync'd before the retry's swap) |
| 3 | HIGH, OPEN | the chain had no trusted live anchor — `compaction_epoch` is as mutable as the rest of the row, so repointing the header at an older genuine segment AND lowering the epoch walked a valid short chain; `pre_compaction_seq` was only required to increase | the archive anchors the stamp: no segment may carry a generation newer than the live one, derived from each segment's embedded header (content, not name), with an uncommitted orphan of the live generation explicitly legal. `pre_compaction_seq` must fall inside the header's declared source range | `test_repointing_the_header_at_an_older_segment_is_corrupt` — the forgery now copies `compaction_epoch` too, which the wave named as the pin's escape hatch; `test_pre_compaction_seq_must_name_a_row_the_named_source_held`; `test_an_orphan_segment_of_the_live_generation_is_not_a_rollback` guards the fix against over-reach |
| 4 | MEDIUM, OPEN | a fingerprint is not identity: an in-place same-size rewrite inside timestamp granularity, or with the mtime restored, kept the cache hit | a hit also requires an mtime settled for > 2 s and an entry younger than 60 s; past either, the bytes are hashed again. Remaining window disclosed as residual §5.7 | `test_a_rewrite_inside_the_timestamp_window_is_re_hashed_not_recalled`; `test_a_same_size_rewrite_is_caught_once_the_cache_entry_expires` |
| 6 | MEDIUM, OPEN | a symlink AT `archive/usage_ledger` escaped the resolved-parent bound, because segment and directory resolve through the same link | neither `archive/` nor `archive/usage_ledger` may be a link, the resolved directory must be exactly the resolved root's archive path, and no segment may be a link; the reader calls it corruption, the writer aborts its pass | `test_a_symlinked_archive_path_is_refused_by_writer_and_reader` (both levels, reader and writer) |
| 5, 7, 8, 9 | CLOSED by the wave | — | unchanged | unchanged |
ARCHITECTURE and the design note carried absolutes the wave was right to
call out (`never robbed of it`, unqualified `bounded`). Both now state the
contract with its residuals: ownership is defended and its loss is
survivable; the archive bound is exact about symlinks; the cache window and
the orphan segments are named where the mechanism is described (§5.6–5.9).
Round-3 code commits (author `ouroboros-agent`, single-intent): lock
ownership in `platform_layer` + its pins; the pass consuming ownership
(heartbeat abort, span checkpoints, post-swap verify); the unconditional
directory chain; the archive epoch anchor + source-range provenance; the
segment-cache shelf life; the archive symlink bound.
## 8. Round 4 — third adversarial wave disposition (fix-round base `d7b487ab`)
Verdict of the third wave: NEEDS FIXES. It judged the round-3 ownership and
bound fixes still short of the contract in four ways — the exclusion itself
was still only a name protocol, ownership was not proven adjacent to the
decisions it licenses, the recheck→replace gap remained, and the symlink
bound was still check-then-use. **All four accepted and fixed**; nothing was
argued away.
| # | what round 3 left open | fix | red-first pin |
|---|---|---|---|
| 1 | exclusion rested on the O_EXCL name protocol: the stale eviction re-checked the inode and then unlinked the PATH (a pause between the re-check and the unlink lets a second reclaimer remove the first one's freshly won lock — two writers on one monetary authority), and the release had the same window between its look and its unlink | the lock fd HOLDS a kernel lock (`fcntl.flock`; `LockFileEx` on Windows) from acquisition; a stale lock is evicted only while flock-holding the very fd that was judged, with the path re-checked under that hold, and a release unlinks BEFORE its close, under the still-held flock. Windows (no unlink of an open file) and filesystems without kernel locks keep the re-check-then-unlink shape as a best effort chosen by the platform predicate — disclosed, never an exception swallowed *(correction, round 5: false at `13af62c5` — any `OSError` from the kernel lock selected the name shape, silently; fixed by round 5, finding 1)* | `test_two_racing_reclaimers_never_yield_two_holders` (both reclaimers herded into the check-to-unlink window; RED on the round-3 code with both returning descriptors); `test_heartbeat_after_an_atomic_swap_of_the_lock_reports_false` (the path never absent, so an existence check would renew; red against the utime-only mutation) — both in `tests/test_lockfile_helpers.py` |
| 2 | the pre-swap re-check and the rename were separated by the tmp write and fsync: a row appended in that gap was erased by the swap, receipt and all | `_write_bytes_atomic_fsync` takes a `precondition` evaluated after the temp bytes are durable, immediately before `os.replace` — the last instant the replace can still be refused; the compactor passes `_snapshot_intact`, so the pass aborts with the ledger (and the landed row) byte-identical | `test_an_append_between_the_recheck_and_the_replace_aborts_without_loss` (RED on `d7b487ab`: the row was erased and a receipt returned; now the pass returns `None`, the row survives, money = before + that row, no temp residue) |
| 3 | ownership was beaten through the span but not adjacent to the decisions: nothing proved the hold immediately before the snapshot re-checks, and nothing at all between the final re-check and the swap | `beat()` now runs immediately before EACH snapshot look: a hold lost at the archive write aborts before the post-archive re-check is even asked (its answer would be meaningless), and a hold lost after that re-check aborts before the replace — the proof before the swap was moved INSIDE the atomic replace by the verification pass (panel FIX_FIRST; see the verification block below) | `test_a_hold_lost_at_the_archive_is_seen_before_the_snapshot_is_trusted` (asserts exactly ONE `_snapshot_intact` call; the "remove the beat before the re-check" mutation makes it two — red against that exact mutation); `test_a_hold_lost_after_the_recheck_aborts_before_the_swap` (RED on `d7b487ab`: the swap ran) |
| 4 | the symlink bound was check-then-use: `_archive_dir_bounded` / `_segment_path` judged paths, then the write and the read re-resolved those paths — a link planted in between received the segment (writer) or served a foreign file (reader) | POSIX opens the chain root→`archive/`→`usage_ledger` `O_DIRECTORY\|O_NOFOLLOW` handle-to-handle and creates/opens the segment `O_NOFOLLOW` via `dir_fd`, fingerprinting and reading from the open fd; directory durability is fsync'd through the same held handles. The path-based checks remain as the early typed abort and as the Windows best effort (no `dir_fd`/`O_DIRECTORY` there), chosen by the platform predicate | `test_a_link_planted_after_the_writer_bound_check_cannot_receive_history` (RED on `d7b487ab`: the segment crossed the link and the swap completed); `test_a_link_planted_after_the_reader_bound_check_is_refused` (byte-identical copy behind the link — the hash cannot object, only refusing the traversal defends; RED on `d7b487ab`) |
Confirmed rather than changed: every writer of this ledger already takes the
same owner-aware lock with no unlocked fallback (round-3 pin stands; what
changed is that the lock they all take is now kernel-held), and the
post-replace re-read stays, now after the in-swap re-proof.
New/updated residuals (also §5): a live-but-WEDGED holder can no longer be
evicted by age on POSIX — the kernel lock outlives the staleness clock until
the process dies. That is the deliberate trade: age-evicting a live writer
was the two-writers defect; a wedged monetary writer is an availability
incident, not a correctness one. Windows and kernel-lockless filesystems
(bare NFS and friends) run the round-3 identity-re-check shape as a disclosed
best effort selected by the platform predicate *(correction, round 5: at
`13af62c5` the selection was by exception, not by predicate — §9, finding
1)*. `ouroboros/usage_compaction.py`
entered the 1001-1500 size band with a recorded rationale (the dir-fd
anchoring and the in-swap re-proof live beside the pass they defend);
`ouroboros/platform_layer.py` stays inside the band at 1498 lines, paid for
by prose compression in the same module.
### Round-4 verification (base `d7b487ab`; the round-4 work had shipped unexecuted)
Round 4 was authored in an execution-denied environment, so a dedicated
verification pass ran every claim for real. One finding of the round-4
review panel (codex, FIX_FIRST, accepted by the coordinator) was fixed in
the same pass:
- **The ownership proof stood before the swap, not inside it**: `beat()` ran
immediately before `_swap_ledger_fsync`, but the atomic writer can spend
arbitrarily long writing and fsyncing the candidate temp before its
snapshot look and `os.replace` — a hold lost in that window let a new
holder's charge (landing after the in-swap snapshot answer, before the
rename) be erased by the swap. The proof of ownership now lives in the
precondition of the atomic replace itself: once the temp bytes are
durable, immediately before the rename, ownership FIRST and the snapshot
compare only under a proven hold (`_swap_ledger_fsync` passes `beat` into
`_write_bytes_atomic_fsync`'s precondition). Pin:
`test_a_hold_lost_while_the_temp_is_written_refuses_the_replace` — RED on
the round-4-as-authored shape (ownership died with the temp on disk; the
snapshot-only precondition let the rename run and a receipt returned),
green with the fix: the replace is refused and the new holder's charge
survives byte-for-byte, money = before + that charge.
Every round-4 red-first claim was then observed, not argued — each pin was
run against the exact mutation or base it names (mutation applied, pin RED,
mutation reverted, pin green):
| pin | mutation | red observed |
|---|---|---|
| `test_two_racing_reclaimers_never_yield_two_holders` | `platform_layer.py` reverted to `d7b487ab` | both reclaimers returned descriptors: 2 holders |
| `test_heartbeat_after_an_atomic_swap_of_the_lock_reports_false` | identity comparison removed from `refresh_exclusive_file_lock` (utime-only) | heartbeat answered True for a replaced lock |
| `test_an_append_between_the_recheck_and_the_replace_aborts_without_loss` | swap precondition removed entirely | receipt returned; the injected row was erased |
| `test_a_hold_lost_at_the_archive_is_seen_before_the_snapshot_is_trusted` | post-archive `beat()` removed | 2 `_snapshot_intact` calls instead of 1 |
| `test_a_hold_lost_after_the_recheck_aborts_before_the_swap` | in-swap ownership proof removed (snapshot-only precondition, no outer beat) | the swap ran; a baseline landed |
| `test_a_hold_lost_while_the_temp_is_written_refuses_the_replace` | round-4-as-authored shape (outer `beat()` + snapshot-only precondition) | receipt returned while robbed |
| `test_a_link_planted_after_the_writer_bound_check_cannot_receive_history` | `usage_compaction.py` reverted to `d7b487ab` | the segment crossed the link; the swap completed |
| `test_a_link_planted_after_the_reader_bound_check_is_refused` | `usage_compaction.py` reverted to `d7b487ab` | the byte-identical copy was read through the link (no raise) |
Windows tier: the two new lockfile pins exercise POSIX mechanics (flock-held
eviction; replacing an open, kernel-locked file), so both carry
`skipif(IS_WINDOWS)` with the disclosed-best-effort reason; the compaction
pins are platform-neutral, and the two planted-link pins already skip on
Windows. `fcntl` is imported only inside `not IS_WINDOWS` branches of the
`platform_layer` primitives, so the module imports cleanly where `fcntl`
does not exist.
Round-4 verification gate evidence (this host, isolated env roots, venv
python 3.10.12 / pytest 9.1.1): recorded in
`docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 fix-round 4 verification"
— targeted usage/lockfile suites green; CI-shape non-serial battery EXIT=0;
`-m serial` EXIT=0; `-m size_ratchet` green; `ruff check . --select F`
clean; `scripts/check_domains.py` OK; `scripts/regenerate_inventories.py
--check` OK; `git diff --check` clean; `git rev-parse HEAD` verified after
every pytest run. With that run recorded, round 4 is verified, not merely
code-complete.
## 9. Round 5 — fourth wave disposition (fix-round base `13af62c5`)
Verdict of the fourth wave (gpt-5.6-sol, read-only): NEEDS FIXES. It closed
the round-3 split-brain class (stale eviction under flock with the inode
re-check, release of the own pathname only, identity heartbeat), all monetary
writers under one lock, and the archive writer/reader dir-fd anchoring; it
left four items open. **All four accepted and fixed**; nothing was argued
away. Each fix carries a pin verified RED against the exact pre-fix shape or
mutation it names, on this base, before the fix landed.
| # | what round 4 left open | fix | red-first pin |
|---|---|---|---|
| 1 | HIGH — on ANY `OSError` from the kernel lock the acquisition silently degraded to the pathname/inode name tier, where the round-3 race returns (and on Windows the errno-less `LockFileEx` failure fell into the same degrade) | the tier is an explicit capability predicate, `platform_layer.kernel_file_locks_enforced(lock_path)`: one scratch-file kernel lock per lock directory per process; only ENOLCK/EOPNOTSUPP/ENOSYS select the name tier. On the enforced tier contention (EAGAIN/EACCES/EWOULDBLOCK) stands down and re-contends; every other refusal fails CLOSED — no descriptor, our own file removed, a stale lock never evicted without the held flock. `_win32_lock` raises an `OSError` carrying the Windows error so `ERROR_LOCK_VIOLATION` classifies as contention. The name tier makes no kernel call at all, and `compact_usage_ledger_locked` refuses it with the typed `NAME_TIER_REFUSAL` (logged; appends continue under the name protocol, disclosed). `usage_ledger.LOCK_REL` is the lock-path SSOT *(correction, round 5.3: the busy set is EAGAIN/EWOULDBLOCK alone — EACCES fails closed since round 5.2, finding W — and the unsupported set is ENOLCK/EOPNOTSUPP/ENOTSUP/ENOSYS plus the two Win32 codes round 5.3 maps onto it; §9 "Round 5.3", findings 2 and L5; correction, round 5.4: ENOLCK left the set — it fails closed — so the set is EOPNOTSUPP/ENOTSUP/ENOSYS, with winerror 1 mapped onto ENOSYS; §9 "Round 5.4", R1)* | `test_a_kernel_refusal_that_is_not_contention_fails_closed`, `test_a_stale_lock_is_never_evicted_without_the_kernel_hold`, `test_the_name_tier_is_chosen_by_the_predicate_not_by_a_refusal`, `test_the_capability_probe_decides_once_and_leaves_no_residue`, `test_windows_lockfileex_contention_reads_as_busy` (skipif not Windows) in `tests/test_lockfile_helpers.py`; `test_the_pass_refuses_on_the_name_tier_while_appends_continue` |
| 2 | HIGH — the ownership→snapshot precondition ran once before `utils.replace_atomic`, which retries `os.replace` up to ten times with pauses on a Windows sharing violation: a charge appended (or a hold lost) between attempts was erased by the retry that landed | `replace_atomic(src, dst, *, precondition=None)` asks the precondition immediately before EVERY attempt, retries included, and returns False without replacing when refused; `_write_bytes_atomic_fsync` routes its ownership-first, snapshot-second proof through it. POSIX behaviour is unchanged (one syscall) | `test_a_refused_rename_re_proves_the_hold_and_the_snapshot_before_retrying[append]` / `[hold_lost]` (first attempt raises `PermissionError`, the intrusion lands, the second call never happens, the row survives / the ledger is byte-identical) |
| 3 | MEDIUM — `_no_newer_archived_epoch` walked the archive by pathname and turned `OSError` into "no evidence": a directory swapped after the safe chain walk could hide a newer generation and admit a forged rollback (the §5.10 claim was false) | `archived_attempt_ids` opens the `O_DIRECTORY\|O_NOFOLLOW` handle chain ONCE — after the live-header read, for the rest of the question *(round 5.2: a directory swapped before that open is the same power as deleting the newer segments, disclosed; a non-regular entry is skipped, not corruption)*; segment loads and the anchor scan open entries relative to that same held handle (one `_open_archive_entry` rule; path-based only where `dir_fd` is absent). An entry the scan cannot list, open or read is `UsageLedgerCorrupt`; a first row that reads but does not parse stays the disclosed torn-segment case | `test_the_epoch_anchor_scans_the_directory_the_chain_was_walked_in` (POSIX; a look-alike directory swapped in after the walk), `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (a dangling entry) |
| 4 | LOW — two surviving mutations (deleting the first commit-section beat; losing the hold between rename retries) and three doc absolutes («cannot finish while robbed», «a hold lost anyway abandons», «kernel-held») stated without their tier | both pinned (the second by finding 2's `[hold_lost]` variant); DESIGN §8/§10/§12, ARCHITECTURE and this packet now state the contract per tier — enforced tier vs name tier | `test_a_hold_lost_before_the_first_commit_look_writes_no_orphan` |
Red observed, not argued — each pin against the exact pre-fix shape or
mutation it names (pin red, fix applied or mutation reverted, pin green):
| pin | mutation / base | red observed |
|---|---|---|
| `test_a_kernel_refusal_that_is_not_contention_fails_closed` | `13af62c5` (silent degrade on any OSError) | a descriptor was returned for an ENOLCK-refused lock |
| `test_a_stale_lock_is_never_evicted_without_the_kernel_hold` | `13af62c5` (`evict_flockless` on a non-contention errno) | the stale file was evicted by name and a descriptor returned |
| `test_the_name_tier_is_chosen_by_the_predicate_not_by_a_refusal` | `13af62c5` (kernel lock attempted unconditionally) | a kernel call was made on the name tier (`[16] == []`) |
| `test_the_capability_probe_decides_once_and_leaves_no_residue` | `13af62c5` | no predicate exists (`AttributeError: _KERNEL_LOCK_TIER`) |
| `test_the_pass_refuses_on_the_name_tier_while_appends_continue` | `13af62c5` (no tier check in the pass) | a receipt was returned on the name tier |
| `test_a_refused_rename_re_proves_the_hold_and_the_snapshot_before_retrying[append]` | `13af62c5` (precondition once, `replace_atomic` retries blind) | the retried rename landed: receipt returned, the appended row erased |
| `…[hold_lost]` | same | the retried rename landed while robbed: receipt returned |
| `test_the_epoch_anchor_scans_the_directory_the_chain_was_walked_in` | `13af62c5` (path-based `iterdir`) | DID NOT RAISE: the look-alike directory hid epoch 3, the forged rollback passed |
| `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` | `13af62c5` (OSError → continue) | DID NOT RAISE: the dangling entry was swallowed as no evidence |
| `test_a_hold_lost_before_the_first_commit_look_writes_no_orphan` | first commit-section `beat()` deleted | `[1] == []`: the pre-archive look was asked and an orphan segment written |
Windows tier, stated plainly: Windows ALREADY held `LockFileEx` on the lock
fd from acquisition (`file_lock_exclusive_nb` is platform-neutral); what was
missing was error classification, so the wave's suggestion of
`msvcrt.locking` was not adopted — it is a thinner CRT wrapper over the same
kernel lock with an EACCES/EDEADLOCK ambiguity, weaker than the `LockFileEx`
the module already owns. The Windows-only pin
(`test_windows_lockfileex_contention_reads_as_busy`, `skipif(not
IS_WINDOWS)`) and the `OSError(0, msg, None, winerror)` mapping it pins were
NOT executed on this host (Linux); they follow the documented CPython
constructor contract (errno derived from `winerror`, `ERROR_LOCK_VIOLATION`
→ `EACCES`) and stay disclosed as unexecuted until the 3-OS CI matrix runs
them. The four POSIX pins and the compaction pins ran here.
Size ratchet, stated plainly: `ouroboros/platform_layer.py` stays at 1498
lines inside the 1001-1500 band — paid for by prose compression in the same
module and by the pid lock and the port sweep reusing the module's own
primitives (`file_lock_exclusive_nb`/`file_unlock`, `force_kill_pid`), not
by any helper or neighbour module. `ouroboros/usage_compaction.py` grew
1094→1124 inside the band; its band rationale could NOT be extended — the
ratchet's own transition rule makes a surviving band rationale immutable
between adjacent manifests (`validate_manifest_transition`: "surviving band
rationale is immutable"), so the round-5 growth is recorded here and in the
ledger instead. `tests/test_usage_compaction.py` sits at 1492 inside the
band (the four copies of the raced charge folded into one `_raced_row`
helper paid for the new pins).
Round-5 code commits (author `ouroboros-agent`, single-intent): `f5eb969f`
(finding 1: lock tiers, fail-closed acquisition, name-tier refusal),
`8ed4f11b` (finding 2: the precondition before every rename attempt),
`a3d4d51d` (finding 3: the anchor through the held dir-fd, fail-closed),
`4b872c22` (finding 4: the first-commit-beat pin); the docs commit follows.
Gate evidence for this round is recorded in `docs/v7next/LEDGER_CORRECTIONS.md`
§"From the C6 fix-round 5 (base 13af62c5)".
### Round 5.2 — adversarial lenses over round 5 (fix-round base `2dd3e017`)
Verdict of the lenses (independent read of `2dd3e017`, PoCs executed on
scratch copies): five HIGH/MEDIUM findings open, six LOW. **All eleven
accepted**; nine are fixed in code with red-first pins, two are closed by
the disclosure the finding asked for (the doc absolutes; the mixed-tier
eviction residual). Nothing was argued away. Every code fix carries a pin
observed RED against the exact pre-fix shape or mutation it names.
| # | finding | fix | red-first pin |
|---|---|---|---|
| 1 | HIGH — a creator evicted while still lock-less flocks its own unlinked inode: between the O_EXCL create and the kernel lock the file is EMPTY (`owner_pid=0`, so owner-awareness cannot protect the window) and holds nothing an evictor must respect; stalled there past `stale_sec` (SIGSTOP, suspend, debugger, NFS clock skew) it is evicted, and its flock then SUCCEEDS on the unlinked inode — two descriptors believed to be one monetary lock (PoC `HOLDERS: 2`; the append transaction never heartbeats, so duplicate `seq` → a real charge quarantined). Same primitive with `stale_sec=10` and no owner-awareness at five non-monetary locks | the owner pid is written BEFORE the kernel lock, and a freshly won lock is returned only if the path still names the descriptor (one stat) — otherwise the creator closes it and re-contends. Both tiers, every caller of the primitive | `test_a_creator_evicted_while_lock_less_never_returns_a_descriptor` (`tests/test_lockfile_helpers.py`; the creator's first kernel lock ages its own file and runs an age-only reclaimer inline) |
| 2 | MEDIUM — the name-tier refusal was a throttled log line folded into the same `False` as "nothing foldable"; the "20 MB tripwire names the case" claim was false (the tripwire text named only a broken compaction or a large residue) | one typed `usage_ledger_compaction_refused` row per process per data root in `logs/events.jsonl` (the existing `append_jsonl`, contained like the compacted event; no return-type change); the tripwire text and the threshold comment name the third cause and the event; DESIGN §8, §5.10 and the module comment corrected | `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (exactly one row after two refusals; the tripwire note names the tier and the event) |
| 3 | MEDIUM — «cannot finish while robbed» refuted in the last-proof→rename gap: `owned_and_intact` proved ownership, THEN read the whole file (≈1.8 ms on 8 MB), then `os.replace` — an fsync'd append (≈0.2 ms) by an out-of-protocol holder landed after the look answered True and before the rename (PROBE-1: receipt returned, row erased); the snapshot-first/beat-second mutation passed every pin | `owned_and_intact` beats, looks, beats AGAIN — the only interval between the last proof and the rename is the syscall (`replace_atomic` asks it before every attempt); DESIGN §8/§12, §5.8 and ARCHITECTURE state the bounded contract instead of the absolute | `test_a_hold_lost_after_the_last_snapshot_look_refuses_the_rename`; `test_a_hold_lost_after_the_recheck_aborts_before_the_swap` now also requires that the in-swap look is never asked once the hold is gone |
| 4 | MEDIUM — the anchor-swap pin pinned only the listing half: under "list via the held fd, OPEN BY PATH" it stayed green for the wrong reason (missing epoch-3 name → `FileNotFoundError` → "could not complete"), while a look-alike carrying the epoch-3 NAME with the forged live header as its leading row was ADMITTED by the orphan exemption | the look-alike now carries exactly that segment (forged header + the real epoch-3 body) and the pin requires `match="generation newer"` | `test_the_epoch_anchor_scans_the_directory_the_chain_was_walked_in` |
| 5 | MEDIUM — round 5's fail-closed rule made a stray subdirectory (an operator's `backup/`, no forgery) typed-corrupt for every history question forever (`os.read` → EISDIR); `13af62c5` answered. LOW siblings: a FIFO blocked the open indefinitely (pre-existing: neither fail-open nor fail-closed), and a directory standing where the header names a segment escaped as a bare `IsADirectoryError` the sweep's `except UsageLedgerCorrupt` would miss | `_open_archive_entry` opens `O_NONBLOCK` through the held dir-fd; `_no_newer_archived_epoch` fstat-classifies — a non-regular entry is no segment and is skipped, an entry it cannot list/open/read stays corruption; `_load_segment` raises typed on a non-regular named segment or any `OSError` of its fstat/read | `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (subdirectory + FIFO under a SIGALRM guard, then the dangling link with `could not complete`); `test_warm_segment_cache_revalidates_the_file_it_cached` directory shape (`not a regular file`) |
| W | LOW — the Windows busy set was a superset of contention: winerror 5/32/33 all land on EACCES, so a genuine access-denied re-contended until the 45 s timeout (latency only, no descriptor) | `_win32_lock_error` maps ERROR_LOCK_VIOLATION alone onto EAGAIN (winerror kept for diagnostics); every other Win32 error keeps its derived errno and fails closed; the busy set is {EAGAIN, EWOULDBLOCK} on both platforms | `test_lockfileex_refusals_classify_by_the_win32_error` (runs on POSIX too); the Windows-only contention pin keeps `winerror == 33` |
| D | LOW — doc absolutes and gaps: «cannot finish while robbed», «a hold lost anywhere abandons», «held for the whole question» (the handles open AFTER the live header read), the verbatim-restore rollback the orphan exemption admits, the mixed-tier residual omitting by-name eviction, §8 carrying the round-4 predicate claim without a marker, §5.9 contradicting itself | DESIGN §8/§10/§12, the ARCHITECTURE row, PERSISTENCE, §5.8/§5.9/§5.10/§8 of this packet rewritten as each finding asked; no code | — |
Red observed, not argued — each pin against the exact pre-fix shape or
mutation it names, on a scratch copy of this lane (pin red, fix applied or
mutation reverted, pin green):
| pin | mutation / base | red observed |
|---|---|---|
| `test_a_creator_evicted_while_lock_less_never_returns_a_descriptor` | `2dd3e017` | two descriptors returned (`[14, 15]`), `HOLDERS: 2` |
| `test_lockfileex_refusals_classify_by_the_win32_error` | `2dd3e017` | `EACCES in frozenset({11, 13})` |
| `test_a_hold_lost_after_the_last_snapshot_look_refuses_the_rename` | `2dd3e017` (beat → look → replace) | receipt returned while robbed, the charge erased |
| `test_a_hold_lost_after_the_recheck_aborts_before_the_swap` (look-count clause) | snapshot-first / beat-second (mutation M3) | `3 == 2`: the in-swap look was asked after the hold was gone |
| `test_the_epoch_anchor_scans_the_directory_the_chain_was_walked_in` | anchor opens entries by path (listing through the held fd kept) | `DID NOT RAISE UsageLedgerCorrupt` — the forged look-alike admitted; the previous pin shape passed under the same mutation |
| `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (subdirectory) | `79a1b9fb` | `anchor scan could not complete: [Errno 21] Is a directory` |
| same, FIFO half alone | `79a1b9fb` | `TimeoutError: FIFO open blocked` — the open hung until the 5 s alarm |
| `test_warm_segment_cache_revalidates_the_file_it_cached` (directory shape) | `79a1b9fb` | bare `IsADirectoryError: [Errno 21] Is a directory` from `os.read` |
| `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (event + tripwire clauses) | `79a1b9fb` | no `events.jsonl` row at all (`FileNotFoundError`); the tripwire note named no tier |
Windows tier, stated plainly: `_win32_lock_error` and the classification pin
run their errno arithmetic on POSIX too (the new pin is not skipped), but
the LockFileEx call itself and the Windows-only contention pin remain
unexecuted on this host and owed to the 3-OS CI matrix; the path-based
Windows anchor scan keeps the fail-closed rule from round 5 (no
`S_ISREG`/`O_NONBLOCK` classification there — a directory in the archive is
corruption on Windows, disclosed), and the FIFO/dangling-link pin is POSIX
(`skipif(IS_WINDOWS)`).
Size ratchet, stated plainly: `79a1b9fb` (the round-5.2 agent's last
commit before the session limit) left `tests/test_usage_compaction.py` at
1512 lines with the manifest stale — `regenerate_size_ratchet.py --check`
exit 1 at that tree, the suite silently in the 1501-1600 zone; there is no
committed-history replay on this line (`review.py`: the local surface
warns), so the linear repair `6ad110e9` stands: three verbatim scaffolding
duplicates folded in place (the raced-charge-survived assertion, the
retry-durability pin re-running the first-pass proof, the single-caller lock
probe inlined) and PEP 8 spacing, 1512 → 1461, no new abstraction, every
folded pin still red under the swap-precondition-removed mutation; the
round-5.2 pins then bring it to 1492. `ouroboros/usage_compaction.py` grows
1124 → 1158 inside its band (immutable rationale, growth recorded here and
in the ledger); `ouroboros/platform_layer.py` 1499 and
`ouroboros/agent_startup_checks.py` 1490 stay inside theirs.
Round-5.2 code commits (author `ouroboros-agent`, single-intent): `847a1151`
(fold of the lock family's try/except-pass into `contextlib.suppress`, no
behaviour change), `7923e624` (finding 1), `f2b118a4` (finding W),
`ff6bb399` (one snapshot-look recorder for the hold/append pins),
`79a1b9fb` (finding 3), `6ad110e9` (the suite fold), `503a0dd6` (finding 5
and its LOW siblings), `95a53ad2` (finding 4), `208fe5ac` (finding 2); the
docs commit follows. Gate evidence: `docs/v7next/LEDGER_CORRECTIONS.md`
§"From the C6 fix-round 5.2 (base 2dd3e017)".
### Round 5.3 — adversarial lenses over round 5.2 (fix-round base `5e4829e3`)
Verdict of the lenses (independent read of `5e4829e3`, PoCs executed against
this lane's own code): six HIGH/MEDIUM findings open, seven LOW. **All
thirteen accepted**; twelve are fixed in code with red-first pins, one — the
recycled-pid wedge — is closed by the disclosure the finding itself offered
as its alternative (below). Nothing was argued away. Every code fix carries a
pin observed RED against the exact pre-fix shape or mutation it names.
The round ran in two halves: the first fix agent hit its session limit after
`cbfd23ce` with the docs staged and the ledger section unwritten; the resumed
half re-observed every red below in a scratch copy of this worktree before
changing anything, kept all ten commits, and closed two residues of the
round's own fixes (3b and 4b below).
| # | finding | fix | red-first pin |
|---|---|---|---|
| 1 | HIGH — `_lock_identity` answers `()` for a descriptor it cannot `fstat` (ESTALE/EIO — the network filesystems this tier exists for), and the acquisition compared the two identities RAW: with the path momentarily absent (a reclaimer's own unlink→re-create window) `() == ()` was vacuously true and a descriptor for an unlinked inode was returned as the monetary lock — `HOLDERS: 2`, and the ordinary append transaction never heartbeats. Second half: with the path present but the fd unstatable the bare `os.close` left our file stamped with our LIVE pid, which an owner-aware reclaimer may never evict — the lock wedged for the life of the process | the won lock is returned only when its own identity READS and matches; an unreadable one fails closed (`return None`, warning) and takes our stamp off the path when the bytes there are still exactly the ones we wrote. The stamp is captured once, at the write. The module's four other identity comparisons already guarded for the empty answer; `:268` was the outlier this round's own delta introduced | `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` (`tests/test_lockfile_helpers.py`; an fd-blind `_lock_identity` plus an evicting flock) |
| 2 | MEDIUM — the RATIFIED design note still called EACCES a contention code, the negation of the code, of round 5.2's own pin and of §5.10; implementing the note re-opens finding W (a genuine access-denied re-contending for the whole 45 s monetary timeout). The unsupported set was named three-of-four in three places | DESIGN §8 states both sets exactly (`EAGAIN`/`EWOULDBLOCK`; `ENOLCK`/`EOPNOTSUPP`/`ENOTSUP`/`ENOSYS`) with the Win32 answers that map onto them, and a pin compares the note's spelled sets with the code's, by number (EWOULDBLOCK/ENOTSUP are aliases on Linux, not everywhere) | `test_the_design_note_names_the_exact_kernel_refusal_sets` |
| 3 | MEDIUM — `heartbeat` defaulted to `None` and `_beat` returned at once on it, so a pass entered without one swapped the monetary authority with NO ownership check at all; the single production wire was unpinned (the reserve-path pin looked only at the lock, never at the kwargs), and deleting it (MUT-U) left the whole battery green | both entry points take `heartbeat` as a required keyword and `_beat` has no `None` case — a dropped wire is a TypeError at the call, not a silent no-op — and the reserve-path pin asserts the callable it is handed | `test_reserve_path_compacts_only_past_config_threshold` (`assert callable(kwargs["heartbeat"])`), red under MUT-U |
| 3b | LOW, own residue of 3 (resumed half) — the required keyword closes the dropped wire, but a caller passing `heartbeat=None` was unpinned: `_beat(None)` fails at the call and that failure IS the existing "answer we cannot get at all" abort, so the pass is refused — unproven by any pin | pinned beside the False and the raising heartbeats: `None` answers `None`, the ledger stays byte-identical, no orphan is written | `test_a_lost_lock_aborts_the_pass_instead_of_swapping` (`None` clause), red on the pre-finding-3 module |
| 4 | HIGH — the orphan exemption decided on ONE row: a segment whose leading row equalled the live header was an uncommitted orphan. The newest segment IS the previous generation's whole file, so a ledger restored from a backup taken just after that compaction satisfied it while being a strict SUBSET of the exempted segment — the attempts that pass folded exist nowhere else, and the join reported them absent (PoC: 4 of 8 ids hidden, no corruption raised, both segments on disk) | an orphan is the pre-swap COPY of the live file and the live file only grows behind it, so its bytes are still a PREFIX of it — that is the test, and it needs no live-id parse. A restored generation carries rows past the end of the file it was restored from | `test_a_restored_previous_generation_is_out_anchored_not_taken_for_an_orphan[stamped]` |
| 5 | MEDIUM — with the stamp itself gone (a pre-compaction backup restored) the anchor never ran: every gate sat behind `live_header is not None`, so `archived_attempt_ids` answered `frozenset()` having touched the archive zero times, and `_live_baseline_header`'s docstring claimed `None` "means exactly one thing" | the anchor runs either way, with the floor at epoch zero; a data root with no archive directory and no stamp still answers empty at once; the docstring names both states and points at the archive as the thing that tells them apart | `…[unstamped]` (same pin) |
| 4b | LOW, own residue of 4 (resumed half) — the prefix proof re-opened the entry by NAME after classifying it, so the bytes compared against the live file came from a second open: an entry swapped in between (an empty file; a writer-less FIFO under `O_NONBLOCK`) read as zero bytes and passed the anchor although the segment claiming the newer generation had just been read — the same power as deleting that segment before the scan (disclosed), but one open more than the proof needs | one open per entry: classify, parse the leading row and — when it claims a newer generation — `lseek` to the start and compare from the same descriptor (six lines fewer) | `test_a_restored_previous_generation_is_out_anchored_not_taken_for_an_orphan` (a second open of any name answers an empty file; the verdict must still be reached), red on the two-open shape |
| 6 | MEDIUM — `_archive_dir_fds` wrapped every open below the root and left the ROOT's own outside the `try`; `archived_attempt_ids` calls it with no handler, so an unreadable data root (permissions, EMFILE/ENFILE) left a bare `OSError` on the join surface — the class round 5.2 closed one function away | the root open is inside the `try` and typed `usage archive root is not readable` | `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (chmod `0o111` half) |
| L1 | LOW — the name-tier root was marked BEFORE the append, so one transient failure (ENOSPC, an unwritable `logs/`) turned the durable typed event back into a log line for the process's life, and the 20 MB tripwire then names an event that does not exist; the key was the unresolved path while the sibling growth guard resolves (two spellings of one root on this workspace) | the mark follows the row that landed; both maps key on the resolved root | `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (failing append, then a landing one; then the same root through a symlink) |
| L2 | LOW — a lock whose owner died and whose pid was REUSED is never reclaimed (POSIX `kill(0)` answers EPERM for another user's process on this shared host), although the enforced tier's probe flock would settle it; `PERSISTENCE.md` still called these locks self-healing *(correction, round 5.4: the mechanism was misstated — `pid_is_alive` read EPERM as DEAD, so another user's recycle WAS reclaimed through the age path and only a same-uid recycle wedged; EPERM reads alive since round 5.4 and the disclosure names the real wedge — §9 "Round 5.4", R7)* | **disclosed, not changed** — the finding's own alternative. Taking the probe flock whenever the file is aged would also evict a LIVE holder of a mixed-tier install (the name-tier process holds no flock), trading a rare wedge for the two-writer class §5.10 already names; DESIGN §8, the ARCHITECTURE row and the PERSISTENCE row now state the wedge and the hand repair | — |
| L3 | LOW — the swap's own crash durability was unpinned on both sides: deleting the candidate temp's `fsync` (MUT-H) or the ledger directory's `fsync` after the rename (MUT-L) left the battery green, while the archive half carries three pins | one pin records the fsync'd inodes and the moment of the replace | `test_the_swap_fsyncs_the_candidate_before_the_rename_and_its_directory_after` |
| L4 | LOW — `_snapshot_intact` reduced to a size comparison (MUT-E) also left everything green: every intrusion the pins inject is an append | one intrusion rewrites a byte in place, changing no length | `test_a_same_size_rewrite_between_the_recheck_and_the_replace_also_refuses` |
| L5 | LOW — no LockFileEx refusal could select the name tier on Windows (CPython lands ERROR_INVALID_FUNCTION and ERROR_NOT_SUPPORTED on EINVAL), so a lock-less Windows volume failed every monetary append closed instead of degrading as disclosed; the classification pin's POSIX half asserted through errnos the function does not set | `_win32_lock_error` classifies by one table — 33 busy, 1/50 unsupported, anything else winerror-derived and fail-closed — and the classified codes carry their own errno (the 4-argument form derives errno FROM the winerror on Windows and ignores the one passed). Live evidence for exactly those two codes: LockFileEx on `\\wsl$` answers ERROR_INVALID_FUNCTION ("Incorrect function", microsoft/WSL#5762) and on a Samba share ERROR_NOT_SUPPORTED (error 50, "The network request is not supported", samba list thread "FileLockEx Problem") | `test_lockfileex_refusals_classify_by_the_win32_error` (1/50 must land in the unsupported set; 5/32/6 in neither) |
| L6 | LOW — "a stray directory or FIFO is no segment and is skipped" held only where the dir-fd exists: without one the entry was OPENED first, which a directory refuses on Windows (every history question typed-corrupt for one operator `backup/`) and a writer-less FIFO blocks on | without a handle the classification happens BEFORE the open, and the path-based open carries `O_NONBLOCK` where the platform has one | `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption[False]` (the path shape taken on POSIX, FIFO under the SIGALRM guard) |
| L7 | LOW — `_CHAIN_UNION_CACHE` was never bounded or expired although its key changes at every compaction, and DESIGN §10 still claimed the per-question cost was "H cheap walks and ONE union" after round 5 added the anchor | the map is bounded (only the newest chain can be asked again) and §10 states the anchor's per-question cost honestly | `test_archived_id_union_is_built_once_per_chain` (bound clause) |
Red observed, not argued — each pin against the exact pre-fix shape or
mutation it names, in this worktree (pin red, fix applied or mutation
reverted, pin green). Every row was re-observed by the resumed half in a
scratch copy of this worktree — the named source reverted to its base or
mutated as named, pin red; restored, the pins green together as one control:
| pin | mutation / base | red observed |
|---|---|---|
| `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` | `5e4829e3` | `assert 14 is None` — a descriptor for an unlinked inode returned as the lock |
| same, second half | the truthiness guard applied, the stamp cleanup not | `a live pid was stamped on a lock nobody may reclaim` |
| `test_the_design_note_names_the_exact_kernel_refusal_sets` | `5e4829e3`'s design note | `assert 0 == 2` — the note spelled no set at all (and called EACCES contention) |
| `test_reserve_path_compacts_only_past_config_threshold` | MUT-U (`, heartbeat=ledger_lock` deleted at `usage_accounting.py:706`) | `the pass was entered without the lock's heartbeat` (`{'heartbeat': None}`); without the pin that mutation left 136 passed, EXIT=0 |
| `test_a_restored_previous_generation_is_out_anchored_not_taken_for_an_orphan[stamped]` / `[unstamped]` | `5e4829e3` | `DID NOT RAISE UsageLedgerCorrupt` in both; the standalone PoC measured 4 of 8 ids hidden with both segments still on disk |
| `test_an_archive_entry_the_anchor_cannot_open_is_typed_corruption` (chmod half) | `5e4829e3` | bare `PermissionError` from `_archive_dir_fds`' root open |
| same, `[False]` (no dir-fd) | `5e4829e3` | `TimeoutError: FIFO open blocked` — the path-based open hung until the 5 s alarm |
| `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (event clauses) | `5e4829e3` | `FileNotFoundError: …/logs/events.jsonl` — one failed append suppressed the event for good |
| `test_the_swap_fsyncs_the_candidate_before_the_rename_and_its_directory_after` | MUT-H (temp `fsync` deleted) | the candidate's inode absent from the fsyncs before the rename |
| same | MUT-L (`_fsync_dir(path.parent)` deleted) | the ledger's directory absent from the fsyncs after it |
| `test_a_same_size_rewrite_between_the_recheck_and_the_replace_also_refuses` | MUT-E (`_snapshot_intact` by size only) | a receipt returned: the swap landed over the rewritten row |
| `test_lockfileex_refusals_classify_by_the_win32_error` (unsupported clause) | `5e4829e3` | `assert (0 in frozenset({37, 38, 95}))` for winerror 1 |
| `test_archived_id_union_is_built_once_per_chain` (bound clause) | `5e4829e3` | `AttributeError: … has no attribute '_CHAIN_UNION_CACHE_MAX'` |
| `test_a_lost_lock_aborts_the_pass_instead_of_swapping` (`None` clause) | `usage_compaction.py` @ `c71a36ea^` (heartbeat defaulted, `None` skipped) | a receipt returned: `None` skipped every proof |
| `test_a_restored_previous_generation_…` (second-open clause) | `cbfd23ce` (the two-open anchor) | `DID NOT RAISE UsageLedgerCorrupt`, both parametrizations |
**Size ratchet — stated plainly, and an owner decision is now owed.**
`tests/test_usage_compaction.py` LEAVES the 1001-1500 band this round,
1492 → 1597, three lines under the 1600 HARD cap (the 1501-1600 zone is
ungated; above it the ratchet refuses new debt outright). Every commit of the
round carries a manifest that matches its own tree — the crossing commit
regenerates it — so the pairwise base-vs-tip lane is green at the tip and at
each parent, not only in the official CI shape.
The round asked for eight new or extended pins on the monetary authority; two
of them (MUT-H/MUT-L, MUT-E) close mutations that had survived the entire
battery. The ways to stay inside the band were: delete contract-bearing pins;
fold the five distinct `hold lost at X` pins into one table, merging the
per-moment reasoning each docstring carries; or add a neighbour suite — which
this lane's own band rationale rules out ("as one suite") and the owner's
standing rule forbids as payment for a cap. None was taken. What was paid
honestly: the retry-durability pin now calls the fsync-failure pin instead of
re-implementing it verbatim (−11); this round's own docstrings are compressed
with every claim kept and one no-cover `fstat` guard is gone (−6); the resumed half added six lines (the `None` clause, the second-open clause) and compressed the same docstrings once more (−4).
`ouroboros/platform_layer.py` stays at exactly 1500 — the two kill-tree
sweeps reuse the module's own `force_kill_pid` (−17) and `unlink_lockfile`
lost its `exists()`-then-unlink race (−2), which paid for the identity guard
and the Win32 table. `ouroboros/usage_compaction.py` 1158 → 1197, inside its
band (rationale immutable between adjacent manifests; growth recorded here
and in the ledger).
**The owner decision:** at 1597 the suite has three lines of headroom against
a cap that refuses new debt, so the NEXT pin on this surface cannot land
without one of — (a) splitting the CPL-5 join/history-reader pins into their
own suite (a real seam: a different module surface, a different consumer, its
own reason to change) against the recorded "as one suite" rationale, (b) an
authorized rebase of the ratchet baseline, or (c) accepting fewer pins on the
monetary authority. This round does not choose.
Round-5.3 code commits (author `ouroboros-agent`, single-intent): `7d134fd8`
(the kill-tree fold, no behaviour change), `7e6b935e` (finding 1), `f7b8a578`
(finding L5), `c71a36ea` (finding 3), `e08a0392` (findings 4 and 5),
`82250a45` (finding 6), `023b2e84` (finding L1), `232500f4` (finding L6),
`c5fa1ac7` (findings L3 and L4), `cbfd23ce` (finding L7, and the band crossing it pays for); resumed half: `48f7b115` (finding 3b), `72d17f51` (finding 4b); the docs commit —
DESIGN §8/§10/§12, the ARCHITECTURE row, PERSISTENCE, this section and the
ledger, with the design-note pin — follows. Gate evidence:
`docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 fix-round 5.3 (base
`5e4829e3`)".
### Round 5.4 — owner-bounded micro-round (base `096437c2`, owner batch №12, answer A)
Scope fixed by the owner: the residual list left by the Fable lenses over
round 5.3 and the independent gpt-5.6-sol read-only review — eight items, no
new exploration, no redesign. **All eight disposed**: seven changed in code
with red-first pins, one (R8 c–f) closed by the disclosures it asked for.
Every behaviour fix on the monetary path was observed RED on the pre-fix
shape before the fix landed (table below), then green with it.
| # | residual | disposition | fix | red-first pin |
|---|---|---|---|---|
| R1 | HIGH — `ENOLCK` sat in the unsupported set: "no locks available" is a missing lock daemon OR an exhausted kernel lock table, not the kernel saying this filesystem cannot, yet it selected the name tier — where the round-3 race returns; the per-directory tier cache was read and written with no synchronisation, so two first threads could run two probes and disagree | **fixed** | `_LOCK_UNSUPPORTED_ERRNOS` is exactly `EOPNOTSUPP`/`ENOTSUP`/`ENOSYS` (winerror 1 → `ENOSYS`, 50 → `EOPNOTSUPP`); ENOLCK keeps the enforced tier and a live acquisition the kernel refuses with it fails closed — no descriptor, our own file removed, no name protocol — so a lockd-less NFS refuses every monetary write typed (`UsageAccountingError`, the round-3 no-unlocked-fallback pin) and the pass is never entered; `_KERNEL_LOCK_TIER_LOCK` makes the probe single-flight: one probe, one verdict per directory per process. DESIGN §8, the ARCHITECTURE row and §5.10 spell the set (the design-note pin compares it by number) | `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause; name tier now selected by EOPNOTSUPP), `test_enolck_keeps_the_enforced_tier_and_the_acquisition_fails_closed`, `test_two_threads_racing_the_first_probe_run_one_probe_and_read_one_tier` |
| R2 | MEDIUM — the root was marked "already told" whether or not `append_jsonl` landed the refusal row; the helper reports exhausted retries as `False`, not an exception, so one transient failure silenced the durable event for the process's life | **fixed** | the mark follows a `True` answer only; the failed append is logged by the helper and retried at the next refusal | `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (a False-returning append between the raising and the landing one) |
| R3 | MEDIUM — the stamp-less fast path (`Path.is_dir()`) bypassed the typed root open: a regular file where the archive directory belongs answered a silent `frozenset()`, an uninspectable archive a bare `OSError` | **fixed** | before any compaction the question ends early only on the kernel's exact `ENOENT`; a non-directory or an uninspectable archive is `UsageLedgerCorrupt`; every case that reads anything then goes through `_archive_dir_fds`' typed root open as before (a plain `os.stat` classification first, because the Windows path shape has no dir-fd to route through) | `test_a_stamp_less_ledger_still_inspects_its_archive_fail_closed` |
| R4 | MEDIUM — bare `OSError` still escaped `archived_attempt_ids` through `pathlib`'s `is_symlink()` in `_archive_dir_bounded` (both levels) and `_segment_path` (the named segment): `pathlib` re-raises everything but ENOENT/ENOTDIR/EBADF/ELOOP, and an `archive/usage_ledger` readable but not searchable (a `chmod -R 600 data/` hardening) refused the segment's own `lstat` | **fixed, as a class** | both inspections wrap their `OSError` into `UsageLedgerCorrupt` ("cannot be inspected"), the same rule the opens follow since round 5.3 | `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` (the real chmod-600 shape, then `Path.is_symlink` raising `PermissionError`) |
| R5 | MEDIUM — DESIGN §8/§12.9, §5.8 here and the round-5.2 ledger line said a charge landed between the last proof and the rename is "erased, then surfaced by the post-swap re-read or quarantined at the next read"; neither could see it — the re-read compares the NEW inode against the candidate, the archive segment is the pre-row snapshot — so the loss was SILENT and a success receipt was returned | **fixed (POSIX) + docs corrected** | `_swap_ledger_fsync` holds the OLD inode open across the rename (the only witness left) and reads back whatever landed beyond the proven snapshot's length AFTER the fact: those bytes go to `state/usage_attempts.quarantine.jsonl` (`raw_base64`, the torn-tail shape, which flips `integrity_degraded`) and the pass raises `UsageLedgerCorrupt` instead of returning a receipt — never re-appended (`seq` belongs to the live file). Detected by size; Windows cannot hold the destination open through `os.replace` and stays a disclosed silent loss. The trigger's failure log no longer claims the ledger is uncompacted after such a raise | `test_a_swap_that_did_not_land_is_a_typed_failure_not_a_receipt[erased]` (the round-3 `[written_over]` variant unchanged) |
| R6 | MEDIUM — the production heartbeat wire was pinned as `callable(...)`: a constant-True stub (M9) survived the whole battery, every ownership proof a no-op | **fixed (pin)** | the reserve-path pin ages the lock file to the epoch, calls the heartbeat it is handed and requires `True` AND a renewed mtime — judged outside the contained call, so the red names the stub | `test_reserve_path_compacts_only_past_config_threshold` |
| R7 | MEDIUM — the recycled-pid disclosure named the wrong mechanism: `pid_is_alive` folded EPERM into "dead", so another user's recycled pid went down the age-eviction path (flock-guarded on the enforced tier) and only a same-uid recycle wedged — while DESIGN §8, PERSISTENCE and the round-5.3 L2 row said EPERM read alive | **fixed + disclosure corrected** | EPERM (the process EXISTS) reads alive; only ESRCH is dead; anything undeterminable reads present, as Windows already did — so `pid_provably_gone` is the exact negation of `pid_is_alive` and became one line (the reaper's docstring corrected with it). The real residual, stated in DESIGN §8, §5.10, ARCHITECTURE and PERSISTENCE: any live impostor — same uid or another — wedges the lock from the 90 s staleness window (`usage_ledger._locked`, `stale_sec=90.0`, a literal there, not a `config.py` constant) until it exits; the probe flock is deliberately not consulted while the pid reads alive (a mixed-tier name-tier holder has none) | `test_a_pid_that_refuses_our_signal_is_alive_and_its_lock_is_not_reclaimed` |
| R8a | LOW — `_build_candidate` defaulted `beat` to a no-op inside the monetary path | **fixed** | `beat` is required; omitting it is a `TypeError` at the call, contained by the pass, which then never compacts | control: `test_the_long_build_and_verification_section_beats_the_lock` red under M10 |
| R8b | LOW — the heartbeat's own failure modes were unpinned: the `not held or` guard (the heartbeat's analog of round 5.3 finding 1) and the `False` on a refused `utime` could each be removed with both suites green | **fixed (pin)** | the identity pin gains a refresh clause: a refused renewal answers False; an unreadable own identity with the path absent is not a match of two empty answers; a stranger's file at the path is not ours | `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` (refresh clauses) |
| R8c | LOW — a compaction committing between a question's live-header read and its anchor scan yields a transient false `generation newer` (UNKNOWN) | **disclosed** (DESIGN §10, §5.9, ARCHITECTURE) | the owner's scope for this item was disclosure; the bounded single retry the lens offered is not taken this round | — |
| R8d | LOW — stamp-less anchor consequences undisclosed: a ledger reset beside a surviving archive is a permanent `generation newer` verdict; a stray JSON file is corruption on a stamp-less ledger only | **disclosed** (PERSISTENCE ledger and archive rows' Reset columns, DESIGN §10, §5.9, ARCHITECTURE) | reset both together, or keep both | — |
| R8e | LOW — "an entry that is not a regular file … is skipped" was absolute; a UNIX socket cannot be opened at all (ENXIO) and reads as corruption | **qualified** (DESIGN §10/§12.5, §5.9, ARCHITECTURE) | an entry that OPENS but is not regular is skipped; one the kernel refuses to open at all is corruption. No socket pin: `AF_UNIX` paths are capped at 108 bytes and pytest's tmp paths exceed it, and a `chdir`-relative bind leaks process state into a parallel suite — a LOW not worth that hazard | — |
| R8f | LOW — two absolutes: "a release unlinks before its close, under the still-held flock" (POSIX only: Windows closes, then re-checks and unlinks) and no mention of the contention-branch orphan (an `EAGAIN` on the creator's OWN fresh file leaves a live-pid-stamped file the creator re-contends against and no owner-aware acquirer ages out) | **disclosed** (DESIGN §8, §5.10, ARCHITECTURE) | the orphan shape has no in-protocol producer (an evictor's flock unlinks what it judged): theoretical on the enforced tier, stated | — |
Red observed, not argued — each pin against the exact pre-fix shape or
mutation it names, in this worktree (pin red, fix applied or mutation
reverted, pin green):
| pin | mutation / pre-fix shape | red observed |
|---|---|---|
| `test_a_pid_that_refuses_our_signal_is_alive_and_its_lock_is_not_reclaimed` | `platform_layer.py` @ `bd9e99a4` (EPERM folded into dead) | `assert (False is True)` on `pid_is_alive(EPERM)`; the lock clause alone: the aged lock evicted, fd 3 returned, the file re-stamped with our pid |
| `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause) | `platform_layer.py` @ `01c89685` (ENOLCK in the unsupported set) | `AssertionError: 37` — errno 37 selected the name tier (`False is True`) |
| `test_enolck_keeps_the_enforced_tier_and_the_acquisition_fails_closed` | same | `enforced = False`; a descriptor (fd 3) returned on the name tier, the lock file present, no kernel call made |
| `test_two_threads_racing_the_first_probe_run_one_probe_and_read_one_tier` | same (no cache lock) | `2 == 1`: both threads ran a probe |
| `test_the_pass_refuses_on_the_name_tier_while_appends_continue` (False-returning append) | `usage_compaction.py` @ `7ce7e83d` (mark regardless of the return value) | `FileNotFoundError: …/logs/events.jsonl` — the False answer marked the root, no row ever landed |
| `test_a_stamp_less_ledger_still_inspects_its_archive_fail_closed` | `usage_compaction.py` @ `b9c43911` (`is_dir()` fast path) | regular file: `DID NOT RAISE`, answered `frozenset()`; `archive/` chmod 000: bare `PermissionError` |
| `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` | same (bare `is_symlink()`) | `usage_ledger` chmod 600: bare `PermissionError: [Errno 13] … segment_ep0001_….jsonl` from the segment's own lstat; `Path.is_symlink` raising: bare `PermissionError` |
| `test_a_swap_that_did_not_land_is_a_typed_failure_not_a_receipt[erased]` | `usage_compaction.py` @ `d99ff6a9` (no old-inode witness) | `DID NOT RAISE`; standalone: receipt returned, the charge gone from the ledger, no quarantine file, `integrity_degraded` False |
| `test_reserve_path_compacts_only_past_config_threshold` (heartbeat clause) | M9: `heartbeat=lambda: True` at the production wire | `a stub, not the held lock's heartbeat` (`[False] == [True]`); the other 84 items of the two suites green under the same mutation |
| `test_a_lock_whose_identity_cannot_be_read_is_never_a_hold` (refresh clauses) | M15: the `not held or` guard removed from `refresh_exclusive_file_lock` | `assert True is False`: the blind descriptor renewed with the path absent |
| same | M16: a refused `utime` answers True | `assert True is False` |
| `test_the_long_build_and_verification_section_beats_the_lock` (control, R8a) | M10: `beat` dropped at the `_build_candidate` call | `TypeError: _build_candidate() missing 1 required positional argument: 'beat'`, contained by the pass → `assert None is not None` |
Disclosed, not fixed (with the reason each time):
1. **R1's consequence.** An install whose `state/` answers ENOLCK
persistently (bare NFS without lockd) now refuses every monetary write
closed — `UsageAccountingError` at the writer, one warning per attempt
naming errno 37 — where round 5 ran the name protocol there. That is the
owner's decision (fail closed, no name tier); the repair is a filesystem
that locks. "Compaction refuses with a typed reason" is structural, not a
new code path: with no lock the pass is never entered, and the round-3 pin
`test_every_ledger_writer_refuses_when_the_lock_cannot_be_taken` is the
typed refusal. sol's further suggestion — binding the established tier to
the returned hold and passing that attestation to the compactor — is not
taken: the module lock leaves one verdict per directory per process, which
is the disagreement the attestation would have caught; a mixed-tier
install ACROSS processes stays the round-5.2 disclosure.
2. **R2 and fsync.** No `events.jsonl` row is fsync'd, this one included;
the mark is per-process memory that dies with the same crash that could
lose an un-fsync'd row, so a new process re-tells. The residual — a
delayed writeback error with the process alive (row lost, mark standing)
— is the same for every event row and is not closed here.
3. **R5's bounds.** Detection is by size: a same-size in-place rewrite of
the old inode inside the rename syscall is not a landed charge and is
not seen. The erased bytes are preserved and flagged, never re-appended
(a hand repair from the quarantine row). Windows: silent, disclosed.
4. **R7's trade.** The wedge now covers another-uid recycles too (they
were reclaimed through the flock-guarded age path before); the
alternative — the probe flock on any aged file — would evict a live
name-tier holder of a mixed-tier install (round 5.3 L2 stands).
5. **R8c–f** are disclosures by the owner's scope: the transient UNKNOWN
(no retry added), the reset-beside-archive verdict, the socket shape (no
pin: the 108-byte `AF_UNIX` cap and a `chdir` hazard), the contention
orphan and the POSIX-only release-under-flock.
6. **Windows** stays unexecuted on this host, as in every round.
Size ratchet, stated plainly: `ouroboros/platform_layer.py` 1500 → 1497
inside its band — `pid_provably_gone` folded to the one-line negation it now
is and the docstrings it gained reflowed, paying for `threading`, the tier
lock and the EPERM branch. `ouroboros/usage_compaction.py` 1197 → 1245
inside its band; the owner asked for the band rationale to be extended in the
same commit when the file grows, but the ratchet's own transition rule makes
a surviving rationale immutable between adjacent manifests
(`validate_manifest_transition`, "surviving band rationale is immutable"),
so — as in rounds 5, 5.2 and 5.3 — the growth is recorded here and in the
ledger instead. `tests/test_usage_compaction.py` 1597 → 1597: the round
added five new or extended pins (+67 lines) and paid with two no-behaviour
commits — a `compacted` fixture folding twenty-one verbatim seed-then-compact
preambles (−39) and argument-list/data-literal reflows within the file's line
width (−28); no claim, docstring, message or assertion was dropped, and no
neighbour suite was added. The three-line headroom under the 1600 hard cap is
what it was; the owner decision owed since round 5.3 still stands.
Round-5.4 commits (author and committer `Ouroboros`, single-intent):
`bd9e99a4` (the fixture fold, no behaviour change), `01c89685` (R7),
`7ce7e83d` (R1), `12558046` (R2), `b9c43911` (the reflow, no behaviour
change), `d99ff6a9` (R3 + R4), `ea4d4337` (R5), `9306f962` (R6),
`02338c9b` (R8a + R8b); the docs commit — DESIGN §8/§10/§12, the
ARCHITECTURE row, PERSISTENCE, this section and the ledger — follows. Gate
evidence: `docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 micro-round 5.4
(owner batch №12 A, base 096437c2)".
## 10. Round 5.4 close-out — three read-only lenses on `b4938c31`, operator disposition (owner batch №12 A)
Verdicts: 3 × NEEDS_FIXES, no HIGH; 3 MEDIUM + 7 LOW. Fixed here (base `b4938c31`), pinned red-first:
| finding | disposition | pin → pre-fix shape → observed red |
|---|---|---|
| MEDIUM R1 (two lenses) — ENOLCK fail-closed landed in the SHARED primitive: on a lockd-less NFS `state/` every `acquire_exclusive_file_lock` caller failed, no model call could dispatch; the owner decided "compaction refuses", not this | **fixed**: ENOLCK is the name tier with its errno recorded beside the verdict (`_KERNEL_LOCK_TIER[dir] = (enforced, errno)`); `acquire_exclusive_file_lock(refuse_name_tier_errnos=…)` lets a caller fail closed on a recorded errno; only `usage_ledger._named_lock` names ENOLCK. Ordinary locks keep the name protocol they always ran there; money refuses typed | `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause) and `test_enolck_is_the_name_tier_for_ordinary_locks_and_a_typed_refusal_for_money` → `platform_layer.py`/`usage_ledger.py` @ `b4938c31` → `assert True == (True, 5)` (a bare bool cached, ENOLCK enforced) / `assert True is False` |
| MEDIUM R4 — `_segment_path` resolved with `Path.resolve(strict=False)` one line BEFORE the typed `is_symlink()`: a symlink loop escaped as `RuntimeError("Symlink loop …")`, a readlink race as bare `OSError` | **fixed**: `os.path.realpath` (non-strict, never raises on a loop) inside the same `try`, `except (OSError, RuntimeError)` → `UsageLedgerCorrupt` | `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` (self-loop clause) → `usage_compaction.py` @ `b4938c31` → `RuntimeError: Symlink loop from …` and `OSError: [Errno 40] Too many levels of symbolic links` |
| LOW R3 — the stamp-less ENOENT exemption used a FOLLOWING `stat`: a dangling link at either archive level answered a silent empty set where the stamped reader answers corruption | **fixed**: `lstat` both levels first; `S_ISLNK` → typed `usage archive path is a symlink`, other `OSError` → typed `cannot be inspected`; pin deferred (the compaction suite sits at its 1600-line cap, disclosed below); mutation-verified by hand on this host (dangling link at `archive/` → typed) | — |
| LOW R5 — the old-inode witness was opened by PATH before the proof and not tied to the inode the precondition proved; a vanished ledger at the witness open was a bare `OSError` | **fixed**: `owned_and_intact` also proves `fstat(old_fd)` and `stat(path)` name one inode; the witness open is wrapped into `_Abort` (an abort by policy) | — (behaviour-preserving strengthening; no pin, disclosed) |
| LOW R1 — DESIGN §8 "decides once … cached" was absolute; an unprobeable directory answers enforced UNCACHED | **docs**: DESIGN §8, packet §5.10/§9, ARCHITECTURE row | — |
| LOW R6 — the strengthened heartbeat pin proves renewal + True, not ownership: a lock-TOUCHING stub survives it | **disclosed, not fixed**: the pin proves the callable renews THIS lock file's age (the production wire's only observable) — a stub that touches the production lock path is a contrived mutation; the suite is at its line cap | — |
| LOW R7 — EPERM→alive is a flip of a primitive shared by 12 non-test consumers, disclosed only for the monetary lock | **docs**: DESIGN §8 residual, packet §9 R7, ARCHITECTURE/PERSISTENCE wording name the shared primitive and the consumers that now defer | — |
| LOW R8d — "PERMANENT … for the life of the install" over-stated: the verdict lasts until the fresh ledger's epoch passes the surviving segments, which are then silently ignored | **docs**: DESIGN §10, packet §5.9, ARCHITECTURE row | — |
| LOW R8e — the socket qualification introduced its own absolute: a UNIX socket is corruption on the dir-fd shape only; the path shape's stat-before-open skips it | **docs**: DESIGN §10/§12.5, packet §5.9, ARCHITECTURE row | — |
Sizes after the close-out: `platform_layer.py` 1500/1500 (band ceiling; net +3 on the policy, paid by rewrapping two prose blocks — no contract text dropped), `usage_compaction.py` 1262 (band), `tests/test_usage_compaction.py` **1600/1600** (the owner answered this in batch №13 item 11 = A: the archive-reader tests moved to their own module — the natural organ boundary, `archived_attempt_ids` vs the pass. After the split, the suite is `tests/test_usage_compaction.py` 900 + `tests/test_usage_compaction_archive.py` 660 + `tests/fixtures_usage_compaction.py` 123, same 64 node ids), `tests/test_lockfile_helpers.py` 568.
### §10 addendum — the Windows matrix (run 33654743857 on bf8b6549)
The lane never ran on Windows (not pushed until integrated). The first 3-OS matrix
after the merge was red on windows-latest only, in one class plus two test shapes:
- **Class (product):** the `LockFileEx` tier held a MANDATORY byte-range lock on the
lock file, so a contender's `_lock_identity(probe)` read was refused and it could
never judge the hold — `test_concurrent_writers_keep_monotonic_sequence` («usage
accounting lock unavailable»), four `update_json_locked` timeouts, one lost
concurrent chat append. **Disposition:** `kernel_file_locks_enforced` answers
False on Windows — 7.0 ships Windows on the name tier it always ran (compaction
refuses there, typed and disclosed); the tier code stays for the post-release
re-enable with a stamp-safe byte range and a Windows-executed pin.
**Correction (stage-2 delta review, lens e2e-and-ci; run 33663258606 on `35b82db0`):**
the mandatory byte-range lock explained the bf8b6549 leg only; the same two tests
(`test_concurrent_writers_keep_monotonic_sequence`,
`test_terminal_projection_dedup_does_not_lose_concurrent_chat_append`) stayed red on
every name-tier leg after it, because the name tier is NOT «the protocol it always
ran»: since round 3 a contender opens the lock on every poll to read identity and
owner stamp, and on Windows (CPython opens without FILE_SHARE_DELETE) that handle
makes the owner's release unlink fail with a sharing violation — swallowed at debug,
the lock is orphaned with the owner's LIVE pid, which no owner-aware acquirer evicts:
the monetary lock refuses every later writer until restart, `append_jsonl` waits its
2 s and lands unlocked (non-atomic append on Windows → lost rows). Reproduced on Linux
by the verifier's delete-semantics simulator (1 refusal → orphan → 120 timeouts in
20 s). **Fix:** `_unlink_lock_path` retries a transient Windows refusal for a bounded
window at release and in `unlink_lockfile` (simulator: 288 refusals absorbed, 70 238
acquisitions, no orphan); red-first pins
`test_windows_release_retries_a_contenders_transient_sharing_refusal`,
`test_windows_release_gives_up_a_refusal_that_never_clears`,
`test_posix_release_does_not_retry_a_permission_refusal`. Verified by the matrix on
the SHA carrying the fix (see LEDGER «From the Windows CI matrix on 35b82db0»).
**Re-enabled in 7.0 by the Windows kernel-tier lane (commit `eb3ba7a1`), owner batch
№13 item 1 = B: 7.0 does not ship until the kernel tier works.** The disposition above
stands as history; what changed is the byte range. `_win32_lock` now holds ONE byte at
`platform_layer._WIN32_LOCK_OFFSET` (`0x7FFFFFFF00000000`) instead of the whole file, so
the stamp bytes [0, 512) a contender must read are outside every locked range;
`kernel_file_locks_enforced` probes on Windows like POSIX and the compaction pass runs
there (`tests/test_usage_compaction.py`'s `data_root` no longer skips). Windows eviction
takes the same probe lock and unlinks after closing it — a WEAKER guarantee than POSIX's
«at most one may evict», stated as such in DESIGN §8: the loser's unlink is refused by
the winner's open handle, not by the kernel. Release order is unlock → close → unlink.
Linux-side pins (`tests/test_lockfile_helpers.py`): the range constant and its two
wrappers, an emulated LockFileEx refusing the same range while a contender still reads
the stamp, eviction only under the probe hold, and the release order read off the fd's
own liveness; plus the delete-semantics simulator (43 317 acquisitions in 20 s, 1 281
sharing violations absorbed, no orphan). The Windows-EXECUTED proof is the next CI
matrix — see LEDGER «From the Windows kernel-tier lane (owner 1 = B)».
- **Test shape (lane pins, POSIX protocol):** five lock-ownership pins unlink or
rewrite a HELD lock file (impossible on Windows) and two swap pins assert
directory fsync/inode identity — `skipif(IS_WINDOWS)` with the reason stated;
`test_warm_segment_cache_revalidates_the_file_it_cached` accepts the path
shape's typed refusal text.
- **Bystander:** `kill_process_on_port`'s POSIX branch, now routed through
`force_kill_pid`, spelled `signal.SIGKILL`, which Windows lacks — the port-sweep
tests drive that branch with `IS_WINDOWS` patched False; spelled portably.

View file

@ -1,59 +0,0 @@
# F3.3 design note — RC auditor machine-readable scope (ABI-7b, F13)
The RC auditor is the migration-window instrument of Q6=A: a command that
scans a THIRD-PARTY install (skill manifests + settings document) and names
every ABI-7.0 incompatibility with its migration, before the owner upgrades.
It runs LAST in F3 (serial tail): its scope is the UNION of the FROZEN final
inventories of every F3 lane, so it cannot be built before they land.
## Scope schema (machine-readable, one JSON document)
```json
{
"abi": "7.0",
"sources": {"tree": "<sha>", "inventories_frozen_at": "<sha>"},
"checks": [
{"id": "gateway-alias", "surface": "...", "removed": "...", "replacement": "...", "migration": "..."},
{"id": "retired-setting", "key": "...", "since": "7.0", "behavior": "stripped-on-load", "migration": "..."},
{"id": "comma-list", "key": "...", "replacement": "reviewer slots", "migration": "move config to slots BEFORE upgrade"},
{"id": "plugin-api", "requirement": "manifest plugin_api field", "grandfather": "hash-bound PASS", "migration": "..."},
{"id": "schema-stamp", "entity": "task_results", "consequence": "pre-7.0 history quarantined (Q8=B, BY DESIGN)"}
]
}
```
Feeder inventories (each lane freezes its list as data, not prose):
- ABI-3: the per-alias inventory (F11 axes: ingress/egress/JS/producer/
stored/migration/removal-test) — five gateway aliases.
- ABI-5: the Q10-retired keys (`OUROBOROS_SCOPE_REVIEW_FLOOR` in
`RETIRED_SETTING_KEYS`; removed knobs `until_deadline`,
`stall_rounds_threshold`; removed `fail_tasks` has no install-visible key —
it is named only in the report prose).
- ABI-10: comma-list keys retired to `RETIRED_SETTING_KEYS`
(exact list snapped from `settings_defaults.py` at execution time).
- ABI-1: plugin_api admission facts (absent field ≡ LEGACY "1.3";
new-PASS admission predicate; hash-bound grandfather).
- ABI-2: `_schema_version=1` stamps; the auditor MUST name the Q8=B
consequence: pre-7.0 task-result history is quarantined after upgrade,
deliberately (no converter exists).
## Behavior
- Read-only over the audited install; never mutates it. Output: typed report
(JSON + human rendering), exit 0 = clean, 1 = incompatibilities found,
2 = install unreadable or the audit itself failed (traversal/report-write
OSError; PYTHONPYCACHEPREFIX inside the audited root without startup
bytecode suppression). A mandatory source the audit cannot read/parse is a
BLOCKING `unauditable-source` finding (exit 1, an audit-integrity plane
outside the five scope classes) — never a silent exit 0.
- N−1 fixtures (F14, shared with ABI-2/ABI-7a): a settings document and a
skill manifest authored by the previous minor run through the auditor as
test fixtures — real bytes, not synthetic shapes.
- Everything not machine-checkable stays an owner-attestation LIST the
auditor prints (F13 decision) — no pretend-coverage.
## Verification hook
RC audit fixture suite (new, F13/F14) — named in the ADOPTION ABI-7 row;
the auditor script lands under `scripts/` in F3.3.

View file

@ -1,48 +0,0 @@
# F3.2 seam design note — ResolvedModelTarget (ABI-4)
Greenfield §6-design: zero occurrences on tip and in the oracle — this is NOT
a transplant. Owner decision: plan §6 item 4 (frozen dataclass, typed
consumption by every lane). Home: the D02-owner domain — the
`model_slots.py` / `provider_models.py` seam (settings vocabulary side), so
the typed organ (lane A) must land first.
## Contract
One frozen dataclass describing a fully RESOLVED model destination — the
output of route resolution, consumed downstream without re-parsing strings:
```python
@dataclasses.dataclass(frozen=True, slots=True)
class ResolvedModelTarget:
model_id: str # exact provider model id, e.g. "anthropic/claude-..."
provider_route: str # resolved transport lane, e.g. "openrouter" | "openai-compatible" | "local"
credential_ref: str # which configured credential/profile serves the call ("" = default)
effort: str # normalized reasoning-effort label ("" when N/A)
context_window: int # tokens; 0 = unknown (fail-open per cost-unknown rule)
```
Rules:
- Frozen + slots; equality/hash by value. No Optional-by-default sprawl:
absent facts are typed sentinels ("" / 0), never None-vs-missing ambiguity.
- Constructed ONLY at the existing resolution seams; downstream code takes
the dataclass, never a raw comma/at-string. No parallel resolver: the
dataclass wraps what the current resolution already computes (reuse-first).
- No pricing fields: cost stays with the provider-route pricing SSOT
(hardcoded price tables remain banned).
## Consumers (the F3.2 sweep, after lanes A and D4 integrate)
1. `llm_fallback` candidate ladder — candidates become
`tuple[ResolvedModelTarget, ...]`.
2. `review_model_routes` / `reviewer_slot_config` — AFTER ABI-10 lands
(comma-list migration-read removed; slots are the only source).
3. Delegation lanes (delegate/claudexor route pinning) — typed target in the
run request instead of string slugs re-parsed per adapter.
## Verification hook
`tests/test_resolved_model_target.py` (new; the suite name is fixed by this
note — update the ADOPTION ABI-4 row's hook when the suite lands): frozen-ness,
value identity, construction at each seam, and a consumer sweep pin (grep-level:
no new comma/at-string parsing beside a seam that already yields the dataclass).

View file

@ -1,68 +0,0 @@
# F3.1 lane A design note — the typed tool-result organ (D02 re-derivation)
Design-note-before-code (plan §5.4 rule). Audience: the F3.1 lane A operator.
Base for every claim: `ouroboros_v7next @ db944347`; oracle: `v7_wip @ 9f691656`
(frozen). Everything below is RE-DERIVED against tip bytes — verbatim reuse of
oracle spans is forbidden (re-prove trap, ledger D15 entry 3).
## Why this lane is first in the F3.1 fan-out
ABI-4 (`ResolvedModelTarget`, D02-owner) and ABI-6(б) (`_typed_or_adapted`
branch — exists ONLY in the oracle's `loop_tool_execution.py`, zero tip hits)
both execute inside this re-derivation; the lane's protection-closure returns
the D04 remainder (registry_core/tool_result into
SAFETY_CRITICAL_PATHS/HOT_CODE_PATHS).
## Tip facts (spot-verified on db944347)
- Zero `ToolResult` occurrences on the tree; `tools/registry.py` = 2686 lines
(ToolRegistry class ~2252 of them, from line 435); `loop_tool_execution.py`
= 1390 lines.
- Oracle organs: `tools/tool_result.py` (961 lines, 33 symbols),
`tools/registry_core.py` (1139); suites `test_tool_result{,_meta_boundaries,_t46}.py`,
`test_registry_core.py`, `test_tool_classification_differential.py`,
`test_tool_execution_classification.py`, `tool_classification_corpus.py`,
`test_llm_typed_policy_refusal.py`.
## Composition (HOT-DEFERRED ledger rows; all re-derive, see f3 plan §4)
1. `registry_core.py` — D04 entry 3: rows 156/167/170/171/174/175 + 17
method→function extractions (receiver `self`→`registry`); the class does
not fit the band whole (>1500) — decompose through the extractions (Q11=B);
python_interpreter/artifacts import bindings (rows 213/214, 246/247) ride
along.
2. `tool_result.py` — D04 entry 4: closed code table, ToolResult/ToolCodeSpec;
row 139 `_compose_execute_result` is drifted — take tip bytes.
3. `extension_dispatch` typed dispatchers — D04 entry 5 (rows 187/188 +177
producer-boundary lines) + `failure_kind` from extension_process_runner
(D14 entry 10). The ABI-9 digest READ in this file is the F3.2 seam, not
this lane.
4. `loop_tool_execution` cutover — rows 157-164, 826-828: retire result-text
classification (the «D02-петля» mandatory return). ABI-6(б) resolves here:
the unreachable `_typed_or_adapted` branch is NOT reproduced.
5. `_outcome_tool_errors` T1-partition + `reflection._trace_call_errored`
(D15 entries 3-4; re-derive against upstream status handling — the naive
port INVERTS the fix) + row 166 (retire 4 CLAUDE_CODE markers, 0 emitters).
6. D09 typed-policy-refusal subfamily — D02 entry 4: rows
1706/1749/1751/1759/1760 + PROVIDER_POLICY_REFUSAL machinery in
llm_attempt, classification in loop_llm_call; pins
`test_llm_typed_policy_refusal.py` + fallback_ladder.json goldens 17→15.
7. Producer cutovers (tip==merge-base, reference typed):
core_file_tools/core_artifacts (10 producers via `_publish_tool_result`,
incl. row 332), shell_outputs 3-tuple, services.py, mcp_client,
tools/git a5e1cea3-cutover (`_publish_git_error`/`_publish_review_blocked`
+ typed `_git_status`/`_git_diff`/stage cycles), control rows
2548/2549/2556/2571/2574/2579 on the F2.1 leaves.
8. Test rows 832-833 + non-carried D04 entry 9 pins + protection-closure
(SAFETY_CRITICAL_PATHS/HOT_CODE_PATHS return, D04 entry 11).
## Boundaries
- Do NOT touch the ~60-70 str-returning tool handlers: handler-ABI conversion
is ABI-8 = POST-RELEASE (owner Q5=A + Q16=A; validator pins phase=POST).
Exactly one LegacyTextResultAdapter remains, with an inventory — the
owner-approved residual.
- ADOPTION hook for D02: `tests/test_tool_classification_differential.py` +
`tests/test_tool_result.py` (suites arrive with this lane).
- Size law Q11=B (1600 hard / band-rationale); `-m size_ratchet` before every
integration hand-off; ARCHITECTURE.md delta rides the same commit.

File diff suppressed because it is too large Load diff

View file

@ -1,274 +0,0 @@
# Projection report — oracle `MIGRATION_v7.md` rows → v7next families
One-off report, written once and not regenerated by CI. It exists because the
frozen v7 spec asked for a row-level migration ledger and a reverse checker over
it, and the owner accepted the family-level `ADOPTION_v7next.md` manifest instead
(batch №13, item 12 = A). A reader who knows the spec is owed an answer to one
question: **where did the 3901 rows of the oracle's ledger go?** This file answers
it by projection — every oracle row lands in exactly one line of the table in §2 —
and names, honestly, what the family form does not prove.
This report is a projection of a frozen artifact onto a frozen tip. It is not a
gate, nothing imports it, and it is not maintained after 7.0: re-derive it with
the commands in §5 if either side moves.
- Oracle (frozen reference, read-only): `ouroboros_v7_wip @ 9f691656`,
`MIGRATION_v7.md`.
- This tree: the v7next transplant line, `ADOPTION_v7next.md` +
`docs/v7next/LEDGER_CORRECTIONS.md`.
## 1. What the spec asked, and what the owner accepted
Spec `~/.claude/plans/OUROBOROS_V7_SPEC_v72.md`, §3.2 «Артефакты», verbatim:
> `MIGRATION_v7.md` — единственная parseable migration-таблица со схемой:
> `old path/symbol | new owner/path | facade/public contract | semantic delta |
> characterization test | upstream-transfer status/note`; CI проверяет уникального
> owner, полноту moved symbols, валидность facade/test references. Прочие списки —
> проекции.
§8.0 «Пролог», item 2, verbatim:
> Evidence: snapshots frozen contracts/PluginAPI/tool names/public imports/policy
> matrix; MIGRATION_v7.md заведён (схема §3.2); disposition всех >1000; […]
§8.5 «Формальное завершение», verbatim:
> Формальное завершение: гейты зелёные; GIANT_PATHS пуст; артефакты подписаны;
> operational evidence без undispositioned failure; frozen ABI + updater-
> совместимость доказаны; финальный ребейз проверен; ручное ревью владельца
> завершено; открытых owner-блокеров нет.
§8.4 «Human artifacts», verbatim:
> Human artifacts: MIGRATION_v7.md; persistence owner-map; domain mapping; facade
> consumer inventory (external/production/test-private); disposition всех >1000;
> ToolCodeSpec-таблица; band-манифест с rationale; move-манифесты ревью-пакетов;
> test split/delete disposition; навигационное упражнение 20 вопросов
> (качественное); синхронизированные доки.
The «полноту moved symbols» half of §3.2 is the **reverse checker**: on the oracle
branch it is `scripts/v7_migration.py::validate_migration` (its reverse arm emits
`tracked migration missing for moved/removed path`, `… missing for extracted
facade`, `… owner mismatch for extracted facade`, `… facade missing for extracted
facade`, `… missing for moved/removed symbol`), driven by
`tests/test_v7_migration_ledger.py::test_migration_table_is_valid_and_uses_only_spec_approved_pending_owners`,
whose docstring states the contract: «every extraction the v7 branch performed is a
row».
**Owner decision that accepted the family form** — requirements archive
`[A-BATCH-13-ANSWERS]`, 2026-09-02, batch №13 item 12, verbatim answer «12. A»,
recorded as:
> 12=A: family-манифест + одноразовый projection-отчёт «строки оракула → семейства».
Item 17 = B of the same batch withdrew the spec's `TEST_DISPOSITION`/nav20 artifact
(«17 = B — TEST_DISPOSITION/nav20 is withdrawn from the spec by this record»), which
is why the test-split mass in §2 is dispositioned here rather than in a separate
test-disposition artifact.
The accepted form is `ADOPTION_v7next.md`: one row per artifact-level delta family,
never per commit, validated by `scripts/v7next_adoption.py` (unique ids; all 18
required delta families present; `--release` refuses any `pending-decision`
disposition or non-`done` status, and resolves every token of every `done` row's
verification hook).
## 2. The projection table
**Grouping key = the oracle's own.** Column 4 of every oracle row carries a
semantic-delta id (`{"id":"Dnn"}`) or `{"id":"none"}` for an observable-identical
move; the 18 non-`none` ids are exactly the families
`APPROVED_SEMANTIC_DELTAS`/`REQUIRED_DELTAS` enumerate, and are exactly the 18
`kind=semantic-delta` rows of `ADOPTION_v7next.md`. The `none` mass has no id, so it
is grouped by the oracle's other structural key — the stream its `old path/symbol`
belongs to. Rows are never listed one by one.
`dest` below = «the oracle's declared new owner *module path* exists on this tip»
(`git ls-tree`). It is a coarse locator, not a verdict: a leaf renamed on this tree
(D36 `delegate_terminal` → `delegate_terminal_evidence`, owner 5.9=A; the D37
review-stack leaves) counts as absent while being fully landed. For the 18 families
the status authority is the `ADOPTION_v7next.md` row and its hook, not this column.
| oracle row family | oracle rows | this tree's family id(s) | hook test(s) | status |
|---|---|---|---|---|
| D02 typed ToolResult/ToolCodeSpec seam (§4.3.3) | 37 (33 pending + 4 retired) | `D02` | `tests/test_tool_classification_differential.py` + `tests/test_tool_result.py` | covered — `done`, re-derived whole by the F3.1 lane A (design note `docs/v7next/DESIGN_TYPED_ORGAN.md`); the handler-ABI finale is split out as `ABI-8`, deferred post-release |
| D03 settings vocabulary/read seam (§4.3.5) | 10 (9 + 1 retired) | `D03` | `tests/test_settings_read_seam.py` | covered — `done`; oracle rows 840-912, 913-917, 918-920, 1080-1081 named in the row itself, re-derived on tip bytes (closed reader inventory is 5 names, not the oracle's 6) |
| D04 retired settings knobs (§4.3.6) | 12 (7 + 5 retired) | `D04` | `tests/test_legacy_timeout_retirement.py` | covered — `done`, knob set re-checked against the tip vocabulary (owner 1B, 2026-09-01) |
| D05 safety host facts (§4.3.8) | 1 (retired) | `D05` | `tests/test_safety_policy.py` | covered — `done`, three-column outcome recorded in the row |
| D06 events taxonomy (§4.3.12) | 3 | `D06` | `tests/test_event_taxonomy.py` | covered — `done` |
| D07 Emergency Stop 2A (§4.3.11) | 1 | `D07` | `tests/test_panic_stop_port_sweep.py` (2 nodeids) + `tests/test_server_control_panic_daemon.py` + `tests/test_post_task_evolution.py` | covered — `done` |
| D08 cancellation/delegation fail-closed registries (§4.3.13) | 15 | `D08` | `tests/test_cancel_intent_corruption_s6.py` + `tests/test_subagent_worktree_registry_s6.py` + `tests/test_cancel_protocol_inventory_s6.py` | covered — `done` (frozen rows 834-839, 1083-1091) |
| D09 LLM one physical attempt per candidate (§4.3.2) | 6 | `D09` | `tests/test_context_overflow_hint.py::…one_physical_attempt` + `tests/test_llm_typed_policy_refusal.py` + `tests/test_llm_provider_golden.py` + `tests/test_multiprovider_conformance.py::…one_send_only` + `tests/test_llm_extraction.py` | partially covered — both halves landed; **residual named in the row**: the promised Ф4 «D09-invariant» E2E scenario (S24) is post-release backlog by owner batch №11 5=A |
| D11 FUNCTION_DEBT same-qualname relocation rule (§1.9/№8) | 1 | `D11` | `tests/test_repo_health_smoke.py::…relocation_but_not_a_swap` + `tests/test_smoke.py::test_size_ratchet_transition_against_explicit_base` | covered — `done` (`retain`) |
| D13 git_ops pre-init roots follow `OUROBOROS_*` (§6.4) | 2 | `D13` | `tests/test_git_ops_default_roots.py` | covered — `done` (`retain`) |
| D18 queue/pool module-handle delta (§1.9/№8) | 64 (63 + 1 retired) | `D18` | `tests/test_module_handle_extraction.py` (8 LEAVES rows × 3 parametrized invariants) + `tests/test_worker_process_extraction.py` + `tests/test_events_extraction.py::…hot_label` + `tests/test_module_handle_extraction.py::test_queue_snapshot_path_has_a_single_authority` | covered with a **disclosed direction reversal**: oracle row 1030 asked for `supervisor.state` as the snapshot owner; this tree made `supervisor.queue` the sole authority. Same defect closed, mirrored harness consequence, operator's call — disclosed in the `D18` row and in `LEDGER_CORRECTIONS.md` for the owner to overturn |
| D31 contributor-review trust boundary (§1.14-2) | 5 (1 + 4 retired) | `D31` | `tests/test_external_review_script.py` (5 D31 pins) | covered — `done`; the oracle's per-proposal classifier is retired whole, exactly as the oracle's own header says |
| D33 L-B loop module-handle delta | 76 | `D33` | `tests/test_module_handle_extraction.py` (9 `_loop` LEAVES rows × 3 invariants) + `tests/test_loop_owner_facades.py` | covered — `done` |
| D34 carrier-aware update engine (§1.9-10) | 3 | `D34` | `tests/test_update_carriers.py` + `tests/test_carrier_rebase_helper.py` + `tests/test_update_merge_owner_facade.py` + `tests/test_update_merge_assisted.py::…pins_m0` | covered — `done` |
| D35 G1 git_ops module-handle delta | 26 | `D35` | `tests/test_module_handle_extraction.py` + `tests/test_git_ops_owner_facades.py` | covered — `done` |
| D36 DEL1 delegate-family module-handle delta | 19 | `D36` | `tests/test_module_handle_extraction.py` (4 delegate LEAVES rows × 3 invariants) + `tests/test_delegate_owner_facades.py` | covered — `done`; all 19 rows read onto the tree with tip-derived declared sets, row 3467 superseded-by-upstream, one leaf renamed per owner 5.9=A |
| D37 L-C review-stack module-handle delta | 7 | `D37` | `tests/test_module_handle_extraction.py` (3 review LEAVES rows × 3 invariants) + `tests/test_review_owner_facades.py` | covered — `done`; leaf addresses diverge from the oracle's names (recorded in the row and the ledger) |
| D38 L-C2 agent-dispatch/usage module-handle delta | 2 | `D38` | `tests/test_module_handle_extraction.py` (3 LEAVES rows × 3 invariants) + `tests/test_lc2_owner_facades.py` + `tests/test_generated_inventories.py::test_facade_inventory_is_byte_identical` | covered — `done`; **disclosed residual in the row**: the usage declared set and the `post_task_synthesis` handle diverge from the frozen reference table (tip truth) |
| `none` — verbatim moves, `ouroboros/` stream | 599 (597 + 2 retired) — dest present 588 | no D-id; carried by `CPL-1` (domain manifest, module→domain 1:1 over all tracked runtime modules), `CPL-2` (gen/verify inventories incl. the facade inventory) and the per-stream extraction suites | `tests/test_generated_inventories.py` (`test_facade_inventory_is_byte_identical`, `test_every_data_layout_entry_resolves`, `test_frozen_contracts_inventory_is_byte_identical`) + `tests/test_config_extraction.py`, `test_core_extraction.py`, `test_llm_extraction.py`, `test_control_extraction.py`, `test_headless_extraction.py`, `test_review_*_extraction.py`, `test_skill_review_extraction.py`, `test_scope_review_extraction.py`, `test_extension_loader_extraction.py` | covered by class, not by row — 588/599 destinations exist by name; the 11 that diverge in address are `review_execution.py` (9), `loop_tool_execution.py` and `review.py` leaves that landed under different names on the new base, recorded in `LEDGER_CORRECTIONS.md` |
| `none` — verbatim moves, `ouroboros/tools/` stream | 491 (488 + 3 retired) — dest present 456 | as above, plus `D02`'s registry re-split | `tests/test_tool_owner_facades.py` + `tests/test_tool_access_extraction.py` + `tests/test_generated_inventories.py::test_facade_inventory_is_byte_identical` | covered by class — 456/491 destinations exist; all 35 absent are `claude_advisory_review.py` (25), `delegate.py` (7) and `registry.py` (3) leaves that landed under `D37`/`D36`/`D02` names instead |
| `none` — verbatim moves, `supervisor/` stream | 106 — dest present 90 | carried by `D18`/`D33`/`D35` mechanics + `CPL-1` | `tests/test_events_extraction.py` + `tests/test_worker_process_extraction.py` + `tests/test_module_handle_extraction.py` + `tests/test_git_extraction.py` | covered by class — 90/106; all 16 absent are `supervisor/task_lifecycle.py` leaves whose split shape differs here, and `supervisor/workers.py` remains a `GIANT_PATHS` entry (see §4) |
| `none` — verbatim moves, `server.py` stream | 47 — dest present 47 | carried by `D07` + the server extraction suite | `tests/test_server_extraction.py` + `tests/test_shell_extraction.py` | covered by class — every declared destination module exists; `server.py` itself is still a `GIANT_PATHS` entry (shrink-only debt, §4) |
| `none` — verbatim moves, `launcher.py` stream | 3 — dest present 3 | none needed | `tests/test_config_extraction.py` (launcher half of the settings vocabulary) | covered |
| `none` — verbatim moves, `tests/` stream (test split/delete) | 2070 (2067 + 3 retired) — dest present 980 | **no family row.** The spec artifact that would have dispositioned these — «test split/delete disposition» (§8.4) — was withdrawn by owner batch №13 item 17 = B | the split that DID happen is held by the size ratchet (`ouroboros/size_ratchet_manifest.py`, shrink-only) via `tests/test_smoke.py::test_size_ratchet_transition_against_explicit_base` and `scripts/regenerate_size_ratchet.py --check` | **partially covered.** 980 of the oracle's 2070 test destinations exist here. 1090 do not, and 1006 of those sit on the 15 test files this tree still carries on `GIANT_PATHS` (§4). Residual named: the oracle's test-file layout is not reproduced; the debt is carried, shrink-only, not discharged |
| `none` — verbatim moves, `web/` stream (W wave) | 170 (169 + 1 retired) — dest present 13 | **no family row**; only `R-WINWAVE` (cross-OS fix wave) touches the W stream, and not this split | `web/tests/chat_facade.test.js` (the oracle's own hook, absent here) — on this tree the carrier is `ouroboros/size_ratchet_manifest.py::GIANT_PATHS` (`web/modules/chat.js`, `web/tests/harness_accounts.test.js`) + `BAND_BASELINE_PATHS` for the six sibling web modules | **not transplanted.** 157/170 destinations absent, 148 of them on the two web giants. The oracle's `chat.js` decomposition into ~20 modules did not come across; `chat.js` stays a giant. Also inherited untransplanted: the oracle's disclosed *anonymous-source* residual (12 unnamed handler bodies with no addressable identity, listed in the oracle's header) — it has no row on either side |
| `none` — verbatim moves, `devtools/` stream | 84 — dest present 0 | **no family row** | `ouroboros/size_ratchet_manifest.py::GIANT_PATHS` (`devtools/benchmarks/osworld/run_cu_bridge_agent.py`, `run_step_agent.py`) | **not transplanted.** Both OSWorld bench runners stay giants; the oracle's split of them is not reproduced. Benchmarks are operator tooling, outside the 7.0 runtime ABI |
| `none` — verbatim moves, `skills/` stream | 41 — dest present 0 | **no family row** | `ouroboros/size_ratchet_manifest.py::GIANT_PATHS` (`skills/unix_computer_use/plugin.py`) | **not transplanted.** The oracle's split of the computer-use skill plugin is not reproduced; the file stays a giant |
## 3. Arithmetic
Every oracle row appears in exactly one line above (the grouping key is a function
of the row: its delta id if it has one, else its source stream — the two are
disjoint and exhaustive by construction).
18 delta families:
37+10+12+1+3+1+15+6+1+2+64+5+76+3+26+19+7+2 = 290
none, by stream:
ouroboros 599 + ouroboros/tools 491 + supervisor 106
+ server.py 47 + launcher.py 3 + tests 2070 + web 170
+ devtools 84 + skills 41 = 3 611
------------------------------------------------------------------------
total = 3 901
Cross-cut of the same 3901 by upstream-transfer status:
`pending` 3876 + `retired` 25 = 3901.
Cross-cut by destination presence on this tip:
present 2440 + absent 1461 = 3901; of the 1461 absent, **1282 sit on files that are
still `GIANT_PATHS` entries here** (the 23 giants listed in
`ouroboros/size_ratchet_manifest.py`), and the remaining 179 are rows whose source
file is present and non-giant but whose destination landed under a different name
(the D36/D37 renames, the `claude_advisory_review.py`, `review_execution.py`,
`task_lifecycle.py` and review-prompt-caching leaves, `chat_activity.js`) — those are covered, with the
divergence recorded lane by lane in `docs/v7next/LEDGER_CORRECTIONS.md`.
**One number that did not reconcile, and what was done about it:**
`ADOPTION_v7next.md` called the oracle ledger «(3902 rows)» from the F0 skeleton
until this lane. The oracle's own parser (`scripts/v7_migration.py::_parse_migration`:
table lines minus header minus separator) yields **3901**; 3902 is the
header-inclusive count. The manifest's Sources bullet is corrected to 3901 with the
cause named in place; every number in this report is the parser's. Nothing else
depended on the wrong figure — it appeared in prose only, in no test or script.
## 4. What the reverse checker would have proven, and what proves it here
The oracle's reverse arm (`validate_migration`, §1) proved four things by walking
the *tree* and demanding a row, not by reading the ledger:
1. **No unrowed move.** Every symbol or path that moved or was removed between
`MERGE_BASE_SHA` and HEAD has a ledger row.
2. **One owner per moved symbol**, and the declared owner reference resolves.
3. **Every facade is the exact old identity**, re-exports the declared owner, and
the owner actually exports the source symbol.
4. **Every characterization-test reference resolves** to a real node.
What proves the same on this tree today:
- **(2), (3) and (4) for the 18 families:** `scripts/v7next_adoption.py --release`.
It requires all 18 `REQUIRED_DELTAS` present with the pinned `REQUIRED_PHASE`,
refuses any non-`done` status or `pending-decision` disposition, and — for every
`done` row — resolves *every token* of the verification hook, path and `::nodeid`
half alike. The owner-facade suites (`test_loop_owner_facades.py`,
`test_git_ops_owner_facades.py`, `test_delegate_owner_facades.py`,
`test_review_owner_facades.py`, `test_lc2_owner_facades.py`,
`test_tool_owner_facades.py`, `test_update_merge_owner_facade.py`) pin facade
identity and hot-code parity per family — the same property as (3), asserted from
the tree rather than from a row.
- **(3) mechanically, across the whole tree:**
`tests/test_generated_inventories.py::test_facade_inventory_is_byte_identical`
against `docs/v7next/FACADE_INVENTORY.md`, regenerated by
`scripts/regenerate_inventories.py` — a tree-walked facade census, so a new facade
that nobody rowed still shows up as a diff.
- **(1) partially, by a different mechanism:** `CPL-1`'s `ouroboros/domains.toml` is
a 1:1 module→domain manifest over *all* tracked runtime modules with a
completeness checker (`scripts/check_domains.py`, `scripts/v7next_domain_report.py`),
so a new module cannot appear un-owned; and
`ouroboros/size_ratchet_manifest.py` (regenerated/checked by
`scripts/regenerate_size_ratchet.py --check`, pinned by
`tests/test_smoke.py::test_size_ratchet_transition_against_explicit_base`) makes
every oversized file a named, shrink-only debt entry, so an *unsplit* giant is
loud even though an *unrowed split* is not.
- **Per-stream extraction suites** (`tests/test_*_extraction.py`, 26 files) pin the
leaf sets, the parent facades and the module-handle invariants stream by stream.
**What remains unproven, honestly:**
- **(1) in the strong form.** Nothing on this tree walks
`merge-base..HEAD` and demands a ledger row for each moved symbol. A verbatim
extraction performed here with no family row and no facade change would pass every
gate above. The family manifest is a manifest of *decisions*, not of *motions*.
- **The oracle's `GIANT_PATHS` = 0.** Spec §8.5 makes an empty `GIANT_PATHS` part of
formal completion; the oracle reached 0. This tree ships **23** entries (baseline
74 per spec §4.3.1). 15 of them are test files, 2 are web, 2 devtools, 1 skills,
and 3 are runtime (`ouroboros/tools/git.py`, `server.py`, `supervisor/workers.py`).
1282 oracle rows — a third of the ledger — are exactly the splits of those files.
This is the single largest gap between the oracle and this tree, it is not
dispositioned by any `ADOPTION_v7next.md` row, and it is stated here rather than
implied by the word "family".
- **The W (web) stream.** 157 of 170 rows have no destination here and no family row;
the oracle's `chat.js` decomposition is simply absent. Its `web/tests/chat_facade.test.js`
hook does not exist on this tree.
- **The oracle's anonymous-source residual** (12 unnamed handler/IIFE bodies the
oracle disclosed in its header because they structurally cannot carry a
`path::symbol` row) carries over unproven, since the moves that would have created
them did not happen here.
## 5. How to regenerate this table
No script is added: the projection is re-derivable from two frozen inputs by the
commands below. Run from a checkout that has both `ouroboros_v7_wip` and this
branch (any worktree of the repo). No runtime state is touched, so no
`OUROBOROS_*` roots are needed for the read-only `git`/`python` calls below; if you
prefer to run it under the workspace's pytest hygiene, point
`OUROBOROS_APP_ROOT`/`OUROBOROS_REPO_DIR`/`OUROBOROS_DATA_DIR`/`OUROBOROS_SETTINGS_PATH`
at a fresh temporary root first.
Row count (the oracle's own parser shape — table lines minus header and separator):
```
git show ouroboros_v7_wip:MIGRATION_v7.md | grep -c '^|' # 3903 -> 3901 rows
```
The full grouping, the status cross-cut, the destination cross-cut and the
giant correspondence:
```
~/ouro/venv/bin/python - <<'PY'
import ast, collections, re, subprocess
oracle = subprocess.run(["git","show","ouroboros_v7_wip:MIGRATION_v7.md"],
capture_output=True, text=True, check=True).stdout
tree = set(subprocess.run(["git","ls-tree","-r","--name-only","HEAD"],
capture_output=True, text=True, check=True).stdout.split())
tbl = [l for l in oracle.splitlines() if l.startswith("|")]
rows = [[c.strip() for c in l.strip("|").split("|")] for l in tbl[2:]]
did = lambda c: (re.search(r'"id":"([^"]+)"', c) or [None, "PARSE_FAIL"])[1]
st = lambda c: (re.search(r'"status":"([^"]+)"', c) or [None, "PARSE_FAIL"])[1]
def stream(p):
p = p.split("::")[0]
for k in ("web/","tests/","supervisor/","ouroboros/tools/","ouroboros/",
"devtools/","skills/"):
if p.startswith(k): return k.rstrip("/")
return p
fam = lambda r: did(r[3]) if did(r[3]) != "none" else "none/" + stream(r[0])
by_family = collections.Counter(fam(r) for r in rows)
present = collections.Counter(fam(r) for r in rows if r[1].split("::")[0] in tree)
giants = set(ast.literal_eval(next(
n.value for n in ast.parse(open("ouroboros/size_ratchet_manifest.py").read()).body
if isinstance(n, ast.Assign) and n.targets[0].id == "GIANT_PATHS")))
absent = [r for r in rows if r[1].split("::")[0] not in tree]
print("rows", len(rows))
for k in sorted(by_family):
print(f"{k:<24} {by_family[k]:>5} dest_present={present.get(k,0)}")
print("status", collections.Counter(st(r[5]) for r in rows))
print("dest present", len(rows) - len(absent), "absent", len(absent),
"absent-on-giants", sum(1 for r in absent if r[0].split("::")[0] in giants))
PY
```
The status of each of the 18 families is read from `ADOPTION_v7next.md` (the
`kind=semantic-delta` rows) and verified by
`~/ouro/venv/bin/python scripts/v7next_adoption.py --release`.

View file

@ -1,222 +0,0 @@
# R-WINWAVE — one decision per cross-OS class
The v7 campaign and upstream fixed the same Windows/macOS territory
independently. Plan §2 mandates the return of the v7 cross-OS wave and plan §11
names the hazard: re-applying it commit-by-commit would fight the upstream
fixes and duplicate them. So the return is **by class**, and this file is the
registry the ADOPTION row points at — one recorded decision per class, and no
class silently re-applied twice.
Reference wave (frozen, `ouroboros_v7_wip @ 9f691656`): commits `18048261`
(14 files), `073c610d`, `640a249a`, `711c982b`, `6cdc8e16`, `b20c94a1`. None is
an ancestor of this branch or of `managed/ouroboros`. The upstream fixes for the
same territory ARE ancestors here: `7de26338`, `c15389f4`, `18a4e17d`,
`78d168b8`, `d2f6701a`, `0c4acfd9`, `10779106`, `d428f125`, `4a550589`.
Decision vocabulary, one per row:
- **re-applied** — the class exists only on our side; the campaign landed the
reference form (the carrier files do not exist upstream at all).
- **superseded-by-upstream** — upstream solved the same class, differently or
more broadly; the reference form is NOT transplanted, upstream's stands.
- **not-applicable** — the carrier the reference patched does not exist in this
tree, so there is nothing to decide until a carrier arrives.
Sixteen classes from the reference wave. (The campaign audit's summary line
said "15"; the recount below separates the
`PermissionError`-beside-`IsADirectoryError` clause from the `tools.jsonl`
utf-8 read — they were one line there.) A seventeenth class was found by the
matrix itself and is recorded the same way, in its own section: a class this
registry learns about from a red run still owes exactly one decision.
## Re-applied (7)
| # | class | decision | where it lives now |
|---|---|---|---|
| 1 | `fchmod` POSIX guard + paid-lane fail-closed off POSIX (a non-POSIX host must refuse the paid lane rather than write a live key before chmod; reference `b20c94a1`) | re-applied | `tests/fixtures_e2e_cancellation.py` (guard + refusal), landed by `4fffefb1` |
| 2 | 0600-mode assertions skipped where the OS has no POSIX mode bits | re-applied | `tests/test_e2e_cancellation_scenarios.py`, landed by `4fffefb1` |
| 3 | Canonical-path compares instead of string equality (Windows short paths, drive-letter case) | re-applied | `tests/test_external_review_script.py` (`3b62c1d6`) and `tests/test_cancel_protocol_inventory_s6.py` (`4fffefb1`) |
| 4 | `os.sep` / `!r`-mirror expectations instead of hardcoded `/` in message pins | re-applied | `tests/test_core_native_results.py`, landed by `fa2f6fc5` |
| 5 | Per-scenario registry route pin (the `640a249a` lesson: `user_files` is path-dependent so Windows takes the native route, `cognitive` is arg-dependent so the adapter runs everywhere — one blanket platform pin was the over-generalisation) | re-applied | `tests/test_registry_core.py`, landed by `ccbb933a` |
| 6 | Planted-stamp comparison tolerant of the 15 ms clock tick | re-applied | `tests/test_owner_stop_fences_s6.py`, landed by `88479fa7` |
| 7 | utf-8 ARCHITECTURE fixture (explicit encoding on read/write, not the cp1252 default) | re-applied | `tests/test_update_carriers.py`, landed by `7f0a1124` |
## Superseded by upstream (3)
| # | class | decision | evidence |
|---|---|---|---|
| 8 | Text-mode CRLF translation in the atomic writer — the root cause of the original 61-red Windows run | superseded-by-upstream | Upstream `c15389f4` decomposed it into `write_bytes_atomic` (+ UTF-8 encode) instead of adding `O_BINARY` inside `write_text_atomic`; the corrective lane then proved upstream's form is the BROADER fix (the non-fsync `Path.write_text` lane translated too). Replaying the reference would invert that decision. Ledger row 15; pinned by `tests/test_atomic_write_v639.py` |
| 9 | Launcher reaper: normpath'd path literals + POSIX-only enumeration test | superseded-by-upstream | Upstream `7de26338` normpaths the same literals (and the python binary path the reference missed) and, instead of skipping the enumeration test off POSIX, stubs `getuid` so it runs on every OS. Ledger row 16; pinned by `tests/test_launcher_server_reaper.py` |
| 10 | Child-process environment forwarding (`SystemRoot`, `TEMP`, `TMP`, `USERPROFILE`) | superseded-by-upstream | Upstream `78d168b8`, pre-cutoff; pinned by `tests/test_evolution_state_integrity_v3.py` |
## Not applicable in this tree (6)
| # | class | decision | why |
|---|---|---|---|
| 11 | Evidence/migration script hardening: JSON through stdin instead of argv (the Windows 32767-byte **command-line** cap, not a path-length cap), env forwarding, `PYTHONIOENCODING=utf-8`, stderr tail in the failure message | not-applicable | Carriers `scripts/v7_evidence.py` and `scripts/v7_migration.py` do not exist here |
| 12 | POSIX-scoped exactness pin of the prologue evidence | not-applicable | `tests/test_v7_prologue_evidence.py` does not exist here |
| 13 | Ledger test moved to the serial lane (its 72 MB RSS was a timeout symptom, not a memory limit) | not-applicable | The `test_v7_migration_ledger` carrier does not exist here |
| 14 | SIG9 exit-facts parity between the executor and the local shell | not-applicable | The asserting test is absent from `tests/test_shell_run_shell.py` and from everywhere else; the producer-side fallback exists (`ouroboros/tools/process_facts.py`, `tool_result.py`) |
| 15 | `tools.jsonl` read with an explicit utf-8 encoding | not-applicable | The reference test was re-derived as a `tests/test_tool_result.py` case, and the only reader of `tools.jsonl` content on this tree already passes `encoding="utf-8"` |
| 16 | `PermissionError` accepted beside `IsADirectoryError` on a directory write | not-applicable | The asserting directory-write test no longer exists in `tests/test_core_native_results.py` |
## Found by the matrix, not by the reference wave (1)
| # | class | decision | where it lives now |
|---|---|---|---|
| 17 | Source-text regex pins in the JS suite: a test that reads a source file off disk and matches a `\n`-bearing regex against it cannot pass on a CRLF checkout | re-applied | `web/tests/chat_plain_system_rows.test.js` normalizes CRLF to LF at both `readFileSync` reads, landed by `a0b35fcd` on the campaign integration branch — NOT on this worktree, which is why the row below still needs its own matrix leg |
Class 17 is upstream-born territory (`817a834f`, `dbd500cc`, both ancestors of
`managed/ouroboros`), so neither the reference wave nor the upstream fixes had
decided it; the red windows leg of run 33555971481 is what surfaced it. The
decision is the narrow one — normalize at the read, not a `.gitattributes`
checkout policy — because the assertion is about source TEXT, and only the two
reads that feed source-text regexes are touched.
## Open items on this row
1. **`tests/test_registry_core.py` still pins `os.name == "nt"`.** The accepted
2026-08-30 audit item (`~/.claude/plans/v7next/V7NEXT_PLAN.md`; owner
requirements archive, «os.name→alias-условие в route-пине») asks for the pin
to name the actual alias condition instead of the platform.
Class 5 above is landed; this is a follow-up on its expression, not on its
decision. Still open on the integration tip (`tests/test_registry_core.py`
line 813 reads `os.name`); the repin is being landed by the smalls lane of
the stage-2 fix wave, so this item is assigned, not merely listed.
2. **Green windows legs exist, and the whole matrix is green.** This item used to
read «no green windows leg exists yet on any frozen SHA», which the run table
below has contradicted since run 33568728122 (`f5a94675`, first green Windows
leg) and, for the full matrix, since run 33569841899 (`8b27b507`) — four
consecutive first-attempt-green 3-OS matrices are logged there (33569841899,
33570328266, 33571681398, 33572515529), and on the later branch tips two more
whose Windows `full-test` leg went green only on a RERUN (33579445704 on
`1072a317`, 33624546416 on `ac17fa03` — attempt-1 detail under the run table)
plus two more first-attempt greens: 33626834806 on `43dcc1d2`, where all three
`full-test` legs passed on attempt 1, and 33644668074 on the sync #3 merge
`f4abe0a5`, green on every job on attempt 1. Class 17 is decided, fixed and
proved. What is genuinely open is FRESHNESS, not colour: 33644668074 is the
newest verdict (read 2026-09-02 15:00Z); the tips after it (C6 merge, the
stage-2 fix lanes) await their own dispatch on the release candidate. Second,
narrower open point: the attempt-1 Windows failure of 33579445704 was two
failures, and only one is now rooted — the observability copy-back race is
fixed by `626b48b7` (owner 15 = B answered O3 with «fix now»), while the
`tests/test_preflight_runner` xdist worker timeout still has no landed
root-cause fix and is carried below as an intermittent class.
## 3-OS matrix runs
The row's re-prove needs a green full-test 3-OS matrix on a frozen branch SHA
(`gh workflow run CI --ref <branch>` → `ci.yml` full-test). Runs so far:
| run id | SHA | ubuntu | macos | windows | verdict |
|---|---|---|---|---|---|
| [33555971481](https://github.com/razzant/ouroboros/actions/runs/33555971481) | `9a28e58f` (branch `ouroboros_v7next`, 2026-09-01 20:33Z) | green | green | **RED** (class 17) | not a re-prove |
| [33563498919](https://github.com/razzant/ouroboros/actions/runs/33563498919) | `196438c9` (carries the class-17 fix `a0b35fcd`) | green | green | **RED** — 16 tests, nine platform classes (chmod probes, open-file unlink, signal.alarm, separators, shlex backslashes, cp1252, simulated O_BINARY, byte-exact writer vs os.linesep, host-only signal names) | not a re-prove; classes fixed in 20afdbb7..e0aee1ac |
| [33567328254](https://github.com/razzant/ouroboros/actions/runs/33567328254) | `9754cc95` | green | **RED** (two scheduler-sensitive pins, hardened in 455c9a1e) | **RED** (one: the listing pin's own fold of the JSON-escaped separator, fixed d0d52677) | not a re-prove |
| [33568284121](https://github.com/razzant/ouroboros/actions/runs/33568284121) | `7c93e8b7` | green | green | **RED** (the same single pin) | not a re-prove |
| [33568728122](https://github.com/razzant/ouroboros/actions/runs/33568728122) | `f5a94675` | green | **RED** (custody pin, gated in 8b27b507) | **green** — first green Windows leg | not a re-prove (macOS) |
| [33569841899](https://github.com/razzant/ouroboros/actions/runs/33569841899) | `8b27b507` | green | green | green | **RE-PROVE** — full matrix green |
| [33570328266](https://github.com/razzant/ouroboros/actions/runs/33570328266) | `285ab66d` | green | green | green | re-prove holds |
| [33571681398](https://github.com/razzant/ouroboros/actions/runs/33571681398) | `9238cc2d` | green | green | green | re-prove holds |
| [33572515529](https://github.com/razzant/ouroboros/actions/runs/33572515529) | `c0029d45` | green | green | green | re-prove holds |
| [33574822693](https://github.com/razzant/ouroboros/actions/runs/33574822693) | `d21806d8` (first run of the scheduled `system-e2e-mock` job on dispatch) | success | success | **failure** | **not a re-prove** — verdict read 2026-09-02: `full-test (windows-latest)` red, the run never rerun (`run_attempt` 1, run conclusion `failure`); every other job green, the new `system-e2e-mock` included. The failing subtests are not attributable from here — job logs need repo-admin rights — so no class is claimed for it; the two tips after it (1072a317, ac17fa03) went green on the Windows leg only on rerun; the first later first-attempt-green Windows leg is 43dcc1d2 (33626834806) |
| [33579445704](https://github.com/razzant/ouroboros/actions/runs/33579445704) | `1072a317` | success | success | success **on rerun** | re-prove holds **on attempt 2, not on attempt 1** — `full-test (windows-latest)` failed first and was rerun green; every other job green on attempt 1. Named cause and its open residual below |
| [33624546416](https://github.com/razzant/ouroboros/actions/runs/33624546416) | `ac17fa03` | success | success | success **on rerun** | re-prove holds **on attempt 2, not on attempt 1** — `full-test (windows-latest)` failed first and was rerun green; every other job green on attempt 1. A code fix followed (`43dcc1d2`), so this one is rooted, not intermittent |
| [33626834806](https://github.com/razzant/ouroboros/actions/runs/33626834806) | `43dcc1d2` | green | green | green | re-prove holds — full-test 3-OS green on the FIRST attempt; the separate `system-e2e-mock` job was red 2/57 on attempt 1 and green on rerun (see below) |
| [33644668074](https://github.com/razzant/ouroboros/actions/runs/33644668074) | `f4abe0a5` (the sync #3 merge) | success | success | success | **green** — full-test on all three OS, `system-e2e-mock` green on the first attempt, integration-test green (verdict read 2026-09-02 15:00Z) |
| [33654743857](https://github.com/razzant/ouroboros/actions/runs/33654743857) | `bf8b6549` (C6 lane merged; first Windows execution of its code) | success | success | **failure** — 12 tests: LockFileEx mandatory-lock class, five POSIX-protocol pins, `signal.SIGKILL` spelling (packet §10 addendum; abea91ec) | not a re-prove |
| [33658408570](https://github.com/razzant/ouroboros/actions/runs/33658408570) | `f2f014bc` | success | **failure** (executor rule of 504bb20c: hash-less child leaked on macOS `ps`; fixed 35b82db0) | **failure** — compaction fixture class (name tier cannot compact; suite skipped at abe93702) + the two concurrency tests below | not a re-prove |
| [33658966160](https://github.com/razzant/ouroboros/actions/runs/33658966160) | `5ae7f357` (7.0.0 carriers) | success | success | **failure** — same compaction fixture class + the two concurrency tests + three probe pins | not a re-prove |
| [33661022574](https://github.com/razzant/ouroboros/actions/runs/33661022574) | `abe93702` | **failure** (platform guard: `os.kill` outside platform_layer) | failure (same) | **failure** — chat-append concurrency test, three probe pins, platform guard | not a re-prove |
| [33663258606](https://github.com/razzant/ouroboros/actions/runs/33663258606) | `35b82db0` | success | success | **failure** — exactly the two concurrency tests: the name-tier release orphan (LEDGER «From the Windows CI matrix on 35b82db0»; class fix `_unlink_lock_path`) | not a re-prove — `system-e2e-mock`, integration, all smokes green |
| [33668287491](https://github.com/razzant/ouroboros/actions/runs/33668287491) | `d0bb839e` (name-tier release retry) | success | success | **failure** — the two concurrency tests now GREEN (class closed); red = the new POSIX release pin importing `fcntl` on Windows (skipif, next row) and `test_task_result_monotonic::test_proactive_namer_late_settlement_refreshes_cost_without_late_name` (0.0 == 0.25 inside its 2 s poll window; first time on any Windows leg, green on the next run with identical runtime code — intermittent, unrooted: the retry cannot lengthen an uncontended release, and any unlink refusal orphaned the lock before this commit, failing the same test the same way) | not a re-prove |
| [33669250620](https://github.com/razzant/ouroboros/actions/runs/33669250620) | `4c7c5aed` (3.11+ pin, ledger precision) | success | success | **failure** — exactly one: the POSIX release pin's `fcntl` import (test shape; skipif in the next SHA); concurrency tests green, timing test green | not a re-prove — `system-e2e-mock`, integration, all smokes green |
The windows failure in run 33555971481, read from the run's own log rather than
from its exit code: two subtests of `web/tests/chat_plain_system_rows.test.js`
— «render arm order and enhancement guard are pinned in source» and «chat
bubble heading clamp is scoped in style.css». That is class 17 above, not a
regression of anything this campaign re-applied, and the same run's `full-test
(ubuntu-latest)` and `full-test (macos-latest)` were green. Also red in that
run and outside this row's scope: `ui-smoke` and `skill-smoke (ubuntu-latest)`.
Run 33563498919 (`196438c9`, `a0b35fcd` an ancestor — both are ancestors of
every later tip of this branch) cleared class 17 and surfaced nine further
platform classes on Windows (see the run table); they were fixed in
20afdbb7..e0aee1ac and the first green Windows leg is run 33568728122
(`f5a94675`). The full 3-OS matrix went green on run 33569841899
(`8b27b507`) and held on every later run in the table, which is the
re-prove the ADOPTION row R-WINWAVE cites; the per-class decisions above
stand as recorded.
The two red `system-e2e-mock` subtests on attempt 1 of run 33626834806
(`43dcc1d2`) were not platform classes and are not registry rows: both were
races inside the mock lane's own scaffolding — the `/proc`-environ scan of
`pids_with_env_value` and an S22 wait that assumed its window was wide enough
under CI load. Neither is a cross-OS class; both belong to the E2E lane's own
ledger rather than to this row.
The `/proc`-environ half is no longer «lane flakiness … disclosed»: it is
**rooted and fixed by `626b48b7`** (owner batch №13 item 15 = B). The cause is
not «a process can exit between the listing and the read» but the post-exec
window — `Popen` returns once the exec SUCCEEDED (the CLOEXEC error pipe closes
inside `execve`), while the kernel publishes the new image's
`env_start`/`env_end` later in that same path, so a read landing there sees an
EMPTY environ for a live, correctly marked child (the same shape failed again as
`assert 3898 in []` on run 33671108287). The harness now separates the positive
oracle (`wait_pid_env_value`, a bounded poll of THE ONE pid) from the no-orphans
postcondition (`pids_with_env_value`, still a single scan), and the window is
pinned deterministically through their shared read seam. The S22 wait remains a
disclosed lane-timing observation.
### The two rerun-greens, per row
Two «re-prove holds» rows above were reruns, not first-attempt greens, and each
is recorded with its attempt-1 outcome, its attempt-2 outcome, the named cause,
and whether a code fix followed.
- **33579445704 (`1072a317`).** Attempt 1: `full-test (windows-latest)`
**failure**, every other job green. Attempt 2 (rerun of the failed job):
green. Named cause, **operator-read**: two Windows failures —
`tests/test_phase3c_observability_gc` on its copy-back step (intermittent) and
`tests/test_preflight_runner` on an xdist worker timeout. The copy-back half is
now **rooted and fixed by `626b48b7`** (owner batch №13 item 15 = B, which
answered the open **O3** question with «fix now»): two concurrent copy-backs
promote the same content-addressed source handle, and on Windows the loser's
`os.replace` over a destination the winner or a verifying reader holds open is a
sharing violation, so the loser published an INCOMPLETE promotion while the
winner published a complete one. The store is now write-once and the promotion
judges by its postcondition rather than by authorship of the write (diagnosis
and red-first pins: docs/v7next/LEDGER_CORRECTIONS.md, «From the
delegation-mutation and races lane»). The `tests/test_preflight_runner` xdist
timeout had **no code fix** and stands as **intermittent, unrooted**. Either
way this SHA's Windows leg is a rerun-green and must not be cited as a
first-attempt green.
- **33624546416 (`ac17fa03`).** Attempt 1: `full-test (windows-latest)`
**failure**, every other job green. Attempt 2: green. Named cause,
operator-read and independently corroborated by the fix commit's own message:
the session-engine horizon is the ceiling of the seconds left to the deadline,
and a sub-second remainder on the coarse Windows clock made the pin read 301
for a deadline 300 s away. **A code fix followed** — `43dcc1d2` («tests: the
session-engine horizon pin tolerates the coarse-clock ceiling (Windows
full-test)», `tests/test_review_agent_session_route.py`, naming this run id) —
so this class is **rooted and closed**, and the next run on `43dcc1d2`
(33626834806) has all three `full-test` legs green on attempt 1.
How the attempt structure was read: read-only against the public GitHub API —
`GET /repos/razzant/ouroboros/actions/runs/<id>` for `run_attempt` and
`conclusion`, and `.../attempts/<n>/jobs` for the per-job conclusions. `gh` is
installed on this host but not authenticated and nobody logged in for this read;
unauthenticated requests answered both endpoints because the repository is
public. Job **logs** are not readable that way (403, «Must have admin rights to
Repository»), and the check-run annotations carry only «Process completed with
exit code 1» — which is why the failing test names above are recorded as
operator-read facts rather than as facts re-derived here. Attempt counts as
read: 33569841899, 33570328266, 33571681398, 33572515529 and 33644668074 are
`run_attempt` 1 with conclusion `success`; 33579445704, 33624546416 and
33626834806 are `run_attempt` 2; 33574822693 is `run_attempt` 1 with conclusion
`failure`.
(Written on the adoption lane's own worktree, where `a0b35fcd` and `196438c9`
were not yet ancestors; on the integrated branch both are, and the run table
above carries the outcomes.)

View file

@ -20,8 +20,9 @@ row is red too). A new cross-domain import direction, a wider cycle group, or a
cross-domain literal copy is a red gate, not a warning: needing one is an owner
decision, not a manifest edit. The witness-level detail behind the baseline —
every module-edge witness, the lazy/guarded/dynamic classification, the cycle
groups — lives in `docs/v7next/DOMAIN_QUOTIENT_REPORT.md` (report only,
regenerated by `python scripts/v7next_domain_report.py`).
groups — is available from `python scripts/domain_report.py` on stdout, or
with `--output <path>` for an explicit report file. `docs/DOMAIN_MAP.md` remains
the maintained generated map.
Rules here describe current practice or a deliberately enforced standard. When
code and prose disagree, inspect the implementation and history, repair the

View file

@ -214,7 +214,7 @@ defaults belong to their owners (ARCHITECTURE, CHECKLISTS, BIBLE,
`config.py`) and are pointed to, not restated — the ARCHITECTURE settings and
endpoint tables are test-checked registries of those owners, not second
authorities. A change REPLACES the description of the node it touched; release
history lives in git and the README history table, leftovers go to issues.
history lives in git and the README history table.
Residue — parenthesized version stamps, decision codenames, "used to /
previously" narrative — is caught by the shrink-only residue check in
`tests/test_docs_sync.py`, which enforces only the explicit, case-sensitive
@ -223,6 +223,17 @@ language-tagged fences (the untagged module-tree fence in ARCHITECTURE §1 IS
scanned — an owner decision); semantically equivalent historical prose stays
review-only under CHECKLISTS item 7.
Track assets with a continuing purpose for the product, contributors, verification,
legal requirements or evidence for public claims, beyond the work that introduced them. Plans, review packets, run receipts
and campaign bookkeeping belong in the external work area or durable task evidence,
not the tracked source tree; a test preserving their presence or wording does not
give them a permanent product role. Retire temporary campaign tooling when its
purpose ends. Keep current behavior and its rationale in their existing owners;
future-work lists and campaign backlog stay outside the tracked product tree.
Generated snapshots with real product, verification or publication consumers remain
valid; optional reports use stdout or an explicit output destination. Existing
review enforces this contract, without automatic deletion or filename matching.
Both of those documents are reference BOOKS: an entrypoint carrying its H1, one
authored introductory paragraph and an ordered `## Chapters` membership list,
plus one chapter file per subject under `docs/architecture/` or
@ -243,8 +254,8 @@ tree so a breach is red rather than discovered by the next review:
owns; moving prose between chapters is a documentation change like any other
and REPLACES the description at its destination.
- **One line ending.** `.gitattributes` pins `docs/**/*.md` to LF, so the
physical line ranges and SHA-256 digests the generated inventories and the
transfer table carry mean the same bytes on every platform; a CRLF checkout
physical line ranges and SHA-256 digests the generated inventories carry
mean the same bytes on every platform; a CRLF checkout
would move every cited line.
- **Readers ask for a view, not for a file.** `load_governance_doc` composes a
book for a surface that owes it in full, `context_layout.book_navigation`
@ -253,8 +264,6 @@ tree so a breach is red rather than discovered by the next review:
Never read an entrypoint with `read_text()` and treat the result as the book
— that is a membership list, and a substring pin over it passes while testing
nothing (`tests/_governance_docs_shared.py` is the one reader tests use).
`docs/reference-books-migration.md` is the operator record of the original
split and is deliberately not a member of either book.
### Generality and emergence (P13)

View file

@ -2,7 +2,7 @@
Machine extraction of the `docs/ARCHITECTURE.md` "Data layout (`~/Ouroboros/`)" tree — the durable-file orientation carrier (this tree's counterpart of the reference PERSISTENCE_OWNERS derivation checklist) — regenerated by `python scripts/regenerate_inventories.py`. Do not edit. Every entry is probed against reality: repo entries must exist as tracked paths; data-plane entries must appear as a literal in the runtime sources that construct them. A durable file renamed or removed in code while its tree row survives = red (`tests/test_generated_inventories.py`).
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 568-657; UTF-8 SHA-256 `391c814a062ec74ce6c2ba11f0f9e1c348a75c2078ab1b0303fb1670a86eaa25`.
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 568-657; UTF-8 SHA-256 `8f1d5d8f360afdcc1cd0ef0cf477ef5f64ba6440673943efae29256f924b4417`.
- entries: **78** (code-ref: 71, repo-dir: 6, repo-path: 1)

View file

@ -2,7 +2,7 @@
Machine extraction of `docs/ARCHITECTURE.md` §11.1 (the frozen-ABI SSOT), regenerated by `python scripts/regenerate_inventories.py`. Do not edit — edit the owning chapter named in the Source line and regenerate; `tests/test_generated_inventories.py` pins byte-identity and the resolution invariants (a §11.1 row whose owner or anchor file disappeared from the tree = red).
Source: `docs/architecture/11-frozen-contracts-v1.md`, physical LF lines 7-38; UTF-8 SHA-256 `a1236222936532cf2e2bd1c28f08c374dc692e24741f1cb3ee2c7e7f325e0cd4`.
Source: `docs/architecture/11-frozen-contracts-v1.md`, physical LF lines 7-38; UTF-8 SHA-256 `e78a0586a65071360d8b98845a8eaf845bbead5265337d536b04f2bd70984809`.
- table rows: **27**
- browser-envelope prose owners:

View file

@ -1,324 +0,0 @@
# Reference books: chapter migration transfer table
This is the operator record of the physical split of the two reference books
(`docs/ARCHITECTURE.md`, `docs/DEVELOPMENT.md`) at base commit `5585133db86419c1a28673e498de4fb13c6b2d1e`.
It is a docs file and deliberately NOT a book member: it lives directly under
`docs/`, so `validate_reference_books` never sees it in either book's chapter
population.
The split happened in two phases, both recorded here. Phase one (the tables
under each book heading below) was a **verbatim move**: the moved bytes were
every byte of the old `##` section AFTER its heading line, including its
`###`/`####` sub-headings at the levels they already had; nothing was merged,
rewritten, summarized, reordered or deleted, and no section title was renamed
— the rename column is empty for every row. Phase two, the semantic
subtraction, changed chapter bodies on purpose and is recorded in the
"Semantic subtraction" section at the end: every merged duplicate, rewritten
explanation and removed piece of narrated history, with the chapter and
heading that keeps the fact.
## What changed, exactly
Per chapter file, the only new bytes are a two-line prologue:
1. `# <the old section title>` — the old `## N. Title` text at H1, numbering
text kept so every cross-reference in the corpus still reads;
2. one authored introductory paragraph (2–4 sentences: what the chapter owns
and why it exists), the only new prose in this migration.
Everything after that prologue is the old section body, byte for byte.
Per entrypoint, the body is replaced by an ordered `## Chapters` membership
list; the H1 line stays byte-identical in both books.
## Byte proof
`tests/test_reference_book_migration.py` reverses the prologue of every chapter
AS THE MIGRATION COMMIT WROTE IT (drop the H1 line, drop the one introductory
paragraph, re-prefix `## ` to the H1 text), concatenates the results in
membership order, and requires the SHA-256 below against `git show
<base>:<path>`. The proof is bound to history — the chapters at the commit that
added this file against the monolith at the base commit — so the semantic
subtraction below could not and did not turn it red; it stays as the record
that phase one moved bytes, not meaning. The current tree is proven by the
structural and composition tests instead: `test_every_base_section_is_still_exactly_one_chapter`,
`tests/test_reference_book_validation.py` (the validator over the tracked
tree) and `tests/test_reference_book_pack_assembly.py` (the packs assembled
from the real chapters).
| Book | Old file | Old bytes | Old SHA-256 | Preamble bytes (replaced) | Moved-body bytes | Moved-body SHA-256 |
|---|---|---|---|---|---|---|
| architecture | `docs/ARCHITECTURE.md` | 724691 | `5db278f8ef5060c4aff5ee1e8743c279661ddd975a311a9bafb5858f32b080de` | 610 | 724081 | `f1f054c700a15533687e0cf81cf19ac53ffcf022eb179f84c0cbccacbb1d305e` |
| development | `docs/DEVELOPMENT.md` | 275551 | `50eb460602f1501915195e3ad1918366312e6292b0dccec6f7ef53f5a5302f3b` | 60 | 275491 | `bffc00227bc5e91f054b38eaed63acd256c2ddf111e231754bfbe92c7dd122e3` |
## `docs/ARCHITECTURE.md`
Entrypoint preamble before: `# Ouroboros v7.0.0 — Architecture & Reference` (byte-identical, the release version carrier `release_sync.VERSION_CARRIER_SPANS` writes), then two paragraphs (`This file is NOT a changelog…` and `This is the present-tense operational map…`) and a `---` rule.
Entrypoint preamble after: the same H1, then ONE merged paragraph carrying both original paragraphs' claims (present-tense map in three layers, not a changelog, WHY stays in the book, rationale self-contained), then `## Chapters`. The `---` rule is dropped.
| Old `##` section | Lines at base | Section bytes | Destination chapter | Disposition | Title rename |
|---|---|---|---|---|---|
| `1. High-Level Architecture` | 9–659 | 188782 | `docs/architecture/01-high-level-architecture.md` | verbatim move | — |
| `2. Startup / Onboarding Flow` | 660–695 | 14555 | `docs/architecture/02-startup-onboarding-flow.md` | verbatim move | — |
| `3. Web UI Pages & Buttons` | 696–868 | 88631 | `docs/architecture/03-web-ui-pages-and-buttons.md` | verbatim move | — |
| `4. Server API Endpoints` | 869–1031 | 23636 | `docs/architecture/04-server-api-endpoints.md` | verbatim move | — |
| `5. Supervisor Loop` | 1032–1082 | 32383 | `docs/architecture/05-supervisor-loop.md` | verbatim move | — |
| `6. Agent Core` | 1083–1871 | 258915 | `docs/architecture/06-agent-core.md` | verbatim move | — |
| `7. Configuration (ouroboros/config.py)` | 1872–2084 | 33538 | `docs/architecture/07-configuration.md` | verbatim move | — |
| `8. Git Branching, CI, and Build` | 2085–2140 | 17850 | `docs/architecture/08-git-branching-ci-and-build.md` | verbatim move | — |
| `9. Shutdown & Process Cleanup` | 2141–2156 | 12956 | `docs/architecture/09-shutdown-and-process-cleanup.md` | verbatim move | — |
| `10. Key Invariants` | 2157–2235 | 15834 | `docs/architecture/10-key-invariants.md` | verbatim move | — |
| `11. Frozen Contracts v1 (`ouroboros/contracts/`)` | 2236–2326 | 19130 | `docs/architecture/11-frozen-contracts-v1.md` | verbatim move | — |
| `12. Host Service, Companion Processes, and Chat IDs` | 2327–2371 | 10594 | `docs/architecture/12-host-service-companions-and-chat-ids.md` | verbatim move | — |
| `13. External Skills Layer` | 2372–2386 | 7277 | `docs/architecture/13-external-skills-layer.md` | verbatim move | — |
## `docs/DEVELOPMENT.md`
Entrypoint preamble before: `# DEVELOPMENT.md — Development Principles & Module Guide` (byte-identical), then `## Role and authority` directly, with no introductory paragraph of its own.
Entrypoint preamble after: the same H1, then ONE newly authored orientation paragraph (what the handbook is, how the chapters are ordered, read the chapter for the class of change in hand), then `## Chapters`. No moved prose.
| Old `##` section | Lines at base | Section bytes | Destination chapter | Disposition | Title rename |
|---|---|---|---|---|---|
| `Role and authority` | 3–32 | 1707 | `docs/development/01-role-and-authority.md` | verbatim move | — |
| `Naming and boundaries` | 33–526 | 35291 | `docs/development/02-naming-and-boundaries.md` | verbatim move | — |
| `Module Size & Complexity` | 527–849 | 21621 | `docs/development/03-module-size-and-complexity.md` | verbatim move | — |
| `Core Governance Artifacts` | 850–1121 | 20584 | `docs/development/04-core-governance-artifacts.md` | verbatim move | — |
| `Review & Commit Protocol` | 1122–1351 | 15585 | `docs/development/05-review-and-commit-protocol.md` | verbatim move | — |
| `Rules by change class` | 1352–2963 | 118624 | `docs/development/06-rules-by-change-class.md` | verbatim move | — |
| `Managed Update Rule` | 2964–3022 | 3732 | `docs/development/07-managed-update-rule.md` | verbatim move | — |
| `Mutation Attribution Rule` | 3023–3059 | 2289 | `docs/development/08-mutation-attribution-rule.md` | verbatim move | — |
| `Process Custody Rule` | 3060–3203 | 10776 | `docs/development/09-process-custody-rule.md` | verbatim move | — |
| `Platform Abstraction Rule` | 3204–3247 | 2514 | `docs/development/10-platform-abstraction-rule.md` | verbatim move | — |
| `Design System` | 3248–3557 | 22543 | `docs/development/11-design-system.md` | verbatim move | — |
| `MCP Client Integration` | 3558–3605 | 3454 | `docs/development/12-mcp-client-integration.md` | verbatim move | — |
| `Gateway Boundary Pattern` | 3606–3635 | 1986 | `docs/development/13-gateway-boundary-pattern.md` | verbatim move | — |
| `Build & CI` | 3636–3886 | 14785 | `docs/development/14-build-and-ci.md` | verbatim move | — |
## Chapter granularity
One chapter per old `##` section, with no merges. Two adjacent pairs were
under the ~60-line merge threshold on both sides — Architecture §12/§13 and
Development "MCP Client Integration"/"Gateway Boundary Pattern" — but neither
pair shares a subject (a host callback boundary is not the external skills
plane; an outbound MCP client is not the inbound browser boundary), so the
default 1:1 mapping was kept. It also keeps every existing cross-reference of
the form `ARCHITECTURE "8. Git Branching, CI, and Build"` resolving to exactly
one chapter.
## Semantic subtraction
Phase two walked both books paragraph by paragraph and gave every fact stated
in more than one place ONE owner: structure, mechanism and WHY to an
Architecture chapter; process, gate and how-to-change to a Development
chapter. The other statement became a pointer to the owner's chapter and
heading, or nothing when the surrounding paragraph already implies it. Every
imperative, every enforcing test name, every owner citation and every disclosed
residual survived; where the Development copy carried a fact the Architecture
owner lacked, the owner received it (listed under "Facts whose owner moved").
Base commit of the subtraction: `4128b2048` (the integration branch after phase one).
### Statistics
| Book | Paragraphs merged (duplicate) | Paragraphs rewritten | Paragraphs removed (obsolete history) | Bytes before | Bytes after | Delta |
|---|---:|---:|---:|---:|---:|---:|
| architecture | 7 | 9 | 15 | 731409 | 730965 | -444 |
| development | 93 | 6 | 1 | 284650 | 252872 | -31778 |
Paragraph counts are disposition rows: one row per paragraph, bullet or table
row whose text changed. Byte counts are the sum of the book's chapter files
(entrypoints unchanged).
### Facts whose owner moved between the books
Development → Architecture (the Development copy was the only complete statement; the owner now carries it):
- the shared read/search byte masker — `docs/architecture/06-agent-core.md` § "Tool capability and execution"
- the path-selected attachment-ingest checks (exact credential leaves, credential/control directory components; enumerated owner stores as mutation-fence authority) — the same section
- the size ratchet's merge-aware manifest resolution and own-tree bootstrap — `docs/architecture/06-agent-core.md` § "Review stack"
- the `reasoning_effort_clamped` usage disclosure of the DeepSeek tier projection — `docs/architecture/07-configuration.md` (DeepSeek provider specifics)
- the date of the transcript-cache byte-prefix measurement (2026-09-14) — `docs/architecture/01-high-level-architecture.md`, the `transcript_prefix.py` row
- why a resolvable CI base without a manifest fails closed (copied manifests would launder debt) — `docs/architecture/08-git-branching-ci-and-build.md` § "CI topology"
- the README history row and the named direct-download links as release carriers — `docs/architecture/10-key-invariants.md`, invariant 2
Architecture → Development (the Architecture copy is now a pointer):
- what belongs in a prompt versus a tool schema (a prompt sentence restating a schema is a second copy that drifts) — `docs/development/02-naming-and-boundaries.md` § "LLM-first affordances"
- what each review-enforcement mode permits after a technical review failure — `docs/development/05-review-and-commit-protocol.md`
### Byte proof after phase two
`test_the_migration_commit_reconstructs_the_base_monolith_byte_for_byte` is kept:
it proves the phase-one commit against the base monolith and never reads the
working tree, so it stays green after this subtraction and records what phase
one was. No byte-equality promise is made for the current tree.
### Dispositions — `docs/architecture/`
| Chapter | Heading | Disposition | Retained owner | What changed | Chars before → after |
|---|---|---|---|---|---:|
| `docs/architecture/06-agent-core.md` | Tool capability and execution | merged duplicate | docs/architecture/06-agent-core.md § "Tool capability and execution" (first statement, same paragraph) | the three discovery outcomes were stated twice in one paragraph | 463 → 55 |
| `docs/architecture/06-agent-core.md` | Tool capability and execution | rewritten explanation | docs/architecture/06-agent-core.md § "Tool capability and execution" | owner now states the shared read/search masker fact that Development carried | 197 → 308 |
| `docs/architecture/06-agent-core.md` | Tool capability and execution | rewritten explanation | docs/architecture/06-agent-core.md § "Tool capability and execution" | owner now states the attachment-ingest leaf/directory checks that Development carried | 175 → 354 |
| `docs/architecture/06-agent-core.md` | Context fitting, retry, and compaction | merged duplicate | docs/development/02-naming-and-boundaries.md § "LLM-first affordances" | schema-is-the-SSOT sentence; Development owns the prompt-edit rule | 180 → 127 |
| `docs/architecture/06-agent-core.md` | Task lifecycle | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | the nomination facts (deferred_to_host_acceptance, authoritative=false) now stated once | 214 → 306 |
| `docs/architecture/06-agent-core.md` | Task lifecycle | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | second statement of the nomination | 256 → 57 |
| `docs/architecture/06-agent-core.md` | Review delivery | merged duplicate | docs/architecture/06-agent-core.md § "Caller-owned subscription model calls" | "the raw-model adapter is Codex" stated once | 120 → 81 |
| `docs/architecture/06-agent-core.md` | Review delivery | merged duplicate | docs/development/05-review-and-commit-protocol.md | advisory-continue / blocking-refuse / never-PASS rule; Development owns the gate rule | 184 → 149 |
| `docs/architecture/06-agent-core.md` | Review stack | rewritten explanation | docs/architecture/06-agent-core.md § "Review stack" | owner now states the merge-aware resolution and own-tree bootstrap that Development carried | 271 → 430 |
| `docs/architecture/07-configuration.md` | Default settings (OUROBOROS_REVIEWER_SLOTS) | rewritten explanation | docs/architecture/07-configuration.md § "Default settings" | the row said an empty value reads the retired comma keys; §11.4 and reviewer_slot_config.py say those keys are stripped at load and the shipped default panel serves | 80 → 134 |
| `docs/architecture/07-configuration.md` | Default settings (DeepSeek provider specifics) | rewritten explanation | docs/architecture/07-configuration.md (DeepSeek provider specifics) | owner now names the usage disclosure field Development carried | 83 → 113 |
| `docs/architecture/07-configuration.md` | Default settings (OUROBOROS_ACCEPTANCE_REVIEW_EST_SEC) | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | no-prediction / R23 clamp restated in the settings row | 343 → 196 |
| `docs/architecture/07-configuration.md` | Default settings (OUROBOROS_REVIEW_MAX_CYCLES) | rewritten explanation | docs/architecture/06-agent-core.md § "Review stack" | §10 invariant 17 itself points to §6 Review stack for the four meanings | 56 → 68 |
| `docs/architecture/08-git-branching-ci-and-build.md` | CI topology | rewritten explanation | docs/architecture/08-git-branching-ci-and-build.md § "CI topology" | owner now states the debt-laundering WHY Development carried | 56 → 114 |
| `docs/architecture/10-key-invariants.md` | Key Invariants (2) | rewritten explanation | docs/architecture/10-key-invariants.md invariant 2 | owner now lists the history-row and download-link carriers Development carried | 99 → 195 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (transcript_prefix.py row) | rewritten explanation | docs/architecture/01-high-level-architecture.md (transcript_prefix.py row) | owner now carries the measurement date Development carried | 44 → 65 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (review_execution.py row) | obsolete history | — | 'out of this change s scope' narrates one past commit; the residual and its issue stay | 128 → 100 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (plan_packet.py row) | obsolete history | — | design-wave codename with no current effect | 59 → 56 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (deep_self_review.py row) | obsolete history | — | narrated the retired error it replaced; the current rung stays | 187 → 137 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (reviewer_slot_config.py row) | obsolete history | — | "is gone" narration; owner citation kept | 184 → 130 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (review_execution.py row) | obsolete history | — | "is gone" narration; owner citation kept | 110 → 74 |
| `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (review_substrate.py row) | obsolete history | — | "are gone" narration; owner citation kept | 120 → 48 |
| `docs/architecture/03-web-ui-pages-and-buttons.md` | Settings and onboarding | obsolete history | docs/architecture/07-configuration.md (OUROBOROS_MODEL_DEEP_SELF_REVIEW row states where the row lives now) | narrated a removed UI field; the settings row states the current home | 180 → 138 |
| `docs/architecture/03-web-ui-pages-and-buttons.md` | Chat and Projects | obsolete history | — | "former … rejection" narration | 112 → 102 |
| `docs/architecture/03-web-ui-pages-and-buttons.md` | Chat and Projects | obsolete history | — | "former"/"remain" narration; the boundary itself stays | 65 → 55 |
| `docs/architecture/06-agent-core.md` | Delegated subagents (Claudexor transport + the nanny) | obsolete history | — | plan-item codename with no current effect | 87 → 69 |
| `docs/architecture/06-agent-core.md` | Caller-owned subscription model calls | obsolete history | — | "initial" narrates the adoption order | 63 → 55 |
| `docs/architecture/06-agent-core.md` | Review delivery | obsolete history | docs/architecture/11-frozen-contracts-v1.md § "11.4 Recent ABI Retirements" | retirement narrated beside the rule; §11.4 is the retirement ledger | 83 → 68 |
| `docs/architecture/07-configuration.md` | Default settings (OUROBOROS_REVIEWER_SLOTS) | obsolete history | — | plan-step codename | 74 → 68 |
| `docs/architecture/07-configuration.md` | Default settings (OUROBOROS_REVIEW_NATIVE_MAX_TRANSCRIPT_CHARS) | obsolete history | docs/architecture/11-frozen-contracts-v1.md § "11.4 Recent ABI Retirements" | retirement narrated beside the rule | 81 → 66 |
| `docs/architecture/12-host-service-companions-and-chat-ids.md` | Host Service, Companion Processes, and Chat IDs | obsolete history | — | "old … is removed" narration | 104 → 94 |
### Dispositions — `docs/development/`
| Chapter | Heading | Disposition | Retained owner | What changed | Chars before → after |
|---|---|---|---|---|---:|
| `docs/development/02-naming-and-boundaries.md` | CLI and headless work | merged duplicate | docs/architecture/06-agent-core.md § "Tool capability and execution" | credential fence locations, byte masker, PEM content evidence, restricted-read masking, unlisted-store rule | 2091 → 1292 |
| `docs/development/02-naming-and-boundaries.md` | Documentation contract | merged duplicate | docs/development/01-role-and-authority.md | handbook definition (rules by change class naming the enforcing surface) | 277 → 184 |
| `docs/development/02-naming-and-boundaries.md` | Task-authored messages are never owner text | merged duplicate | docs/architecture/06-agent-core.md § "Durable memory and project focus" | routing issuer definition (owner turn vs task speaking for itself) | 352 → 282 |
| `docs/development/02-naming-and-boundaries.md` | Task-authored messages are never owner text | merged duplicate | docs/architecture/06-agent-core.md § "Durable memory and project focus" | written/refused receipt, task_message_routed row, no chat told | 286 → 73 |
| `docs/development/02-naming-and-boundaries.md` | Anti-pattern: a chat id tested for truth | merged duplicate | docs/architecture/12-host-service-companions-and-chat-ids.md | HIDDEN_CHAT_ID partition definition | 284 → 230 |
| `docs/development/02-naming-and-boundaries.md` | Anti-pattern: a chat id tested for truth | merged duplicate | docs/architecture/05-supervisor-loop.md | browser-source Main addressing vs hidden API default | 140 → 228 |
| `docs/development/02-naming-and-boundaries.md` | Provider Independence | merged duplicate | docs/architecture/07-configuration.md (DeepSeek provider specifics) | DeepSeek tier projection map, forced tool choice thinking rule, live probe date; `reasoning_effort_clamped` name moves to the owner | 609 → 330 |
| `docs/development/02-naming-and-boundaries.md` | Provider Independence | merged duplicate | docs/architecture/06-agent-core.md § "Context fitting, retry, and compaction" | direct-OpenAI dialect ladder | 356 → 371 |
| `docs/development/02-naming-and-boundaries.md` | Provider Independence | merged duplicate | docs/architecture/06-agent-core.md § "Context fitting, retry, and compaction" | Anthropic native custody receipt | 357 → 260 |
| `docs/development/02-naming-and-boundaries.md` | Anti-pattern: content-derived identity for host-minted records | merged duplicate | docs/architecture/06-agent-core.md § "Durable memory and project focus" | retry binding transaction (bind_retry_to_origin_project) and immutability WHY | 787 → 296 |
| `docs/development/03-module-size-and-complexity.md` | Module Size & Complexity | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" | size-ratchet CI lane mechanics and no-history-replay WHY; the own-tree bootstrap fact moves to the owner | 860 → 655 |
| `docs/development/03-module-size-and-complexity.md` | Invariant: Projection over replay (hot readers of growing stores) | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Chat and Projects" | task_list_scan memo rules and SSE v2 cursor/rotation/handle discipline | 1486 → 426 |
| `docs/development/03-module-size-and-complexity.md` | Review presentation adapters | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Chat and Projects" | compact review rows carry no dollars; Skill attempt money only in lazy detail via physical_attempt_v1 | 308 → 171 |
| `docs/development/03-module-size-and-complexity.md` | Invariant: Continuation authority and bounded Main projection | merged duplicate | docs/architecture/01-high-level-architecture.md § "CLI / Headless Boundary" | predecessor_task_id contract, snapshot/restore retention, defensive projection outcomes | 1023 → 889 |
| `docs/development/03-module-size-and-complexity.md` | Invariant: UI resources carry a disposer | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Skills and Widgets" | ordered dispose-with-acknowledgement sequence | 422 → 258 |
| `docs/development/03-module-size-and-complexity.md` | Invariant: Embedded surfaces declare geometry and refresh semantics | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Skills and Widgets" | widget fault channel | 286 → 140 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Full availability in reasoning flows | merged duplicate | docs/architecture/06-agent-core.md § "Plan construction and review" | constitutional classification rule (affected_paths vs affected_resources vs evidence) | 412 → 406 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Full availability in reasoning flows (context-delivery registry) | merged duplicate | docs/architecture/06-agent-core.md § "Plan construction and review" | registry row restated the classification; "(W3)" wave codename dropped | 503 → 361 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Full availability in reasoning flows (context-delivery registry) | merged duplicate | docs/architecture/06-agent-core.md § "Deep self-review" | deep self-review delivery mechanics (probe/rebuild, receipt extents, coverage states, memory header) | 1027 → 704 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Full availability in reasoning flows | merged duplicate | docs/architecture/06-agent-core.md § "Plan construction and review" | two planning roots and named-omission rule | 724 → 589 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Full availability in reasoning flows | merged duplicate | docs/architecture/06-agent-core.md § "Plan construction and review" | SPEC field list, verdict/finding vocabulary, note-only and blocking-finding closure rules, no competing-plan quota | 2513 → 1313 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Full availability in reasoning flows | merged duplicate | docs/architecture/06-agent-core.md § "Context fitting, retry, and compaction" | Max/Low/Nano book projections and context_fit rendering | 1289 → 792 |
| `docs/development/04-core-governance-artifacts.md` | Invariant: Compaction must earn its rewrite | merged duplicate | docs/architecture/06-agent-core.md § "Context fitting, retry, and compaction" | compaction selection rules and no-reclaim rule | 527 → 358 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Review delivery" | AdvisoryRunRecord.execution binding, audited-skip custody, late results, native preflight custody | 1498 → 821 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Git and commit review" | staged fingerprint composition | 174 → 85 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Git and commit review" | post-commit re-read of the binding | 117 → 72 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Git and commit review" | managed-update resolution subject binding | 158 → 166 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" | scope assembler degradation ladder summary | 322 → 229 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Review delivery" | native episode bounds (transcript bound measurement, window clamp, ledger, typed exhaustion) | 794 → 207 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Review delivery" | advisory row enum parsing, final/telemetry.yaml identity, no extra engine panel | 890 → 371 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" | per-gate meanings of the shared review-cycle cap | 141 → 196 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | acceptance paid-stamp points, R52/R55/R23 pacing, deadline-cut residual, issue #588 residual | 1872 → 980 |
| `docs/development/05-review-and-commit-protocol.md` | Review & Commit Protocol | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" | the $0 exit shape and its four instances | 553 → 167 |
| `docs/development/05-review-and-commit-protocol.md` | Release sync | merged duplicate | docs/architecture/10-key-invariants.md (invariant 2) and docs/architecture/08-git-branching-ci-and-build.md § "Build scripts" | version-carrier list (history row + download links move to invariant 2), exact-tag download links, main promotion | 1248 → 660 |
| `docs/development/06-rules-by-change-class.md` | Live subagents | merged duplicate | docs/architecture/06-agent-core.md § "Delegated subagents (Claudexor transport + the nanny)" | WHY a false "spent nothing" terminal is the one forbidden direction | 190 → 172 |
| `docs/development/06-rules-by-change-class.md` | Live subagents | merged duplicate | docs/architecture/01-high-level-architecture.md (delegate_supervision.py row) | read-only poll anchor semantics, torn-tail quarantine, lock recovery mechanism | 1144 → 674 |
| `docs/development/06-rules-by-change-class.md` | Live subagents | merged duplicate | docs/architecture/06-agent-core.md § "Delegated subagents (Claudexor transport + the nanny)" | orphan apply containment relaxation | 522 → 348 |
| `docs/development/06-rules-by-change-class.md` | Cancellation and effective status | merged duplicate | docs/architecture/05-supervisor-loop.md (cancellation skeleton) and docs/architecture/10-key-invariants.md invariant 14 | fail-closed intent write, allow_settled_target, widen-only scope | 549 → 390 |
| `docs/development/06-rules-by-change-class.md` | Cancellation and effective status | merged duplicate | docs/architecture/05-supervisor-loop.md (cancellation skeleton) | completion-wins and reaper-is-not-a-cancel-ingress mechanism | 310 → 185 |
| `docs/development/06-rules-by-change-class.md` | Cancellation and effective status | merged duplicate | docs/architecture/10-key-invariants.md invariant 15 and docs/architecture/05-supervisor-loop.md | strict registry reads and durable task_done validation | 366 → 322 |
| `docs/development/06-rules-by-change-class.md` | Cancellation and effective status | merged duplicate | docs/architecture/05-supervisor-loop.md (stop policy / hurry) and docs/architecture/01-high-level-architecture.md (owner_hurry.py, task_hurry.py rows) | stop_policy axis semantics, hurry projection mechanics, non-chat event family | 1164 → 878 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/02-startup-onboarding-flow.md | wizard step list; Accounts shared / Models and Agents edit roles | 325 → 324 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Navigation and shared UI contracts" | model chooser native-select / arbitrary-id behaviour | 330 → 303 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/06-agent-core.md § "Caller-owned subscription model calls" | Auto-lane account preference and suppression | 625 → 465 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/02-startup-onboarding-flow.md | completion transaction contents, GET-never-persists WHY, post-disk failure reporting | 703 → 497 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/02-startup-onboarding-flow.md | served wizard page as ES module | 168 → 113 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/02-startup-onboarding-flow.md | three install-time proofs and their WHY | 554 → 425 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/01-high-level-architecture.md § "Gateway Boundary v1" | settings lock precondition, CommitBoundary, saved on both sides | 541 → 283 |
| `docs/development/06-rules-by-change-class.md` | Onboarding and Settings surfaces | merged duplicate | docs/architecture/02-startup-onboarding-flow.md | preset compiler roster rules (review-<harness> rows, validate-only roster) | 533 → 334 |
| `docs/development/06-rules-by-change-class.md` | Transport and late-result custody | merged duplicate | docs/architecture/06-agent-core.md § "Review delivery" | stream assembler completeness/form doctrine and SSE error-frame classification | 1504 → 833 |
| `docs/development/06-rules-by-change-class.md` | Transport and late-result custody | merged duplicate | docs/architecture/06-agent-core.md § "Review delivery" | late-completion receipt binding fields | 632 → 497 |
| `docs/development/06-rules-by-change-class.md` | Transport and late-result custody | merged duplicate | docs/architecture/06-agent-core.md § "Review delivery" | subscription-catalog reachability proof, HEAD connection allowance | 826 → 444 |
| `docs/development/06-rules-by-change-class.md` | Transport and late-result custody | merged duplicate | docs/architecture/06-agent-core.md § "Delegated subagents (Claudexor transport + the nanny)" | observation beat, per-class quiet reasons, once-per-episode disclosure | 680 → 423 |
| `docs/development/06-rules-by-change-class.md` | LLM call rules | merged duplicate | docs/architecture/06-agent-core.md § "Budget tracking" | attempt lifecycle, release-only-on-typed-pre-dispatch-failure, projections never a second authority | 551 → 337 |
| `docs/development/06-rules-by-change-class.md` | LLM call rules | merged duplicate | docs/architecture/06-agent-core.md § "Budget tracking" | ledger lock discipline and failed-settlement bound | 235 → 201 |
| `docs/development/06-rules-by-change-class.md` | LLM call rules | merged duplicate | docs/architecture/06-agent-core.md § "Budget tracking" | tree-spend pacing mechanics, wallet read, post-task frozen snapshot, acceptance-panel projection in synthesis prompts | 1761 → 860 |
| `docs/development/06-rules-by-change-class.md` | LLM call rules | merged duplicate | docs/architecture/06-agent-core.md § "Delegated subagents (Claudexor transport + the nanny)" | delegated cash cases and input_token_usage validation | 1261 → 773 |
| `docs/development/06-rules-by-change-class.md` | LLM call rules (cache-friendliness) | merged duplicate | docs/architecture/01-high-level-architecture.md (transcript_prefix.py row) | append-only transcript mechanism, prompt_prefix_break kinds, byte-prefix cache measurement (date moves to the owner) | 1189 → 560 |
| `docs/development/06-rules-by-change-class.md` | LLM call rules | merged duplicate | docs/architecture/06-agent-core.md § "Context fitting, retry, and compaction" | transport-death repeat rail (who, how often, ledger rows, round-record terminal) and upstream-observed continuation | 2808 → 1633 |
| `docs/development/06-rules-by-change-class.md` | Timeout & Wait Control | merged duplicate | docs/architecture/05-supervisor-loop.md (owner-wait paragraphs) | owner-wait capacity transfer, cold preparation (CostCeiling/ContextFit rebinding), resume locator consumption, direct-actor wait | 2410 → 1329 |
| `docs/development/06-rules-by-change-class.md` | Timeout & Wait Control | merged duplicate | docs/architecture/05-supervisor-loop.md (worker readiness paragraph) | readiness/exhaustion lifecycle, temporary slots, watcher release | 1186 → 610 |
| `docs/development/06-rules-by-change-class.md` | Timeout & Wait Control | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" | $0 exit shape restated for acceptance | 468 → 374 |
| `docs/development/06-rules-by-change-class.md` | Timeout & Wait Control | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" and § "Context fitting, retry, and compaction" | custody precedence, provider_outcome_unknown, interactive repeat rail restatement | 1369 → 634 |
| `docs/development/06-rules-by-change-class.md` | Timeout & Wait Control | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" and § "Review delivery" | retry-key identity, Skill Review wave reservation and review_resume_of, locked paid=True write | 1173 → 393 |
| `docs/development/06-rules-by-change-class.md` | Timeout & Wait Control | merged duplicate | docs/architecture/06-agent-core.md § "Review stack" | process-local custody tombstone, tokenless waiter settlement, pid-death owner-loss | 506 → 121 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | deferred_to_host_acceptance / authoritative=false nomination, host advance after the tool-result block, early settlement does not seal ingress | 528 → 310 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/06-agent-core.md § "Post-task reflection" | sealed final package contents and what tool success does not prove | 698 → 434 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/10-key-invariants.md (post-map paragraphs) and docs/architecture/06-agent-core.md § "Post-task reflection" | canonical-first persistence, native reader selector, CURRENT-basis publication, relocation rules | 2318 → 1465 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/05-supervisor-loop.md (pooled completion paragraph) | _files_prepared_attempt, terminal_task_files_ready, split-drive adoption | 783 → 475 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/05-supervisor-loop.md (terminal-file recovery paragraphs) | recovery ownership split, infra_failed fault policy, deferred replay | 1068 → 703 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/10-key-invariants.md (post-map paragraphs) | same-store copy rules and alias resolution | 459 → 372 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/05-supervisor-loop.md (pooled completion and startup recovery paragraphs) | mailbox cleanup predicate, startup source recovery and prune deferral | 545 → 305 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | acceptance delivery work orders, money rule, R52/R55/R23 pacing, deadline-cut residual (third copy) | 2277 → 777 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | the three acceptance decision states | 174 → 121 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | forced-rail terminalization and never-overwritten pair | 364 → 207 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/development/11-design-system.md (task outcome truth) and docs/architecture/03-web-ui-pages-and-buttons.md § "Chat and Projects" | shared outcome phase across Chat/Logs/Project rows/Telegram | 329 → 251 |
| `docs/development/06-rules-by-change-class.md` | Loop / State-Machine Changes | merged duplicate | docs/architecture/06-agent-core.md § "Task lifecycle" | dialogue_status vote reduction | 369 → 260 |
| `docs/development/07-managed-update-rule.md` | Managed Update Rule | merged duplicate | docs/architecture/02-startup-onboarding-flow.md | fresh-rescue WHY and replayed-rollback pointer semantics | 438 → 323 |
| `docs/development/09-process-custody-rule.md` | Process Custody Rule | merged duplicate | docs/architecture/01-high-level-architecture.md § "Runtime topology" | ledger path/role, strict fingerprint, skill-companion daemon-scope exception | 861 → 381 |
| `docs/development/09-process-custody-rule.md` | Process Custody Rule | merged duplicate | docs/architecture/09-shutdown-and-process-cleanup.md | daemon stop protocol (CLI stop receipt, forced fallback custody, HTTP refusal semantics, Windows empty-birth rows) and latch classification | 2938 → 1850 |
| `docs/development/09-process-custody-rule.md` | Process Custody Rule | merged duplicate | docs/architecture/09-shutdown-and-process-cleanup.md | latch release list | 447 → 272 |
| `docs/development/11-design-system.md` | Design System | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Chat and Projects" | taskReasonDetail precedence order | 246 → 155 |
| `docs/development/11-design-system.md` | Design System | rewritten explanation | docs/architecture/03-web-ui-pages-and-buttons.md § "Chat and Projects" | pointer re-pointed from the monolith to the chapter section | 122 → 139 |
| `docs/development/11-design-system.md` | Design System | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Chat and Projects" | 48 CSS-pixel live-edge zone | 119 → 189 |
| `docs/development/11-design-system.md` | Browser dialogs | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Navigation and shared UI contracts" | native-dialog WHY and openConfirmDialog mode contract | 546 → 226 |
| `docs/development/11-design-system.md` | Browser dialogs | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Navigation and shared UI contracts" | bindDialogFocus/bindMenu/bindPopoverPosition structure | 686 → 403 |
| `docs/development/11-design-system.md` | Declarative widgets | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Skills and Widgets" and docs/architecture/01-high-level-architecture.md (module tree) | module-frame CSP, Widgets module inventory, list-signature reconciliation | 988 → 415 |
| `docs/development/11-design-system.md` | Optional author controls | merged duplicate | docs/architecture/03-web-ui-pages-and-buttons.md § "Skills and Widgets" | author-kit recipe mechanics | 563 → 317 |
| `docs/development/12-mcp-client-integration.md` | MCP Client Integration | merged duplicate | docs/architecture/06-agent-core.md § "MCP and browser-facing external tools" | stdio contract, reference precedence, unknown-field warning, MCP_CONFIG_ERROR, auth_configured, secret masking | 1688 → 899 |
| `docs/development/13-gateway-boundary-pattern.md` | Gateway Boundary Pattern | merged duplicate | docs/architecture/12-host-service-companions-and-chat-ids.md | host attachment copy custody and operation correlation | 1004 → 658 |
| `docs/development/14-build-and-ci.md` | Python dependency locks | merged duplicate | docs/architecture/08-git-branching-ci-and-build.md § "Build scripts" | dependency authority structure | 425 → 240 |
| `docs/development/14-build-and-ci.md` | Pytest marker lanes | merged duplicate | docs/architecture/08-git-branching-ci-and-build.md § "CI topology" | secret-step ordering WHY | 347 → 195 |
| `docs/development/14-build-and-ci.md` | Pytest marker lanes | merged duplicate | docs/architecture/08-git-branching-ci-and-build.md § "CI topology" | base fallback rule; the debt-laundering WHY moves to the owner | 388 → 307 |
| `docs/development/14-build-and-ci.md` | The commit gate mirrors the CI split | merged duplicate | docs/architecture/06-agent-core.md § "Git and commit review" | preflight test proof binding and reuse rules | 1192 → 619 |
| `docs/development/02-naming-and-boundaries.md` | Mutable external-fact inventory | obsolete history | — | release stamp inside prose; the sentence is about the table, not a release | 81 → 88 |
| `docs/development/05-review-and-commit-protocol.md` | (introduction) | rewritten explanation | — | intro re-read: the carrier list now lives with invariant 2 | 104 → 89 |
| `docs/development/07-managed-update-rule.md` | (introduction) | rewritten explanation | — | intro re-read: the fresh-rescue WHY now lives in the startup chapter | 262 → 227 |
| `docs/development/09-process-custody-rule.md` | (introduction) | rewritten explanation | — | intro re-read: ledger/fingerprint mechanism and the stop protocol now live in Architecture | 259 → 261 |
| `docs/development/12-mcp-client-integration.md` | (introduction) | rewritten explanation | — | intro re-read: the stdio contract now lives in Architecture | 147 → 139 |
| `docs/development/14-build-and-ci.md` | (introduction) | rewritten explanation | — | intro re-read: the lock authority structure now lives in Architecture "Build scripts" | 149 → 122 |
### References re-pointed
| File | Heading | Disposition | Retained owner | What changed |
|---|---|---|---|---|
| `tests/test_v652_scratch_and_masking.py` | (test docstring) | reference re-pointed | docs/development/06-rules-by-change-class.md § "Loop / State-Machine Changes" | line-number reference replaced by the heading |
### Ported upstream edits (b6a7702b0)
Not part of the split or the subtraction: this records ordinary target drift.
Merging `managed/ouroboros` at `b6a7702b0` brought ten hunks that upstream wrote
into the two monoliths, which are entrypoints here. The entrypoints were kept
byte-for-byte and each hunk was applied once, at the chapter that owns the
section its surrounding text belongs to. None of the ten landed on a sentence
the subtraction had merged, so no hunk had to choose between two copies.
| Upstream hunk | Destination chapter | Heading |
|---|---|---|
| `review_owner_custody.py` module-tree row: owner-death batching, one off-lock event read, locked reconcile | `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (module tree, `ouroboros/`) |
| new `ouroboros/gateway/update_progress.py` module-tree row | `docs/architecture/01-high-level-architecture.md` | High-Level Architecture (module tree, `ouroboros/gateway/`) |
| Updates page: the executor's process-local stage observations, `update_progress_changed` invalidation, lock-free passive Git reads | `docs/architecture/03-web-ui-pages-and-buttons.md` | Dashboard |
| outbound envelope list gains `update_progress_changed` and its non-authority sentence | `docs/architecture/04-server-api-endpoints.md` | WebSocket protocol |
| new paragraph: the startup seal audit's bounded manifest set and shared archive-ID observation | `docs/architecture/06-agent-core.md` | Usage ledger substrate vs. accounting policy |
| paid-attempt locked write: the confirmed-dead PID batch, the empty-custody skip, one event snapshot then a locked reread | `docs/architecture/06-agent-core.md` | Review stack |
| Managed update ABI row gains `update_progress` and `update_progress_changed` | `docs/architecture/11-frozen-contracts-v1.md` | 11.1 What is frozen |
| new subsection "Shared behavior and data-flow changes" with its review-only enforcement | `docs/development/03-module-size-and-complexity.md` | Shared behavior and data-flow changes |
| projection-over-replay first bullet rewritten as whole-operation growth reasoning | `docs/development/03-module-size-and-complexity.md` | Invariant: Projection over replay (hot readers of growing stores) |
| item-24 trigger wording: data readers, startup/shutdown and other batch operations | `docs/development/03-module-size-and-complexity.md` | Invariant: Projection over replay (hot readers of growing stores) |

View file

@ -1,117 +0,0 @@
# ABI-3 — F11 per-alias inventory (gateway compat aliases, ABI 7.0 window)
Frozen BEFORE the first removal (lane D3, base 29e2b045). Axes per F11:
ingress / egress / JS / producer / stored / migration / removal-test.
This document is the ABI-3 feeder inventory for the RC auditor scope
(docs/v7next/DESIGN_RC_AUDIT_SCOPE.md); the removal tests live in
`tests/test_gateway_abi3_removals.py`.
Lane constraint (coordinator, 2026-08-31): `web/` JS is untouchable in this
lane (`chat.js` sits at its BYTE_DEBT ceiling). Verified consequence: NO alias
requires a functional web-client edit for its removal — the only JS artifacts
are stale JSDoc typedef lines and the `GATEWAY_CONTRACT_VERSION` carrier
switch in `web/modules/api_types.js`, both HOT-DEFERRED (evidence per alias
below), so no alias itself is deferred.
## 1–2. `cost_usd` / `cost_usd_with_children` (ChatOutbound)
- Declaration: `gateway/contracts.py` ChatOutbound (deprecated aliases beside
the honest `accounted_upper_bound_usd[_with_children]` names, C2/owner 10=B).
- ingress: none — outbound-only fields; no inbound surface declares them.
- egress: WS chat frames (task_done / heartbeat / progress / subagent meta)
exclusively through the cost SSOT emitters
`ouroboros/cost_projection.py::{with_cost_aliases, carry_cost_meta,
cost_projection, live_root_cost_projection}`; task-result open-shape
passthrough on `GET /api/tasks/{task_id}` for records stamped with the
alias; history replay of stored `chat.jsonl` rows via
`task_results.TASK_COST_META_FIELDS`.
- JS: `web/modules/utils.js` `resolveCostPair` resolves the pair with
deprecated-wins precedence AND an honest-name fallback. Removal DID need a
web edit, made in the stage-2 fix wave: `chat.js::costMetaKeys` copies the
retired names unconditionally, and nothing serializes them away on that
in-memory path (no JSON round-trip runs between the whitelist and the
reader), so an alias-free frame reached the reader with `cost_usd*` as own
properties valued `undefined`. The resolver read that as "the deprecated
name is present" and answered null, freezing a subagent card on
"cost pending" for the rest of the run. Presence there now means a DEFINED
value; an explicit `null` is still present (Python parity with `old in src`)
and a mirrored legacy amount still wins its pair. The JSDoc typedefs already
name the honest fields: `accounted_upper_bound_usd` and
`accounted_upper_bound_usd_with_children` at `api_types.js:287` and `:290`
on this tree (this sentence was written in the stage-2 fix wave, so unlike
the frozen-base citations elsewhere in this file it names current lines;
`:288/:291` were the two description lines under them).
- producer: `cost_projection.py` is the one author of the emitted pair;
`gateway/tasks.py:227` stamps `cost_usd=0.0` into the stored
admission-failure record; hand literals (`agent_task_pipeline.py`,
`supervisor/events_task_done.py:256`) flow through the SSOT seam.
- stored: `task_results/<id>.json`, `chat.jsonl` rows, and task_summary rows
carry historical alias keys. Read tolerance is KEPT: `resolve_cost_pair`
still reads both spellings (deprecated wins on a diverged stored pair) and
`TASK_COST_META_FIELDS` still carries stored legacy keys through history
replay. Replay of history is never rejected (stored axis, per plan).
- migration: none required — the additive honest names shipped in C2 and both
Python and JS readers already resolve the pair.
- removal: SSOT emitters stop emitting the deprecated spellings and STRIP them
from write-side copies (`with_cost_aliases` normalizes a producer literal
away); contracts.py drops the two ChatOutbound fields;
`gateway/tasks.py:227` stamps the honest name. Live-frame readers that
consumed the emitted alias key (`agent_task_pipeline.py:842/:853`) switch to
the honest name.
- removal-test: declaration gone from ChatOutbound hints; emitters emit no
alias key; deprecated-wins read tolerance for stored pairs pinned.
## 3. `telegram_chat_id` (ChatOutbound / PhotoOutbound / VideoOutbound / DocumentOutbound)
- Declaration: 4 `NotRequired[int]` fields in `gateway/contracts.py`
(deprecated compatibility twins; runtime emits `transport` instead).
- ingress: none — never declared on any inbound surface.
- egress: ONLY the history replay mapper `gateway/history.py` —
`:849` re-emits `int(entry.get("telegram_chat_id") or 0)` unconditionally
(every replayed frame carried `telegram_chat_id: 0`), `:381`/`:401`
hard-code `0` into synthesized origin rows. No live producer emits it.
- JS: JSDoc typedefs only (`api_types.js:333/:370/:389/:410`); zero
functional readers in `web/modules` — no web edit needed; typedef cleanup
HOT-DEFERRED (web/ untouchable).
- producer: none live (no runtime writer of the key into chat.jsonl since the
transport generalization).
- stored: historical `chat.jsonl` rows carry the key. Readers TOLERATE it
(the mapper reads-and-ignores after removal; replay is never rejected, and
ingress validation does not apply to replay — it is an egress path).
- migration: `transport` (`TransportMetadata`) shipped earlier as the
replacement.
- removal-test: declaration gone from all four outbound TypedDicts; a legacy
stored row WITH the key still replays (without re-emitting it); synthesized
rows carry no dead zero field.
## 4–5. `project_last_viewed` / `project_hidden` (UiPreferencesResponse)
- Declaration: `gateway/contracts.py` UiPreferencesResponse (marked
"deprecated one-minor accepted no-op" — the one-minor window has closed).
- ingress: `POST /api/ui/preferences` accepted them as LOUD no-ops
(`deprecated_ui_preferences_ignored` warning + zeroing). After removal the
existing unknown-key policy answers 400 — the documented end state of a
one-minor acceptance window.
- egress: always emitted as `{}` defaults on GET/POST responses.
- JS: JSDoc typedef only (`api_types.js:1017-1018`); the shipped client
neither reads nor sends the keys (it writes `project_seen_revision`) — no
web edit needed; typedef cleanup HOT-DEFERRED (web/ untouchable).
- producer: `gateway/ui_preferences.py` (defaults, normalize branch, POST
zeroing, `_legacy_keys`/`_deprecated_warning` machinery).
- stored: `state/ui_preferences.json` may carry legacy values; after removal
unknown stored keys are ignored on read by `_normalize_preferences`
construction (only known keys propagate) and dropped on the next write —
tolerated, never fatal.
- migration: `project_seen_revision` (shipped replacement).
- removal-test: fields gone from the response contract and payloads; POST
with a legacy key → 400 unknown-key; a stored legacy file still loads.
## HOT-DEFERRED (evidence, not removals)
- `web/modules/api_types.js` stale JSDoc typedef lines for all five aliases
(`:283`, `:291`, `:333`, `:370`, `:389`, `:410`, `:1017-1018`): comment-only
drift, zero runtime effect; web/ JS untouchable in this lane.
- `GATEWAY_CONTRACT_VERSION` in `api_types.js` (currently `'6.113.4'`) as the
JS-side ABI-version mirror: the server-side carrier
(`gateway.schema.GATEWAY_ABI_VERSION`) lands in this lane; the JS mirror
switch + sync pin follow when a web lane opens.

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@

View file

@ -10,7 +10,7 @@ Five pure queries whose consumer #1 is Ouroboros itself (self-evolution):
per ``docs/PERSISTENCE.md``;
- ``protected_contracts_affected(diff)`` — the protected surfaces
(``runtime_mode_policy`` inventories) and frozen-contract rows
(``docs/v7next/FROZEN_CONTRACTS_INVENTORY.md``) a change set touches.
(``docs/inventories/FROZEN_CONTRACTS_INVENTORY.md``) a change set touches.
Everything here is a pure function over data the repository already pins as
SSOT — the domain manifest, the generated inventories, and the protected-path
@ -18,8 +18,7 @@ inventories. No LLM, no caches, no ledgers: every reader takes an explicit
``repo_root``, reads the carrier files fresh, and raises a teaching
``ValueError`` when a carrier is missing or an argument is malformed. The
model consumes these through the existing ``query_code`` tool
(``op=architecture``) — the seam decision is recorded in the campaign ledger
(``docs/v7next/LEDGER_CORRECTIONS.md``, F5 lane C section).
(``op=architecture``), so code and architecture facts share one query surface.
"""
from __future__ import annotations
@ -34,7 +33,7 @@ from ouroboros.code_intelligence import CodeInventory, _resolve_relative_import
DOMAIN_MANIFEST_RELPATH = "ouroboros/domains.toml"
PERSISTENCE_DOC_RELPATH = "docs/PERSISTENCE.md"
FROZEN_INVENTORY_RELPATH = "docs/v7next/FROZEN_CONTRACTS_INVENTORY.md"
FROZEN_INVENTORY_RELPATH = "docs/inventories/FROZEN_CONTRACTS_INVENTORY.md"
ARCHITECTURE_FACTS = (
"owner_of",
@ -331,7 +330,7 @@ def facade_consumers(
if not targets:
raise ValueError(
f"{text} is not a facade module (no top-level noqa:F401 re-exports); "
"see docs/v7next/FACADE_INVENTORY.md for the facade list"
"see docs/inventories/FACADE_INVENTORY.md for the facade list"
)
else:
dotted_map = {_module_dotted(path): path for path in reexports}

View file

@ -196,8 +196,8 @@ from ouroboros.settings_integrity import ( # noqa: E402, F401 — public config
RESTART_EXIT_CODE = 42
PANIC_EXIT_CODE = 99
AGENT_SERVER_PORT = 8765
# --- Usage-ledger compaction policy (CPL4-C6, owner sanction 1A) -------------
# docs/v7next/DESIGN_USAGE_COMPACTION.md. Constants, not env knobs. Compact the
# --- Usage-ledger compaction policy -----------------------------------------
# docs/USAGE_COMPACTION.md. Constants, not env knobs. Compact the
# monetary ledger once its byte size reaches ~0.2s-per-cold-replay scale, well
# under the measured 20MB degradation point (USAGE_LEDGER_WARN_BYTES in
# context_budget.py), which stays as the broken-compaction regression tripwire.

View file

@ -1,6 +1,5 @@
# Production domain manifest — module -> domain, 1:1, for every runtime module
# (plan §7.1, CPL-1). The Ф0 report-only stage lived at scripts/v7next_domains.toml;
# this file is its production successor and the SSOT the domain gates read.
# Production domain manifest — module -> domain, 1:1, for every runtime module.
# This is the SSOT the domain gates read.
#
# Structure:
# - [domains] / [modules] / [classification] are HUMAN sections: the domain
@ -618,20 +617,11 @@ D20 = "Presence"
"supervisor/telemetry_events.py" = "D08"
[classification]
# The 80 new-upstream module placements this campaign derived rode here as
# `proposed` (owner review pending) until owner batch №9 №10=A (2026-09-01)
# accepted them as the 7.0 base; nine of them (the Presence family) had been
# approved earlier under 4.2=A. The assignment itself lives in [modules] and
# is unchanged by the acceptance — what is gone is the review marker, so
# docs/DOMAIN_MAP.md no longer stars any row. A future placement that is
# genuinely undecided goes back on this list rather than into [modules]
# silently.
# Module placements needing owner review are listed here and starred in
# docs/DOMAIN_MAP.md. Accepted assignments remain in [modules]; an undecided
# placement belongs on this list rather than being silently accepted.
proposed = []
# The Ф0 [split_pending]/[split_pending_leaves] sections are retired: every row
# was resolved during F2 (dispositions recorded in docs/v7next/LEDGER_CORRECTIONS.md,
# owner decisions 2026-08-31).
# --- BEGIN GENERATED (python scripts/check_domains.py --write) ---
# Factual dependency data of the live tree, pinned as baseline data.
# Regenerate with `python scripts/check_domains.py --write`; the diff of

View file

@ -675,14 +675,9 @@ def run_llm_loop(
_delegate_hold_close(tools, drive_logs=drive_logs, task_id=task_id, detail="loop_exit")
_cleanup_loop_resources(stateful_executor, exit_ctx)
# The v7 L-B split: the members below moved into cohesive leaves (module-size
# boundary). This tree keeps the FULL re-export surface: the tip consumer set
# (production callers and tests) still addresses every moved name at its
# historical ouroboros.loop binding, and the D33 call-time handle reads of the
# sibling leaves resolve through this module as the family rendezvous. The
# oracle's later L3-trimmed surface (RETIRED_FROM_LOOP) is a consumer-rebind
# wave, not part of the byte-preserving relocation (see LEDGER_CORRECTIONS,
# D01 lane).
# Cohesive leaves own the implementations below. Keep the full re-export
# surface: production callers and tests address these historical loop bindings,
# and sibling leaves resolve their call-time handles through this module.
from ouroboros.loop_messages import ( # noqa: E402, F401 -- intentional public re-exports
_emit_checkpoint_event,
_extract_plain_text_from_content,

View file

@ -1,9 +1,8 @@
"""CPL-5: the runtime invariant ``model-visible ⟺ logged`` for ``model_send``.
"""The runtime invariant ``model-visible ⟺ logged`` for ``model_send``.
The invariant binds exactly one object — the physical candidate payload at the
last host-controlled pre-transport seam (``llm_attempt._candidate_before_dispatch``)
— per the design note ``docs/v7next/DESIGN_MODEL_VISIBLE_LOGGED.md`` (narrowed
per roast finding F15):
— per the design note ``docs/MODEL_SEND_OBSERVABILITY.md``:
- **Forward** (``sent ⟹ logged``): every physical attempt persists a sealed
durable record of its exact send copy before dispatch (the ``model_send_seal``
@ -131,13 +130,12 @@ def persist_physical_candidate(
``persist_call`` refs describe the redacted-by-default CAS blob; the two
digest domains are deliberately labelled rather than equated.
The manifest carries the CPL-5 ``model_send_seal`` block (additive key under
The manifest carries the ``model_send_seal`` block (additive key under
the existing SCHEMA_VERSION object; readers ignore unknown keys), and the
returned ``manifest_ref`` is stamped ``model_send_seal_version`` so the
accounting row it lands on names its attempt as seam-sealed — the join key
the reverse reconciliation sweep enforces. (Moved here whole from
``observability.py`` at its module-size ceiling; that module keeps the
historical compatibility name.)
the reverse reconciliation sweep enforces. ``observability.py`` keeps the
compatibility export so existing callers use this same implementation.
"""
from ouroboros.anthropic_native_custody import physical_custody_projection
from ouroboros.observability import persist_call

View file

@ -4,11 +4,9 @@ The live-task census the restart drain consults, the teardown arguments that
finalize interrupted tasks with an honest reason, the managed-update guard on
preserving queued work, the checkout/update serialization gate, the owned-work
stop of the owner's manual Restart, the planned restart's engine-pin daemon stop,
and the event bus shutdown. The restart
transaction itself — the deferred drain record and the performer that raises
the exit signal — stays in ``server.py`` for now: the upstream delegation
train coupled it to the composition root through the planned-handoff
transaction id (see docs/v7next/LEDGER_CORRECTIONS.md, D11).
and the event bus shutdown live here. The restart transaction itself stays in
``server.py``: its planned-handoff transaction id joins the deferred drain record
to the performer that raises the exit signal.
"""
from __future__ import annotations

View file

@ -158,7 +158,7 @@ BAND_PATHS = {
"ouroboros/tools/delegate.py": "D07 finisher DEL1 split brought the nanny-verb monolith DOWN from the 1600 hard cap into the band (1600->1263); terminal-evidence family extracted to tools/delegate_terminal_evidence.py, shrink-only direction",
"ouroboros/tools/plan_review.py": "Entered the band from 999 lines: the required-affected_paths form (owner 9=A) added the schema field and the PLAN_RESOURCE_FORM_REQUIRED refusal, which must name the task's open wave and the $0 disposition exit \u2014 it belongs beside the one preamble both the paid and dry-run paths share, not in the pure plan_spec companion that owns no task state.",
"ouroboros/tools/plan_review_runtime.py": "Entered the band from 986 lines: timeout custody synthesis joined the existing plan-review runtime owner while preserving profile-continuity disclosures and typed health facts during target integration.",
"ouroboros/tools/registry_core.py": "F3.1 typed-organ re-split (D04 rows 156/167/170/171/174/175): the tip ToolRegistry class body re-homed whole from the protected registry facade; the guard/dispatch surface already left for its sibling leaves, and the class shrinks further only with the ABI-8 post-release handler conversion.",
"ouroboros/tools/registry_core.py": "ToolRegistry owns the registry class behind the protected facade; guard and dispatch implementations live in sibling leaves.",
"ouroboros/tools/review.py": "D06 F2.3a re-entry by extraction: the multi-model fan-out moved to review_multi_model.py (1550->1269); the remaining single-owner review cycle machinery lands in the 1001-1500 band with headroom",
"ouroboros/tools/review_context_atlas.py": "Grew INTO the band by the #284 pack-arithmetic fixes: measured render charged at admission, exact per-row costs, target capped at the hard rail, honest eviction diagnostics \u2014 all in the module that owns the arithmetic.",
"ouroboros/tools/skill_exec.py": None,
@ -168,7 +168,6 @@ BAND_PATHS = {
"ouroboros/usage_compaction.py": "Entered the band from 971 lines: the C6 round-4 fixes homed here \u2014 dir-fd/O_NOFOLLOW anchoring of the archive writer and reader (a link planted after any path check cannot receive or serve monetary history) and the swap's last-instant snapshot re-proof inside the atomic replace \u2014 defenses that belong beside the compaction pass they defend.",
"ouroboros/workspace_executor.py": None,
"scripts/claudexor_platform_smoke.py": "The managed Claudexor platform smoke owns a multi-platform fixture, lifecycle receipt, and cleanup proof; keeping this runner in the documented band preserves the release gate without moving those checks into product runtime.",
"scripts/v7next_transplant.py": "F0 phase-review CRITICAL hardening: whole-leaf runtime invariants (handle existence/shape, declared/preamble disjointness, unread-declared) added to the byte-proof verifier; campaign tool retires with F6, not a runtime module",
"skills/telegram/plugin.py": None,
"skills/telegram/scripts/companion.py": None,
"skills/telegram/scripts/sidecar.py": None,

View file

@ -1,9 +1,7 @@
"""Who is acting and where each resource root physically lives.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations
@ -27,7 +25,7 @@ def _tool_access():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros import tool_access

View file

@ -1,9 +1,7 @@
"""The closed access vocabulary and the profile x root x operation policy matrix.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations

View file

@ -1,9 +1,7 @@
"""The user_files confinement: secret-name policy and path resolution.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations
@ -23,7 +21,7 @@ def _tool_access():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros import tool_access
@ -237,7 +235,7 @@ class UserFilesPathBlockedError(ValueError):
the typed ``⚠️ USER_FILES_PATH_BLOCKED`` prefix so the outcome axis can
partition it into ``execution.policy_denials`` (v6.57.0) instead of the
generic ``error`` status that falsely degraded a shipped task to
``tool_failure`` (the submarine wave-3 incident)."""
``tool_failure``."""
def resolve_user_file_path(

View file

@ -1,12 +1,9 @@
"""Evolution campaign authority at the reviewed-commit and publication
boundaries, split out of ``ouroboros/tools/git.py`` (v7 module-size
discipline). Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py; the parent re-exports every moved name.
Parent-scope helpers the monolith read as module globals are read through
the call-time handle ``_git()`` — never a from-import — so the facade
binding stays the one tests monkeypatch. ``_sanitize_git_error`` is the one
f-string-read exception (the byte gate cannot rewrite f-string internals):
it binds the plumbing owner at import time.
"""Evolution campaign authority at the reviewed-commit and publication boundaries.
The ``ouroboros/tools/git.py`` facade re-exports these definitions. Shared
helpers are read through the call-time handle ``_git()`` so the facade binding
stays the one tests monkeypatch. ``_sanitize_git_error`` binds the plumbing
owner at import time, including calls from f-strings.
"""
from __future__ import annotations
@ -28,7 +25,7 @@ def _git():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import git

View file

@ -1,13 +1,10 @@
"""Low-level git plumbing shared by the git tool owners.
Runtime-mode projection, git error sanitisation, staging hygiene, the
cross-process git lock, and the resolved-binding path projections that every
git tool leaf builds on. Split out of ``ouroboros/tools/git.py`` (v7
module-size discipline); every span is extracted VERBATIM from the parent's
tip bytes by scripts/v7next_transplant.py and the parent re-exports every
moved name. Parent-scope helpers the monolith read as module globals are
read through the call-time handle ``_git()`` — never a from-import — so the
facade binding stays the one tests monkeypatch.
cross-process git lock, and resolved-binding path projections live here.
The ``ouroboros/tools/git.py`` facade re-exports these definitions. Shared
helpers are read through the call-time handle ``_git()`` so the facade binding
stays the one tests monkeypatch.
"""
from __future__ import annotations
@ -30,7 +27,7 @@ def _git():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import git

View file

@ -1,10 +1,8 @@
"""Uncommitted repo write and exact-match edit surface, split out of
``ouroboros/tools/git.py`` (v7 module-size discipline). Every span is
extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py; the parent re-exports every moved name.
Parent-scope helpers the monolith read as module globals are read through
the call-time handle ``_git()`` — never a from-import — so the facade
binding stays the one tests monkeypatch.
"""Uncommitted repo write and exact-match edit surface.
The ``ouroboros/tools/git.py`` facade re-exports these definitions. Shared
helpers are read through the call-time handle ``_git()`` so the facade binding
stays the one tests monkeypatch.
"""
from __future__ import annotations
@ -23,7 +21,7 @@ def _git():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import git

View file

@ -1,12 +1,10 @@
"""Generic VCS inspection and rollback operations, split out of
``ouroboros/tools/git.py`` (v7 module-size discipline). Every span is
extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py; the parent re-exports every moved name.
Parent-scope helpers the monolith read as module globals are read through
the call-time handle ``_git()`` — never a from-import — so the facade
binding stays the one tests monkeypatch. ``_sanitize_git_error`` and
``format_protected_paths`` are the f-string-read exceptions (the byte gate
cannot rewrite f-string internals): they bind their owners at import time.
"""Generic VCS inspection and rollback operations.
The ``ouroboros/tools/git.py`` facade re-exports these definitions. Shared
helpers are read through the call-time handle ``_git()`` so the facade binding
stays the one tests monkeypatch. ``_sanitize_git_error`` and
``format_protected_paths`` bind their owners at import time, including calls
from f-strings.
"""
from __future__ import annotations
@ -28,7 +26,7 @@ def _git():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import git

View file

@ -1,9 +1,7 @@
"""Process/shell guard helpers: self-change tripwires, read-only inspection classification and light-mode repo snapshots.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations
@ -38,7 +36,7 @@ def _registry():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import registry

View file

@ -1,9 +1,7 @@
"""Host-owned pre-dispatch guards: capability/resource, managed-update and skill-payload constraints.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations
@ -34,7 +32,7 @@ def _registry():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import registry
@ -329,7 +327,7 @@ def _disabled_tools(ctx: Any) -> frozenset:
# too (harmless: nothing registers it), so old contracts round-trip as-is.
if "claude_code_edit" in names:
names.add("delegate_start")
# Q1 rename compatibility: contracts that withheld `advisory_review` keep
# Rename compatibility: contracts that withheld `advisory_review` keep
# withholding the SAME organ under its new name, and vice versa (a new
# contract naming only the new spelling must also silence the alias).
if "advisory_review" in names:
@ -793,7 +791,7 @@ def _shell_git_and_runtime_block(
"""Direct-git-via-shell policy + the external-workspace runtime/secret read
guard. External workspaces AND the default (non-workspace) lane get full
task-local git through ONE target-aware resolver — only the Ouroboros
runtime is protected (Q4=A unwind, 2026-08-08) — while raw non-git shell
runtime is protected — while raw non-git shell
in external workspaces still cannot read the runtime/secrets;
self_worktree keeps the strict read-only git policy."""
if not _registry().shell_argv(raw_cmd):
@ -866,7 +864,7 @@ def _shell_git_and_runtime_block(
),
)
return None
# DEFAULT (non-workspace) lane. Q4=A (owner 2026-08-08): mutating git
# DEFAULT (non-workspace) lane: mutating git
# is free EVERYWHERE outside the Ouroboros runtime. The argv-text
# blanket is replaced by the SAME target-aware resolver the external
# lane runs since v6.27: read-only git allowed even at a runtime

View file

@ -1,9 +1,7 @@
"""Concrete per-task context shared by tool handlers and the registry facade.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations
@ -31,7 +29,7 @@ def _registry():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import registry

View file

@ -1,9 +1,7 @@
"""Argument normalization and physical target binding for tool dispatch.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
The facade re-exports these definitions so existing imports and monkeypatch
targets retain the same bindings.
"""
from __future__ import annotations
@ -36,7 +34,7 @@ def _registry():
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
at import time.
"""
from ouroboros.tools import registry

View file

@ -1,6 +1,6 @@
"""Seq-preserving compaction of the monetary usage ledger (CPL4-C6, owner 1A).
"""Seq-preserving compaction of the monetary usage ledger.
Design contract: docs/v7next/DESIGN_USAGE_COMPACTION.md. Terminal, non-review
Design contract: docs/USAGE_COMPACTION.md. Terminal, non-review
``kind="attempt"`` chains fold into a stamped baseline block (one
``usage_baseline`` header + per-attribution ``usage_baseline_group`` rows);
the raw pre-compaction bytes move verbatim into an append-only
@ -999,7 +999,7 @@ def maybe_compact_usage_ledger_locked(
return False
# --- History readers (CPL-5 reverse-sweep join surface; audits) --------------
# --- History readers (model-send reverse-sweep join surface; audits) --------------
def _live_baseline_header(root: pathlib.Path) -> Optional[Dict[str, Any]]:
@ -1013,7 +1013,7 @@ def _live_baseline_header(root: pathlib.Path) -> Optional[Dict[str, Any]]:
tell those apart and does not try — the archive does, in the epoch anchor,
which runs on a stamp-less file too. A row that cannot be read AT ALL is
corruption and says so: reporting it as "not compacted" would hand the
CPL-5 sweep an empty archive and let it call a folded attempt an orphan
model-send reconciliation sweep an empty archive and let it call a folded attempt an orphan
seal.
"""
try:
@ -1136,7 +1136,7 @@ def _load_segment(
break
chunks.append(chunk)
payload = b"".join(chunks)
except OSError as exc: # the CPL-5 sweep maps typed corruption to UNKNOWN; a bare OSError escapes it
except OSError as exc: # the model-send reconciliation sweep maps typed corruption to UNKNOWN; a bare OSError escapes it
raise UsageLedgerCorrupt(f"usage archive segment unreadable: {path}") from exc
finally:
os.close(fd)
@ -1266,7 +1266,7 @@ def archived_attempt_ids(root: pathlib.Path | str | None = None) -> frozenset:
Segments are immutable, so per-segment reads and the union over a given
chain are cached. An unreadable (or not-a-regular-file), hash-mismatched,
mis-stepped, cyclic or out-anchored chain raises ``UsageLedgerCorrupt`` —
the CPL-5 reverse sweep must treat that as its existing UNKNOWN /
the model-send reverse sweep must treat that as its existing UNKNOWN /
skip-pass state, never as evidence of an orphan."""
root = pathlib.Path(_drive_root(root))
live_header = _live_baseline_header(root)
@ -1349,7 +1349,7 @@ def usage_attempt_recorded(
"""Membership of ``attempt_id`` in the live replay ∪ archived segments.
The join primitive for per-attempt history questions on a compacted
ledger (CPL-5 reverse sweep: an id absent HERE — not merely absent from
ledger (model-send reverse sweep: an id absent HERE — not merely absent from
the live replay — is what "no attempt row" means)."""
attempt_id = str(attempt_id or "")
if not attempt_id:

View file

@ -33,7 +33,7 @@ log = logging.getLogger(__name__)
LEDGER_REL = pathlib.Path("state/usage_attempts.jsonl")
QUARANTINE_REL = pathlib.Path("state/usage_attempts.quarantine.jsonl")
LOCK_REL = pathlib.Path("state/usage_attempts.lock") # the ONE monetary lock
# The ONE directory a baseline header may name (CPL4-C6). The substrate owns
# The ONE directory a baseline header may name. The substrate owns
# it because the substrate is what decides a row is well formed: a reference
# out of this directory is corruption, not a reader's problem.
ARCHIVE_SEGMENT_DIR_REL = pathlib.Path("archive/usage_ledger")
@ -127,7 +127,7 @@ def valid_archive_rel(value: Any) -> bool:
def _validate_baseline_header(row: Dict[str, Any], sequence: int) -> None:
"""Provenance checks on the compaction stamp (CPL4-C6).
"""Provenance checks on the compaction stamp.
The header claims a summary of bytes that are no longer in this file, so
its claim must be checkable WITHOUT reading them: a bounded archive path,
@ -251,7 +251,7 @@ def _write_bytes_atomic_fsync(
because a proof taken before a refused attempt is stale by the next one —
the last instant each replace can still be refused. A ``False`` answer
cleans up the temp file and returns ``False`` with the destination
untouched (CPL4-C6: the compactor re-proves lock ownership and that the
untouched (the compactor re-proves lock ownership and that the
live ledger is still the snapshot it folded, INSIDE the swap, so neither a
lost hold nor an append landing between attempts is erased by a rename)."""
path.parent.mkdir(parents=True, exist_ok=True)
@ -327,7 +327,7 @@ def _validate_records(
is mutated in place as the tail validates). Defaults reproduce the historic
whole-ledger behavior exactly.
Baseline rows (CPL4-C6 compaction, docs/v7next/DESIGN_USAGE_COMPACTION.md)
Baseline rows (docs/USAGE_COMPACTION.md)
are legal ONLY as the leading block of a from-scratch validation: exactly
one ``usage_baseline`` header at seq 1, ``usage_baseline_group`` rows
joined to it by ``baseline_id``. The compactor rewrites the whole file

View file

@ -124,7 +124,7 @@ def render_domain_map(manifest: Manifest) -> str:
mods_by_domain[manifest.modules[path]].append(path)
L: list[str] = []
L.append("# Domain map — v7next")
L.append("# Domain map")
L.append("")
L.append("Generated from `ouroboros/domains.toml` by `python scripts/check_domains.py"
" --write`. Do not edit — edit the manifest and regenerate;"
@ -171,8 +171,8 @@ def render_domain_map(manifest: Manifest) -> str:
L.append("The strict domain quotient is **acyclic** (`cycle_groups = []`).")
else:
L.append(f"{len(manifest.cycle_groups)} pinned cycle group(s) — the SCC ceiling;"
" the target is zero. Witness-level detail lives in"
" `docs/v7next/DOMAIN_QUOTIENT_REPORT.md`.")
" the target is zero. Generate witness-level detail with"
" `python scripts/domain_report.py`.")
L.append("")
for i, group in enumerate(manifest.cycle_groups, 1):
L.append(f"- group {i} ({len(group)} domains): {' ⇄ '.join(group)}")

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Shared import-graph core for the domain manifest tools (plan §7.1, CPL-1).
"""Shared import-graph core for the domain manifest tools.
Single home of the machinery that both the report generator
(``scripts/v7next_domain_report.py``) and the gate checker
(``scripts/domain_report.py``) and the gate checker
(``scripts/check_domains.py``) consume, so the two tools cannot drift apart —
the same discipline the checker itself enforces on runtime code (the
literal-copy ban).
@ -17,8 +17,7 @@ Provides:
- the domain quotient (cross-domain edges keyed by domain pair, with exact
module-edge witnesses) and Tarjan SCC over domain nodes;
- the literal-copy scan: normalized function-body source segments appearing
in more than one domain (the span-normalization approach follows
``scripts/v7next_transplant.py``: exact source segments, not name matching).
in more than one domain (exact source segments, not name matching).
This is analysis tooling, not runtime code: nothing under ``ouroboros/``
imports it.
@ -43,7 +42,7 @@ MANIFEST_PATH = REPO_ROOT / "ouroboros" / "domains.toml"
DOMAIN_MAP_PATH = REPO_ROOT / "docs" / "DOMAIN_MAP.md"
STRICT, TYPE_ONLY, LAZY, DYNAMIC = "strict", "type_checking", "lazy", "dynamic"
# Executed at import time but failure-tolerant / entrypoint-only (F0 review F4):
# Executed at import time but failure-tolerant / entrypoint-only:
# a `try: import x except ImportError/Exception` or an import under
# `if __name__ == "__main__"` must not stand as a strict cycle witness.
GUARDED = "guarded"
@ -86,8 +85,8 @@ def try_swallows_import_failure(node: ast.Try) -> bool:
"""True when at least one handler catches import failure (or everything)
AND does not re-raise. A handler whose body contains a top-level ``raise``
may propagate the failure (``except ImportError: raise``), so it is not a
swallow — misclassifying it as guarded would hide a strict cycle witness
(F0 review round 2). A conditional re-raise nested in an ``if`` still
swallow — misclassifying it as guarded would hide a strict cycle witness.
A conditional re-raise nested in an ``if`` still
counts as re-raising here: erring toward STRICT is the safe direction."""
for h in node.handlers:
reraises = any(isinstance(s, ast.Raise) for s in ast.walk(h))

View file

@ -1,8 +1,7 @@
#!/usr/bin/env python3
"""Report-only domain quotient graph for the v7next integration tree.
"""Report-only domain quotient graph for the current source tree.
Reads ``ouroboros/domains.toml`` (module -> domain, 1:1; the production
manifest — the Ф0 stage lived at scripts/v7next_domains.toml), computes the
Reads ``ouroboros/domains.toml`` (module -> domain, 1:1), computes the
import graph of THIS tree through the shared ``scripts/domain_graph.py`` core,
collapses modules to domain nodes and REPORTS:
@ -12,7 +11,7 @@ collapses modules to domain nodes and REPORTS:
- lazy / guarded / dynamic / TYPE_CHECKING imports, classified separately and
excluded from the strict graph.
This tool NEVER gates (roast F17 discipline): the GATE over the same data is
This tool NEVER gates: the GATE over the same data is
``scripts/check_domains.py`` + ``tests/test_domain_manifest.py``, which pin
the manifest's generated baseline sections. The report stays the witness-level
companion: exit code is 0 whenever the report was produced, regardless of
@ -20,11 +19,12 @@ findings. Exit 2 only when the report itself cannot be trusted
(unreadable/unparseable source, missing manifest) — a silent skip would
falsify the graph.
Output: ``docs/v7next/DOMAIN_QUOTIENT_REPORT.md`` (generated; header names the
generator and the input SHAs).
Output goes to stdout by default, or to an explicit ``--output PATH``.
Diagnostics go to stderr. The report header names the generator and input SHAs.
"""
from __future__ import annotations
import argparse
import hashlib
import pathlib
import subprocess
@ -48,10 +48,13 @@ from scripts.domain_graph import ( # noqa: E402
tracked_population,
)
REPORT = REPO_ROOT / "docs" / "v7next" / "DOMAIN_QUOTIENT_REPORT.md"
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--output", type=pathlib.Path,
help="write the report to this path instead of stdout")
args = ap.parse_args(argv)
def main() -> int:
if not MANIFEST_PATH.is_file():
print(f"missing manifest: {MANIFEST_PATH}", file=sys.stderr)
return 2
@ -71,8 +74,8 @@ def main() -> int:
tracked_set = tracked_population(REPO_ROOT)
# Content fingerprint of the actual analyzed inputs (working-tree bytes,
# not the HEAD claim): the report is often generated pre-commit, so HEAD
# alone cannot bind it to a SHA (F0 review round 2, F3). Verification
# against any later tree = regenerate there and compare fingerprints.
# alone cannot bind it to a SHA. Verification against any later tree =
# regenerate there and compare fingerprints.
fp = hashlib.sha256()
for path in sorted(tracked_set):
f = REPO_ROOT / path
@ -104,13 +107,12 @@ def main() -> int:
dom_counts = Counter(dom_of_path.values())
L: list[str] = []
L.append("# Domain quotient report — v7next (report-only)")
L.append("# Domain quotient report (report-only)")
L.append("")
L.append(f"Generated by `scripts/v7next_domain_report.py` on {now}. Do not edit.")
L.append(f"Generated by `scripts/domain_report.py` on {now}. Do not edit.")
L.append("")
L.append(f"- generated from the WORKING TREE based on HEAD `{head}` (the report is "
"typically written pre-commit, so this HEAD is the parent of the commit "
"that carries it, not that commit itself)")
L.append(f"- generated from the WORKING TREE based on HEAD `{head}` "
"(uncommitted input changes are covered by the content fingerprint below)")
L.append(f"- analyzed-inputs content fingerprint: sha256 `{content_fingerprint}` "
"(sorted tracked runtime files, path+bytes; regenerate on any tree and "
"compare to verify freshness)")
@ -241,14 +243,21 @@ def main() -> int:
# trailing separators and write exactly one final newline.
while L and not L[-1]:
L.pop()
REPORT.parent.mkdir(parents=True, exist_ok=True)
REPORT.write_text("\n".join(L) + "\n", encoding="utf-8")
print(f"wrote {REPORT}")
print(f"strict: {len(edges[STRICT])} module edges, {len(dom_edges)} domain edges, {len(cycles)} cycle groups")
report = "\n".join(L) + "\n"
if args.output is None:
sys.stdout.write(report)
else:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(report, encoding="utf-8")
print(f"wrote {args.output}", file=sys.stderr)
print(f"strict: {len(edges[STRICT])} module edges, {len(dom_edges)} domain edges, {len(cycles)} cycle groups",
file=sys.stderr)
for comp in cycles:
print(" cycle:", " ⇄ ".join(comp))
print(" cycle:", " ⇄ ".join(comp), file=sys.stderr)
return 0
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
sys.exit(main())

View file

@ -1,25 +1,21 @@
#!/usr/bin/env python3
"""ABI 7.0 RC auditor (ABI-7b, F13): pre-upgrade scan of a third-party install.
"""ABI 7.0 RC auditor: pre-upgrade scan of a third-party install.
The migration-window instrument of owner decision Q6=A: point it at an
install's DATA ROOT (the directory holding ``settings.json``, ``skills/``,
``state/``, ``task_results/``) and it names every ABI-7.0 incompatibility with
Point the migration-window instrument at an install's DATA ROOT (the directory
holding ``settings.json``, ``skills/``, ``state/``, ``task_results/``) and it names every ABI-7.0 incompatibility with
its migration BEFORE the owner upgrades. It is strictly READ-ONLY over the
audited install — it never writes, moves, creates, or locks anything there
(the report file, when requested, is refused inside the audited root).
Scope (docs/v7next/DESIGN_RC_AUDIT_SCOPE.md) is the UNION of the frozen F3
lane inventories, emitted as one machine-readable JSON document
(``--scope``): abi "7.0", sources (tree SHA + the SHA the feeder inventories
Scope combines the ABI retirement inventories, emitted as one machine-readable
JSON document (``--scope``): abi "7.0", sources (tree SHA + the SHA the feeder inventories
were frozen at), and ``checks[]`` of exactly five classes:
- ``gateway-alias`` — the five removed gateway compat aliases
(docs/v7next/ABI3_GATEWAY_ALIAS_INVENTORY.md, F11 axes). Stored rows stay
read-tolerated BY DESIGN, so on-disk hits are notes; live clients are
- ``gateway-alias`` — the five removed gateway compat aliases, listed in
``_GATEWAY_ALIASES`` below. Stored rows stay read-tolerated BY DESIGN, so on-disk hits are notes; live clients are
owner attestation.
- ``retired-setting`` — keys a release deleted (``RETIRED_SETTING_KEYS``,
ABI-5/Q10 and D04 plus earlier retirements): stripped-on-load, value
inert. ``since`` separates this window's own removals
- ``retired-setting`` — keys a release deleted (``RETIRED_SETTING_KEYS``):
stripped-on-load, value inert. ``since`` separates this window's own removals
(``RETIRED_IN_THIS_ABI``) from ones that were already inert.
- ``comma-list`` — the ABI-10 reviewer comma-list / route keys
(``RETIRED_COMMA_LIST_SETTING_KEYS``, snapped from settings_defaults at
@ -31,11 +27,11 @@ were frozen at), and ``checks[]`` of exactly five classes:
is refused via ``extension_new_pass_admission_error``).
- ``schema-stamp`` — ABI-2: durable task results require
``_schema_version: 1``; pre-7.0 history is QUARANTINED after upgrade
(owner decision Q8=B, BY DESIGN — no converter exists; manual recovery
(by design — no converter exists; manual recovery
only: re-stamp and move the file back).
Everything not machine-checkable is an OWNER ATTESTATION list the auditor
prints verbatim (F13: no pretend-coverage).
prints verbatim, without claiming automatic verification.
Exit codes: 0 = clean, 1 = incompatibilities found, 2 = install unreadable or
the audit itself failed (traversal/report-write OSError, or the RuntimeError
@ -122,12 +118,12 @@ from ouroboros.task_result_schema import ( # noqa: E402
ABI = "7.0"
# The base SHA at which every feeder inventory of this scope was frozen and
# landed (ABI-3 doc, ABI-5/ABI-10 RETIRED_SETTING_KEYS, ABI-1 admission facts,
# ABI-2 stamp semantics) — the F3.3 serial-tail base.
# ABI-2 stamp semantics).
INVENTORIES_FROZEN_AT = "4fa2f01abc02e7f68ee3ce0e3c7931046fc92173"
# Retirements this ABI window itself performs, as opposed to the ones it merely
# inherits: the P3 scope-review floor (Q10=A) and D04's flat wall-clock timeout
# pair (owner 1B). An upgrading install reads the difference as "your stored
# inherits: the P3 scope-review floor and the flat wall-clock timeout
# pair. An upgrading install reads the difference as "your stored
# value stopped working in THIS upgrade" versus "it was already inert".
RETIRED_IN_THIS_ABI = frozenset({
"OUROBOROS_SCOPE_REVIEW_FLOOR",
@ -140,7 +136,7 @@ SEV_NOTE = "note"
_MANIFEST_NAMES = ("SKILL.md", "skill.json")
# ABI-3 feeder: docs/v7next/ABI3_GATEWAY_ALIAS_INVENTORY.md (frozen, F11 axes).
# Removed gateway aliases and their durable-read compatibility.
_GATEWAY_ALIASES: List[Dict[str, str]] = [
{
"id": "gateway-alias",
@ -187,7 +183,7 @@ _GATEWAY_ALIASES: List[Dict[str, str]] = [
_UI_PREFERENCES_LEGACY_KEYS = ("project_last_viewed", "project_hidden")
_STORED_COST_ALIAS_KEYS = ("cost_usd", "cost_usd_with_children", "telegram_chat_id")
# ABI-5 (Q10) knobs removed WITHOUT an install-visible settings key: named in
# Knobs removed WITHOUT an install-visible settings key: named in
# the scope prose and the schema-stamp/attestation planes, never as key checks.
_REMOVED_KNOBS_PROSE = (
"fail_tasks: the budget-drain batch terminalizer is removed with no "

View file

@ -5,20 +5,20 @@ Each inventory is a generated document whose staleness turns CI red
(``tests/test_generated_inventories.py`` pins byte-identity against a fresh
in-memory regeneration, plus the resolution invariants below):
1. ``docs/v7next/FROZEN_CONTRACTS_INVENTORY.md`` — machine extraction of the
1. ``docs/inventories/FROZEN_CONTRACTS_INVENTORY.md`` — machine extraction of the
ARCHITECTURE §11.1 frozen-contracts table: per row the contract label, the
owner files, and the anchoring suites, every referenced repo path resolved
against the tree (a row whose owner or anchor file disappeared = red), plus
the ``ouroboros/contracts/`` package coverage (a contracts module never
referenced by §11.1 is listed as a gap — growth of that list = red).
2. ``docs/v7next/DATA_LAYOUT_INVENTORY.md`` — machine extraction of the
2. ``docs/inventories/DATA_LAYOUT_INVENTORY.md`` — machine extraction of the
ARCHITECTURE "Data layout (`~/Ouroboros/`)" tree (the closest thing this
tree has to the reference's PERSISTENCE_OWNERS carrier): every entry is
probed against reality — repo entries must exist as tracked paths, data-
plane entries must appear as a literal in the runtime sources that
construct them (a renamed/removed durable file whose tree row survived =
red).
3. ``docs/v7next/FACADE_INVENTORY.md`` — AST-derived facade inventory: every
3. ``docs/inventories/FACADE_INVENTORY.md`` — AST-derived facade inventory: every
runtime module whose top-level ``from <population module> import ...``
statements carry the ``noqa: F401`` re-export marker (the codebase's
declared "this binding exists for compatibility" convention, per the
@ -54,7 +54,7 @@ from ouroboros.reference_books import ( # noqa: E402
read_book_section,
)
OUT_DIR = REPO_ROOT / "docs" / "v7next"
OUT_DIR = REPO_ROOT / "docs" / "inventories"
FROZEN_OUT = OUT_DIR / "FROZEN_CONTRACTS_INVENTORY.md"
LAYOUT_OUT = OUT_DIR / "DATA_LAYOUT_INVENTORY.md"
FACADE_OUT = OUT_DIR / "FACADE_INVENTORY.md"
@ -71,8 +71,7 @@ def _tracked_all() -> set[str]:
def _split_row(line: str) -> list[str]:
"""Split one markdown table row on unescaped pipes (adoption-validator
convention)."""
"""Split one markdown table row on unescaped pipes."""
body = line.strip().strip("|")
cells, cur, escaped = [], [], False
for ch in body:
@ -451,6 +450,7 @@ def main(argv: list[str] | None = None) -> int:
if on_disk != rendered:
stale.append(str(rel))
else:
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(rendered, encoding="utf-8")
print(f"wrote {rel}")

View file

@ -1,557 +0,0 @@
#!/usr/bin/env python3
"""Validate ADOPTION_v7next.md — the v7-side adoption manifest (Ф0 skeleton).
The manifest enumerates every v7-side delta that must be re-applied on top of
the v7next upstream base: the 18 approved semantic-delta families from the
frozen reference ledger (``ouroboros_v7_wip @ 9f691656`` —
``scripts/v7_migration.py::APPROVED_SEMANTIC_DELTAS`` minus ``"none"``) plus
the campaign-decision items of plan §6 (ABI package 7.0) and §7 (completeness)
and the plan §2 class returns.
Checks (plan §5.1, roast F2 — artifact/train-based manifest):
- the fixed 7-column table schema parses;
- ids are unique and well-formed;
- every required delta family D02–D38 is present as ``kind=semantic-delta``;
- ``kind`` / ``disposition`` / ``status`` / ``phase`` come from closed enums
(dispositions per the plan §5.4 three-column rule: retain / re-prove /
superseded-by-upstream, with ``pending-decision`` allowed only before
release);
- every row carries a non-empty verification hook;
- every post-cutoff upstream train the campaign absorbed keeps its row, still
naming its upstream tip and its campaign merge (both modes — a whole-file
overwrite deleted the sync #2 row past a bar that only the default mode ran);
- a ``done`` row's hook RESOLVES: every repo path exists and every ``::nodeid``
names something the file actually defines (read by AST);
- a ``done`` row does not say the work is open, unless it declares what stays
open in an explicit ``residual:`` clause;
- a post-release row's recorded authority and its text tell one story: an
OWNER deferral carries the owner's ``owner verbatim «…»`` quote in the row,
an operator disclosure carries none;
- the prose outside the table (header, schema, Notes) names ids only as the
table has them: every id-shaped token there resolves to a row unless the
prose declares it on a ``No-row ids: …`` line, and a declared no-row id must
not have a row (the Notes called W4-F3/W4-F4 rowless for two days after
d348ea46 made them rows — a green bar both days, because nothing read the
Notes). Disclosed residual: a rowless claim written as free English is not
read — a word marker was tried and misfired on «No row carries
pending-decision any more» — so the schema gives the claim its declared
form and the id resolution is the check that does not depend on wording;
- the Notes' ``Deferral authorities: <id> <authority>, …`` declaration (the
register's mirror) names exactly the ids ``DEFERRED_OUT_OF_V70`` records with
exactly their authority — the Notes called W4-F4 operator-disclosed for a day
after the register made it an owner deferral, and nothing read the Notes;
free prose about authority is still not read, the declared form is;
- ``--release``: no ``pending-decision`` dispositions and every row ``done``
("no unresolved rows at release", plan §10), with post-release rows leaving
the bar only through a recorded deferral (owner-authored for the required
inventory).
Exit 0 when green, 1 with findings, 2 when the manifest itself is missing or
structurally unparseable.
"""
from __future__ import annotations
import argparse
import ast
import pathlib
import re
import sys
from collections import Counter
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
MANIFEST = REPO_ROOT / "ADOPTION_v7next.md"
HEADER = ["id", "kind", "what", "disposition", "status", "phase", "verification hook"]
# APPROVED_SEMANTIC_DELTAS of the frozen reference, minus "none".
REQUIRED_DELTAS = (
"D02", "D03", "D04", "D05", "D06", "D07", "D08", "D09", "D11",
"D13", "D18", "D31", "D33", "D34", "D35", "D36", "D37", "D38",
)
# Required non-delta inventory (F0 phase review F2): the ABI package and the
# compatibility retirements are release-gated too, not only the D-families.
REQUIRED_ABI = tuple(f"ABI-{n}" for n in range(1, 11))
REQUIRED_CPL = tuple(f"CPL-{n}" for n in range(1, 8))
# F0 review round 2: the phase of every required row is itself part of the
# owner-approved inventory — a required row silently rescheduled to another
# phase (or parked post-release without a recorded deferral) must turn the
# validator red. DEFERRED_OUT_OF_V70 is that record: every post-release row
# must appear here, and a row of the owner-approved required inventory
# (REQUIRED_PHASE) may only be parked with OWNER authority, so flipping a
# required row post-release still cannot bypass the release bar. Operator
# authority covers rows that are disclosures rather than owner decisions —
# a defect a wave found and named instead of fixing.
REQUIRED_PHASE = {
# D02 F1->F3: owner-ratified F3 layout (2026-08-31) — the typed organ is
# re-derived whole by the F3.1 lane A; seam commit updates row + pin together.
# D03 F1->F6 (ADOPTION truth wave, 2026-09-01): F1 closed with the settings
# seam's rows 913-917/1080-1081 still hot-deferred, so the pin named a dead
# phase. F6 is the live phase. This is an OPERATOR scheduling correction, not
# an owner decision — disclosed in the manifest row and the ledger so the
# owner can overturn it. Sibling rows D04/D05/D06/D35 landed through their
# owner-decided lanes and read done; their F1 pins were deliberately left
# alone (one decision per class, and nobody has decided this one).
"D02": "F3", "D03": "F6", "D04": "F1", "D05": "F1", "D06": "F1",
"D07": "F2", "D08": "F2", "D09": "F1", "D11": "F1", "D13": "F1",
"D18": "F1", "D31": "F2", "D33": "F1", "D34": "F2", "D35": "F1",
"D36": "F2", "D37": "F2", "D38": "F1",
"ABI-1": "F3", "ABI-2": "F3", "ABI-3": "F3", "ABI-4": "F3",
"ABI-5": "F3", "ABI-6": "F3", "ABI-7": "F3", "ABI-8": "POST",
"ABI-9": "F3", "ABI-10": "F3",
"CPL-1": "F5", "CPL-2": "F5", "CPL-3": "F5", "CPL-4": "F5",
"CPL-5": "F5", "CPL-6": "F5", "CPL-7": "F5",
"DEFER-BROWSER": "POST",
}
OWNER, OPERATOR = "owner", "operator-disclosed"
DEFERRED_OUT_OF_V70 = {
# Owner decisions: Q5=A kept the handler ABI out of the bundle and Q16=A
# retired the «7.1» label into the post-release backlog (ABI-8); batch №9
# №14=A put the browser wave after the release, with a green smoke — not a
# green browser lane — as the condition on the tag (DEFER-BROWSER).
"ABI-8": OWNER,
"DEFER-BROWSER": OWNER,
# (W4-F1 and W4-F2 — the two evolution crash windows the F4 wave-4 lane
# disclosed instead of fixing — were pulled INTO 7.0 by owner batch №13
# item 9 = B, so they are no longer deferrals: their rows read done.)
# Owner-sanctioned deferrals that lived as prose inside done rows or in the
# ledger until the stage-2 bookkeeping made them rows (quotes in each row):
# batch №7 5=A (headless cancel receipts), batch №9 №12=A (two frozen modules),
# batch №12 A (C6 residuals), batch №8 5=A (task_results eternal).
# Operator disclosure WITHOUT an owner decision: the wave-4 observation
# W4-F4 only. Batch №13 item 13(и) asked to ratify W4-F3/W4-F4 and the owner
# answered that he had not read that item; the F3 owner batch of 2026-09-04
# re-asked, and its item 5 = A pulled W4-F3 INTO 7.0 (the marker is always
# written; that row reads done and is no longer a deferral). Owner-decided
# since batch №13:
# DEFER-E2E-PAID-LANE (item 2 = A ordered the paid lane RUN once — E1/E13
# executed green; E2/E3 stay unexecuted for a structural reason, the real
# Claudexor lane needs a logged-in Claude account, which is the owner's act
# — so the quote covers the execution and the remainder is a disclosed
# block, not a waiver) and DEFER-SPEC64-PATHS (item 8 = A). (Left this
# record by the same batch: item 10 = B pulled DEFER-TYPED-PROC-5 into 7.0;
# item 15 = B landed the mutating delegation scenarios S24/S25 —
# DEFER-E2E-DELEG-MUT reads done at phase F4; item 7 = A closed F23 as
# covered by the release bar.)
"DEFER-HEADLESS-CANCEL": OWNER,
"DEFER-FROZEN-2": OWNER,
"DEFER-C6-RESIDUALS": OWNER,
"DEFER-C19-RETENTION": OWNER,
"W4-F4": OWNER,
"DEFER-E2E-PAID-LANE": OWNER, # batch №13 item 2 = A (the run order); E2/E3 blocked structurally
"DEFER-SPEC64-PATHS": OWNER, # batch №13 item 8 = A
}
# Post-cutoff upstream adoption trains: id -> (upstream tip, campaign merge).
# A frozen inventory rather than a git derivation, and the history is the
# reason. Each sync's absorb merge does take its upstream tip as the literal
# second parent (20850191<-8d13373b, b9ceed6e<-f3fbfdbb, f4abe0a5<-a76961de),
# but only f4abe0a5 sits on this branch's first-parent line: the other two were
# made on lane lines and reached mainline on the second-parent side of a lane
# integration merge over a CAMPAIGN commit (0aa74e9f over 816e7b82, 0f9a8daf
# over 4c32691e). So a rule walking --first-parent merges and reading second
# parents would police one train of three and stay blind to the other two —
# exactly the hole that lost TRAIN-F6b-f3fbfdbb; widened to "second parent
# descends from a recorded upstream tip" it would demand a train row for every
# lane merge made after a sync (35 / 15 / 6 merges on this tree for the three
# tips), the C6 lane merge 9faccf31 over 8fb08d44 included. Neither is honest,
# and both need a subprocess. Sync #1 is recorded by its mainline carrier
# 0aa74e9f and names absorb merge 20850191 in the row text too; syncs #2 and #3
# are recorded by the absorb merge itself. Adding a train here is the same edit
# as merging one, and a deleted row is red at once.
REQUIRED_TRAINS = {
"TRAIN-F6-8d13373b": ("8d13373b", "0aa74e9f"),
"TRAIN-F6b-f3fbfdbb": ("f3fbfdbb", "b9ceed6e"),
"TRAIN-F6c-a76961de": ("a76961de", "f4abe0a5"),
}
KINDS = frozenset({"semantic-delta", "plan-item", "class-return"})
DISPOSITIONS = frozenset({"retain", "re-prove", "superseded-by-upstream",
"pending-decision", "post-release"})
STATUSES = frozenset({"pending", "in-progress", "done", "deferred"})
PHASES = frozenset({"F0", "F1", "F2", "F3", "F4", "F5", "F6", "POST"})
ID_RE = re.compile(r"^(D\d\d|ABI-\d+|CPL-\d+|R-[A-Z0-9]+|TRAIN-[A-Za-z0-9._-]+"
r"|DEFER-[A-Z0-9][A-Z0-9-]*|W\d-F\d+)$") # DEFER ids may carry hyphenated tokens (DEFER-E2E-PAID-LANE)
# The same id grammar, unanchored, for the prose outside the table. The
# boundaries keep `D-14` (a plan decision) and `CPL4-C6` (a lane label) out;
# the TRAIN class admits dots, so a sentence-final one is stripped by the reader.
_PROSE_ID_RE = re.compile(r"(?<![\w-])" + ID_RE.pattern[1:-1] + r"(?![\w-])")
# The one declared form for an id the prose names without a row (a folded or
# withdrawn row): a line `No-row ids: A, B`, optionally as a Notes bullet.
_NO_ROW_DECL_RE = re.compile(r"^\s*(?:-\s*)?No-row ids:(.*)$", re.M)
# The manifest's quote convention for an owner decision, as every OWNER row
# already spells it — the marker the authority lint keys on.
_OWNER_QUOTE_MARKER = "owner verbatim «"
# The one declared form for the deferral authorities the Notes claim — a
# `Deferral authorities…: <id> <authority>, …` bullet (continuation lines
# indented) mirroring DEFERRED_OUT_OF_V70. Free prose about authority is not read.
_DEFERRAL_DECL_RE = re.compile(
r"^\s*(?:-\s*)?Deferral authorities\b[^:]*:(?P<body>.*(?:\n[ \t]+(?!-\s).*)*)", re.M)
_DEFERRAL_PAIR_RE = re.compile(_PROSE_ID_RE.pattern + rf"\s+({OPERATOR}|{OWNER})\b")
def split_row(line: str) -> list[str]:
"""Split one markdown table row on unescaped pipes."""
body = line.strip().strip("|")
cells, cur, escaped = [], [], False
for ch in body:
if escaped:
cur.append(ch)
escaped = False
elif ch == "\\":
cur.append(ch)
escaped = True
elif ch == "|":
cells.append("".join(cur).strip())
cur = []
else:
cur.append(ch)
cells.append("".join(cur).strip())
return cells
def parse_rows(text: str) -> tuple[list[dict[str, str]], list[str]]:
errors: list[str] = []
rows: list[dict[str, str]] = []
lines = text.splitlines()
header_at = None
for i, line in enumerate(lines):
if line.startswith("|") and [c.lower() for c in split_row(line)] == HEADER:
header_at = i
break
if header_at is None:
errors.append(f"table header not found; expected columns: {' | '.join(HEADER)}")
return rows, errors
for j in range(header_at + 2, len(lines)):
line = lines[j]
if not line.startswith("|"):
break
cells = split_row(line)
if len(cells) != len(HEADER):
errors.append(f"line {j + 1}: expected {len(HEADER)} cells, got {len(cells)}")
continue
rows.append(dict(zip(HEADER, cells)))
return rows, errors
def manifest_prose(text: str) -> str:
"""Everything outside the table — the header, the schema, the Notes — by
the rule ``parse_rows`` already uses: a table line starts with ``|``."""
return "\n".join(line for line in text.splitlines() if not line.startswith("|"))
def _prose_id_errors(prose: str, by_id: dict[str, dict[str, str]]) -> list[str]:
"""The prose may name a row id only as the table has it. Tokens are read by
the table's own id grammar, not by phrasing, so the check does not depend
on how a sentence says 'gets no row': an id without a row must be declared
on a ``No-row ids:`` line, and a declared id must not have a row."""
declared: set[str] = set()
for m in _NO_ROW_DECL_RE.finditer(prose):
declared.update(t.rstrip(".-") for t in _PROSE_ID_RE.findall(m.group(1)))
named = {t.rstrip(".-") for t in _PROSE_ID_RE.findall(prose)}
errors: list[str] = []
for rid in sorted(declared & by_id.keys()):
errors.append(f"prose: {rid} is declared under 'No-row ids:' while the "
"table has its row — drop the declaration or the row")
for rid in sorted(named - by_id.keys() - declared):
errors.append(f"prose: {rid} is named outside the table but has no row "
"and no 'No-row ids:' declaration")
return errors
def declared_deferral_authorities(prose: str) -> dict[str, str] | None:
"""The Notes' ``Deferral authorities:`` declaration as ``{id: authority}``;
``None`` when the prose carries no declaration."""
m = _DEFERRAL_DECL_RE.search(prose)
if m is None:
return None
return dict(_DEFERRAL_PAIR_RE.findall(m.group("body")))
def _deferral_declaration_errors(prose: str) -> list[str]:
"""A declared deferral-authority list must match ``DEFERRED_OUT_OF_V70``
exactly — the same ids, each with its recorded authority — so the Notes
cannot tell a different story than the register."""
declared = declared_deferral_authorities(prose)
if declared is None:
return []
errors: list[str] = []
for rid in sorted(declared.keys() - DEFERRED_OUT_OF_V70.keys()):
errors.append(f"prose: Deferral authorities declares {rid}, which "
"DEFERRED_OUT_OF_V70 does not record")
for rid in sorted(DEFERRED_OUT_OF_V70.keys() - declared.keys()):
errors.append(f"prose: Deferral authorities omits {rid} "
f"({DEFERRED_OUT_OF_V70[rid]} in DEFERRED_OUT_OF_V70) — declare every recorded deferral")
for rid in sorted(declared.keys() & DEFERRED_OUT_OF_V70.keys()):
if declared[rid] != DEFERRED_OUT_OF_V70[rid]:
errors.append(f"prose: Deferral authorities says {rid} is {declared[rid]} while "
f"DEFERRED_OUT_OF_V70 records {DEFERRED_OUT_OF_V70[rid]}")
return errors
def validate(rows: list[dict[str, str]], release: bool, prose: str = "") -> list[str]:
errors: list[str] = []
ids = [r["id"] for r in rows]
for rid, n in Counter(ids).items():
if n > 1:
errors.append(f"duplicate id: {rid} ({n} rows)")
for r in rows:
rid = r["id"]
if not ID_RE.match(rid):
errors.append(f"{rid or '<empty>'}: malformed id")
if r["kind"] not in KINDS:
errors.append(f"{rid}: unknown kind {r['kind']!r}")
if r["disposition"] not in DISPOSITIONS:
errors.append(f"{rid}: unknown disposition {r['disposition']!r}")
if r["status"] not in STATUSES:
errors.append(f"{rid}: unknown status {r['status']!r}")
if r["phase"] not in PHASES:
errors.append(f"{rid}: unknown phase {r['phase']!r}")
if not r["what"]:
errors.append(f"{rid}: empty 'what'")
if not r["verification hook"]:
errors.append(f"{rid}: empty verification hook")
by_id = {r["id"]: r for r in rows}
for d in REQUIRED_DELTAS:
row = by_id.get(d)
if row is None:
errors.append(f"required semantic delta {d} is missing")
elif row["kind"] != "semantic-delta":
errors.append(f"{d}: must be kind=semantic-delta, got {row['kind']!r}")
# F0 phase review F2: the ABI package and compatibility retirements are part
# of the release inventory too — deleting their rows must turn --release red.
for rid in (*REQUIRED_ABI, *REQUIRED_CPL):
row = by_id.get(rid)
if row is None:
errors.append(f"required row {rid} is missing")
elif row["kind"] != "plan-item":
errors.append(f"{rid}: must be kind=plan-item, got {row['kind']!r}")
# Row-specific coupling: post-release is a single coherent state, not three
# independent knobs (prevents e.g. disposition=post-release with status=done
# quietly counting as shipped) — and it needs a recorded deferral in
# DEFERRED_OUT_OF_V70, owner-authored for the required inventory, so
# flipping a required row to post-release cannot bypass the release bar.
for r in rows:
post_bits = [r["disposition"] == "post-release", r["status"] == "deferred",
r["phase"] == "POST"]
if any(post_bits) and not all(post_bits):
errors.append(
f"{r['id']}: post-release rows need disposition=post-release + "
f"status=deferred + phase=POST together, got "
f"{r['disposition']}/{r['status']}/{r['phase']}")
if all(post_bits):
authority = DEFERRED_OUT_OF_V70.get(r["id"])
if authority is None:
errors.append(
f"{r['id']}: post-release needs a recorded deferral in "
f"DEFERRED_OUT_OF_V70 (currently "
f"{sorted(DEFERRED_OUT_OF_V70)})")
elif authority != OWNER and r["id"] in REQUIRED_PHASE:
errors.append(
f"{r['id']}: a row of the required inventory can only be "
f"parked post-release by an owner decision, not by "
f"{authority}")
# The record and the row tell one story: an owner deferral carries
# the owner's quote, an operator disclosure carries none. The
# comment block over the record drifted from its values once
# (E2/E3 and spec §6.4 read operator-disclosed beside OWNER), and
# a reader trusts the prose first.
quoted = _OWNER_QUOTE_MARKER in r["what"]
if authority == OWNER and not quoted:
errors.append(
f"{r['id']}: recorded as an owner deferral but the row carries "
f"no '{_OWNER_QUOTE_MARKER}…»' quote — quote the decision or "
f"record the row as {OPERATOR}")
elif authority == OPERATOR and quoted:
errors.append(
f"{r['id']}: recorded as {OPERATOR} but the row carries an owner "
f"quote — record the row as {OWNER} or drop the quote")
# Every upstream train the campaign absorbed must keep its row, and the row
# must still name the tip and the merge it is a record of. Both modes: the
# deletion in 285ab66d survived because only the default mode was run.
for rid, (tip, merge) in REQUIRED_TRAINS.items():
row = by_id.get(rid)
if row is None:
errors.append(f"required upstream train {rid} is missing — every "
"absorbed upstream train keeps a row")
continue
if row["kind"] != "plan-item":
errors.append(f"{rid}: must be kind=plan-item, got {row['kind']!r}")
text = f"{row['what']} {row['verification hook']}"
for sha in (tip, merge):
if sha not in text:
errors.append(f"{rid}: row text no longer names {sha} "
"(upstream tip and campaign merge are what the "
"row records)")
# A shipped row must not say it is unshipped. This is a text-vs-cell
# consistency lint on an operator manifest — not a semantic gate on any
# runtime decision — and the `residual:` clause is the explicit escape, so
# a genuine disclosure on a shipped row stays sayable.
for r in rows:
errors.extend(_honesty_errors(r))
# Hook resolution is a property of a shipped row, not of the release
# invocation, so it runs in both modes and its messages say `hook:`.
# The manifest's own Notes state the same rule for its readers.
if r["status"] == "done":
errors.extend(_hook_resolution_errors(r))
# Phase pinning of the required inventory.
for rid, want in REQUIRED_PHASE.items():
row = by_id.get(rid)
if row is not None and row["phase"] != want:
errors.append(f"{rid}: phase {row['phase']!r} != pinned {want!r} "
"(rescheduling a required row needs a new owner decision "
"and an update to REQUIRED_PHASE)")
if prose:
errors.extend(_prose_id_errors(prose, by_id))
errors.extend(_deferral_declaration_errors(prose))
if release:
for r in rows:
if r["disposition"] == "pending-decision":
errors.append(f"release: {r['id']} still pending-decision")
if r["disposition"] == "post-release":
continue # explicitly deferred out of v7.0 by an owner decision
if r["status"] != "done":
errors.append(f"release: {r['id']} status {r['status']!r} != done")
return errors
# Any-extension token, anchored on BOTH sides: the lookbehind stops
# `not-scripts/x.py` being misread as a scripts/ reference (round 4), the
# lookahead stops `scripts/x.py-not-real` matching by its existing `.py`
# prefix (round 5) — a partial token is prose, not a reference.
_HOOK_PATH_RE = re.compile(r"(?<![\w./-])(?:tests|scripts|docs)/[\w./-]+\.\w+(?![\w-])")
# A shipped row that says the work is open contradicts its own status cell.
# The escape is explicit and named, not a keyword exception list.
_NOT_DONE_MARKERS = ("not done", "open residual", "not integrated yet",
"still owed", "read pending")
_RESIDUAL_CLAUSE = "residual:"
def _honesty_errors(row: dict[str, str]) -> list[str]:
"""A `done` row may carry an open residual — that is what a `residual:`
clause declares — but it may not say the work itself is not done."""
if row["status"] != "done":
return []
text = f"{row['what']} {row['verification hook']}".lower()
if _RESIDUAL_CLAUSE in text:
return []
hits = [m for m in _NOT_DONE_MARKERS if m in text]
if not hits:
return []
return [f"{row['id']}: status is done while the text says {hits!r}; either "
f"the status is wrong or the row needs an explicit "
f"'{_RESIDUAL_CLAUSE}' clause naming what stays open"]
# A hook may name a pytest node id. The path half was already resolved; the
# `::name` half was free text until now, so a hook could point at a suite that
# exists and a pin that does not.
_HOOK_NODEID_RE = re.compile(
r"(?<![\w./-])((?:tests|scripts)/[\w./-]+\.py)((?:::[A-Za-z_]\w*)+)")
def _defined_names(path: pathlib.Path) -> set[str]:
"""Every name a pytest node id could legitimately address in a file:
functions and classes at any depth (``path::Class::method``) plus
module-level bindings — a hook may name the closed inventory a pin drives,
not only the pin (`tests/_shared.py::SETTINGS_WRITERS`)."""
tree = ast.parse(path.read_text(encoding="utf-8"))
names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
names.add(node.name)
for node in tree.body:
targets = node.targets if isinstance(node, ast.Assign) else (
[node.target] if isinstance(node, ast.AnnAssign) else [])
names.update(t.id for t in targets if isinstance(t, ast.Name))
return names
def _hook_nodeid_errors(row: dict[str, str], hook: str) -> list[str]:
errors: list[str] = []
for rel, tail in _HOOK_NODEID_RE.findall(hook):
path = (REPO_ROOT / rel).resolve()
if not path.is_file():
continue # the path half is reported by the resolver above
try:
defined = _defined_names(path)
except SyntaxError as exc: # unparseable file: say so, do not pass it
errors.append(f"hook: {row['id']} hook file {rel} does not parse ({exc})")
continue
for part in tail.split("::"):
if part and part not in defined:
errors.append(f"hook: {row['id']} hook names {rel}::{part}, "
f"which {rel} does not define")
return errors
def _hook_resolution_errors(row: dict[str, str]) -> list[str]:
"""Shipped-row hook contract (F0 review rounds 1-4): a shipped row's
verification hook must RESOLVE — prose alone cannot pass. At least one
repo-path reference must be present, EVERY referenced token must exist
(any extension — a smuggled bogus reference next to a valid one is an
error, not ignored), and the path must stay inside its top directory
(`tests/../x` traversal is rejected). This runs for every `done` row in
BOTH modes — it is a property of a shipped row, not of the --release
invocation — so the messages are prefixed `hook:`, not `release:`. A row
that is not yet `done` keeps a free-prose hook, naming the suite the work
will land in."""
hook = row["verification hook"]
paths = _HOOK_PATH_RE.findall(hook.replace("\\|", "|"))
errors: list[str] = []
if not paths:
errors.append(
f"hook: {row['id']} hook has no resolvable repo-path reference "
"(tests/, scripts/ or docs/ file) — prose-only hooks cannot ship")
for p in paths:
top = p.split("/", 1)[0]
candidate = (REPO_ROOT / p).resolve()
top_root = (REPO_ROOT / top).resolve()
# pathlib containment, not string prefixing: portable across
# separators (round 5: the "/"-suffix check broke on Windows).
inside = candidate == top_root or top_root in candidate.parents
if ".." in p.split("/") or not inside:
errors.append(f"hook: {row['id']} hook path escapes {top}/: {p}")
elif not candidate.is_file():
errors.append(f"hook: {row['id']} hook references missing file {p}")
errors.extend(_hook_nodeid_errors(row, hook))
return errors
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--release", action="store_true",
help="enforce the release bar: no pending-decision, all rows done")
ap.add_argument("--manifest", type=pathlib.Path, default=MANIFEST)
args = ap.parse_args()
if not args.manifest.is_file():
print(f"missing manifest: {args.manifest}", file=sys.stderr)
return 2
text = args.manifest.read_text(encoding="utf-8")
rows, errors = parse_rows(text)
if not rows and errors:
for e in errors:
print(f"ERROR: {e}", file=sys.stderr)
return 2
errors += validate(rows, args.release, prose=manifest_prose(text))
by_phase = Counter(r["phase"] for r in rows)
by_disp = Counter(r["disposition"] for r in rows)
by_status = Counter(r["status"] for r in rows)
by_kind = Counter(r["kind"] for r in rows)
print(f"{args.manifest.name}: {len(rows)} rows")
print(f" kind: {dict(sorted(by_kind.items()))}")
print(f" phase: {dict(sorted(by_phase.items()))}")
print(f" disposition: {dict(sorted(by_disp.items()))}")
print(f" status: {dict(sorted(by_status.items()))}")
if errors:
for e in errors:
print(f"ERROR: {e}", file=sys.stderr)
return 1
print("OK" + (" (release bar)" if args.release else ""))
return 0
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load diff

View file

@ -1,76 +0,0 @@
from __future__ import annotations
import io
import json
import threading
import zipfile
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def _skill_archive() -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr(
"duck/SKILL.md",
"---\n"
"name: duck\n"
"description: mock search skill\n"
"version: 1.0.0\n"
"metadata:\n"
" openclaw:\n"
" install:\n"
" - kind: pip\n"
" package: ddgs\n"
"---\n",
)
return buf.getvalue()
class MockClawHubServer:
def __init__(self):
self._server = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
@property
def base_url(self) -> str:
return f"http://127.0.0.1:{self._server.server_address[1]}"
def __enter__(self):
self._thread.start()
return self
def __exit__(self, *_exc):
self._server.shutdown()
self._server.server_close()
class _Handler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802 - stdlib callback name
if self.path.startswith("/packages/duck") or self.path.startswith("/skills/duck"):
payload = {
"slug": "duck",
"name": "duck",
"version": "1.0.0",
"metadata": {"openclaw": {"install": [{"kind": "pip", "package": "ddgs"}]}},
}
return self._json(payload)
if self.path.startswith("/download/duck"):
data = _skill_archive()
self.send_response(200)
self.send_header("Content-Type", "application/zip")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
self._json({"packages": []})
def _json(self, payload):
data = json.dumps(payload).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *_args):
return

View file

@ -1,26 +1,25 @@
"""Suite actors beyond the loopback stub models (plan §8).
"""Suite actors beyond the loopback stub models.
``FakeClaudexorDaemon`` (landed with the Ф4 wave-3b delegated-transport lane) is a
loopback claudexord imitation serving the EXACT client contract this tree's
``ouroboros/gateways/claudexor.py`` speaks: the protocol-3 authenticated handshake,
``FakeClaudexorDaemon`` is a loopback claudexord imitation serving the EXACT
client contract this tree's ``ouroboros/gateways/claudexor.py`` speaks: the protocol-3 authenticated handshake,
the capability/quota answers ``subagent_route_health.route_health`` reads, project
registration with Idempotency-Key, ``POST /v2/runs`` with the engine's replay
check (same key + byte-identical body → the ORIGINAL handle; same key + different
digest → 409 ``idempotency_conflict``), run detail with the ``summary`` facts the
custody settler consumes, the cancel control verb, and (wave 4) the interactive
custody settler consumes, the cancel control verb, and the interactive
question surface — ``pendingInteractions`` on the detail plus the
``POST /v2/runs/:id/interactions/:iid/answer`` verb ``delegate_answer`` speaks,
with its typed delivered/already_resolved/rejected statuses. Behavior is scripted
PER RUN by markers in the POSTed prompt (success / hang / typed refusal / ask)
plus the pinned-profile refusal, and (the mutating wave) the applied facts a
plus the pinned-profile refusal, and the applied facts a
WRITING run produces: the edits themselves, made inside the private execution
snapshot the start body names, and the ``attempts/<id>/attempt.yaml`` containment
record ``gateways/claudexor.py::attempt_containment`` reads. So one daemon serves
every delegated-transport scenario without a second boot. It records every request
(method, path, idempotency key, body) for wire-truth assertions.
``PlaywrightUIClient`` stays an interface stub until the gateway/UI-truth wave
lands: instantiating it is a scenario bug, and it refuses loudly.
``PlaywrightUIClient`` is an unimplemented interface stub: instantiating it is
a scenario bug, and it refuses loudly.
"""
from __future__ import annotations
@ -34,10 +33,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any, Dict, List, Optional
_NOT_LANDED = (
"{name} is an interface stub: its implementation lands with the {lane} wave of "
"the Ф4 integration suite (plan §8). Write the scenario against this surface, "
"but do not enable it before the lane lands — see tests/system_e2e/ and "
"docs/v7next/LEDGER_CORRECTIONS.md (F4 lane 1)."
"{name} is an unimplemented interface stub for {lane} scenarios. "
"This test harness is unavailable."
)
# Prompt markers a scenario plants to script ONE run's behavior. Chosen so they can
@ -480,7 +477,7 @@ class FakeClaudexorDaemon:
class PlaywrightUIClient:
"""Real-browser client over an isolated server's web UI (gateway/UI truth)."""
"""Unimplemented browser-client interface for isolated-server UI scenarios."""
def __init__(self, *_args, **_kwargs) -> None:
raise NotImplementedError(_NOT_LANDED.format(

View file

@ -121,8 +121,8 @@ def test_domain_dependencies_unknown_domain_teaches_the_vocabulary():
def test_facade_scan_matches_the_generated_facade_inventory(reexports):
"""Completeness against the gen/verify-pinned carrier: the runtime scan
finds exactly the facade modules docs/v7next/FACADE_INVENTORY.md pins."""
inventory_text = (REPO / "docs/v7next/FACADE_INVENTORY.md").read_text(encoding="utf-8")
finds exactly the facade modules docs/inventories/FACADE_INVENTORY.md pins."""
inventory_text = (REPO / "docs/inventories/FACADE_INVENTORY.md").read_text(encoding="utf-8")
pinned = set()
for line in inventory_text.splitlines():
if line.startswith("| `") and line.count("|") >= 4:

View file

@ -1,4 +1,4 @@
"""ABI 7.0 (ABI-10) — F3.3 comma-list remnant sweep: the phase CI gate.
"""ABI 7.0 reviewer comma-list retirement checks.
Grep-level checker over ``ouroboros/`` + ``web/`` (+ ``supervisor/``) pinning
that the retired reviewer comma-list surface stays retired. Three sweeps:
@ -11,7 +11,7 @@ that the retired reviewer comma-list surface stays retired. Three sweeps:
the known derived-plane parsers — the review-configuration modules
themselves carry NO comma parsing (the structured
``OUROBOROS_REVIEWER_SLOTS`` is the one configuration surface);
3. the phase-5 plumbing removed by the sweep stays removed, and the retired
3. removed route plumbing stays removed, and the retired
per-row route envs are IGNORED at runtime (retired-envs-are-ignored pin).
Allowlist discipline follows tests/test_gateway_abi3_removals.py: PER-SITE and
@ -42,8 +42,7 @@ def _sweep_files():
# (posix path, retired key) -> (reason, exact mention count).
# Every row is a LEGITIMATE remnant class disclosed in
# docs/v7next/LEDGER_CORRECTIONS.md ("From the F3.3 comma-sweep"):
# Every row belongs to one of these legitimate remnant classes:
# retirement-SSOT — the list that declares the keys retired;
# derived env plane — the comma ENV spellings of the two model lists live on
# as the runtime projection for the API-pinned surfaces (never settings);
@ -59,7 +58,7 @@ _RETIRED_KEY_MENTION_ALLOWLIST = {
("ouroboros/settings_defaults.py", "OUROBOROS_REVIEW_ROUTES"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_SCOPE_REVIEW_ROUTES"): ("retirement SSOT", 2),
("ouroboros/settings_defaults.py", "OUROBOROS_ADVISORY_REVIEW_ROUTE"): ("retirement SSOT", 2),
# -- derived env plane: the projection writer (D15) …
# -- derived env plane: the projection writer …
("ouroboros/reviewer_slot_config.py", "OUROBOROS_REVIEW_MODELS"): ("derived env plane projection writer", 4),
("ouroboros/reviewer_slot_config.py", "OUROBOROS_SCOPE_REVIEW_MODELS"): ("derived env plane projection writer", 4),
("ouroboros/reviewer_slot_config.py", "OUROBOROS_SCOPE_REVIEW_MODEL"): ("derived env plane projection writer", 2),
@ -191,7 +190,7 @@ def test_comma_split_ast_scan_sees_the_evasion_spellings():
def test_phase5_route_plumbing_stays_removed():
"""The F3.3 removals stay removed: no per-row route env plumbing, no
"""Removed route plumbing stays removed: no per-row route env plumbing, no
advisory route env constant, anywhere under the swept trees."""
retired_symbols = (
"configured_review_routes",

View file

@ -282,5 +282,5 @@ def test_settings_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.config"] <= 1000
assert all(count <= 1000 for count in counts.values())
assert 250 <= counts["ouroboros.settings_defaults"] <= 500
assert counts["ouroboros.settings_defaults"] <= 500
assert (PACKAGE / "config.py").is_file()

View file

@ -1,20 +1,9 @@
"""Structural contracts for the semantic-no-op core tool extraction.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656) with the following
identity continuations to THIS tree's bytes:
1. The catalog schema hash is re-pinned to tip bytes (upstream drifted the
read/list/write schemas after the reference cutoff).
2. This tree keeps a re-export facade on ``tools/core.py`` (the §5.3 partial-
split idiom, matching the shell facade) instead of the reference's
no-facade cutover, so the reference's ``isdisjoint(vars(core))`` clause is
replaced by the facade identity clause; the consumer-rebinding rows of the
reference (browser/vision/query_code/edit_ops/test bindings) stay pending
and ride with their own hygiene wave.
3. The frozen-tool-inventory clauses are dropped: ``ouroboros.tool_module_
inventory`` is a D04-family v7 leaf absent from this tree; the
non-catalog-owner and no-backedge clauses keep the structural half of that
contract. The inventory clause returns with its leaf.
``tools/core.py`` keeps a re-export facade, matching the shell facade, so
existing importers retain the exact leaf objects. The catalog schema hash
pins the current tool schemas and the handler map pins their owners.
The leaves are non-catalog owners without backedges into the facade.
"""
from __future__ import annotations
@ -184,6 +173,6 @@ def test_core_extraction_size_bounds_have_meaningful_headroom():
module.__name__: len(pathlib.Path(module.__file__).read_text(encoding="utf-8").splitlines())
for module in (core, core_file_tools, core_artifacts)
}
assert 1200 <= counts["ouroboros.tools.core"] <= 1499
assert 750 <= counts["ouroboros.tools.core_file_tools"] <= 1000
assert 150 <= counts["ouroboros.tools.core_artifacts"] <= 1000
assert counts["ouroboros.tools.core"] <= 1499
assert counts["ouroboros.tools.core_file_tools"] <= 1000
assert counts["ouroboros.tools.core_artifacts"] <= 1000

View file

@ -56,24 +56,6 @@ def test_component_basename_does_not_borrow_another_explicit_path():
assert not _names_basename("test_s3_task_control_browser.py", "browser.py")
def test_the_domain_quotient_report_ends_without_a_blank_line():
"""The report generator wrote a blank line at EOF, so the whitespace gate
(`git diff --check`) was red on the one file nobody edits by hand.
Its sections append a trailing "" separator, and `"\\n".join(L) + "\\n"` then
turned the last separator into a blank final line. The generator now drops
the trailing separators; this pins both the artifact and that fix, without
pinning the report's CONTENT — the header carries a HEAD sha and a tree
fingerprint, so byte-identity to a regeneration is deliberately not a gate
(that gate belongs to `docs/DOMAIN_MAP.md`, whose input is the manifest).
"""
report = _read("docs/v7next/DOMAIN_QUOTIENT_REPORT.md")
generator = _read("scripts/v7next_domain_report.py")
assert report.endswith("\n") and not report.endswith("\n\n")
assert "while L and not L[-1]:" in generator
def test_the_domain_manifest_is_reachable_from_the_handbook():
"""The domain SSOT and its generated map were reachable from neither doc.
@ -109,21 +91,19 @@ def test_recent_abi_retirements_section_carries_the_abi_70_window():
assert "OUROBOROS_REVIEWER_SLOTS" in section, "the migration target must be named"
def test_model_send_design_note_matches_the_landed_observability_contract():
"""The CPL-5 note still said DESIGN ONLY and demanded fail-closed dispatch.
def test_model_send_design_note_matches_the_observability_contract():
"""A reconstruction mismatch is observable without blocking dispatch.
`ouroboros/model_send_seal.py` landed with the opposite rule, pinned by
`ouroboros/model_send_seal.py` implements the rule pinned by
`tests/test_model_send_seal.py`: a reconstruction mismatch is a typed
durable fact and the call is NOT blocked — dispatch authority stays with
the pre-existing in-memory identity re-check. A design note that outranks
the code it describes is how the next author reintroduces the gate.
"""
note = _read("docs/v7next/DESIGN_MODEL_VISIBLE_LOGGED.md")
note = _read("docs/MODEL_SEND_OBSERVABILITY.md")
note_flat = " ".join(note.split())
assert (REPO / "ouroboros" / "model_send_seal.py").exists()
assert "Status: DESIGN ONLY" not in note
assert "Status: LANDED" in note
assert "refuse dispatch with the existing `PhysicalAttemptPreparationFailed`" \
not in note_flat
assert "The call is NOT blocked" in note_flat

102
tests/test_domain_report.py Normal file
View file

@ -0,0 +1,102 @@
"""The optional graph report writes only to the requested output surface."""
from __future__ import annotations
import os
import pathlib
import shutil
import subprocess
import sys
import pytest
pytestmark = pytest.mark.serial # Real Git and Python subprocesses.
REPO = pathlib.Path(__file__).resolve().parents[1]
@pytest.fixture
def report_repo(tmp_path):
root = tmp_path / "repo"
(root / "scripts").mkdir(parents=True)
(root / "ouroboros").mkdir()
for name in ("domain_report.py", "domain_graph.py"):
shutil.copyfile(REPO / "scripts" / name, root / "scripts" / name)
(root / "ouroboros/a.py").write_text("import ouroboros.b\n", encoding="utf-8")
(root / "ouroboros/b.py").write_text("import ouroboros.a\n", encoding="utf-8")
(root / "ouroboros/domains.toml").write_text(
'[domains]\nD01 = "Первый"\nD02 = "Second"\n'
'[modules]\n"ouroboros/a.py" = "D01"\n"ouroboros/b.py" = "D02"\n',
encoding="utf-8",
)
for args in (
["init"],
["add", "."],
["-c", "user.name=Test", "-c", "user.email=test@example.invalid",
"-c", "commit.gpgsign=false", "commit", "-m", "fixture"],
):
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True,
encoding="utf-8")
return root
def _files(root):
return {
path.relative_to(root): path.read_bytes()
for path in root.rglob("*")
if path.is_file() and ".git" not in path.relative_to(root).parts
}
def _run(root, *args):
# The report's UTF-8 output must also survive a non-UTF-8 host default.
return subprocess.run(
[sys.executable, "-B", str(root / "scripts/domain_report.py"), *args],
cwd=root, capture_output=True, encoding="utf-8",
env={**os.environ, "PYTHONIOENCODING": "ascii"},
)
def _assert_report(text):
assert text.startswith("# Domain quotient report (report-only)\n")
assert "Generated by `scripts/domain_report.py`" in text
assert "analyzed-inputs content fingerprint: sha256" in text
assert "`ouroboros/a.py` → `ouroboros/b.py` (line 1)" in text
assert "D01 — Первый" in text
assert text.endswith("\n") and not text.endswith("\n\n")
assert "\nstrict:" not in text and "\n cycle:" not in text
def test_report_defaults_to_stdout_without_repository_writes(report_repo):
before = _files(report_repo)
result = _run(report_repo)
assert result.returncode == 0, result.stderr
_assert_report(result.stdout)
assert "strict: 2 module edges, 2 domain edges, 1 cycle groups" in result.stderr
assert "cycle: D01 ⇄ D02" in result.stderr
assert _files(report_repo) == before
def test_report_writes_only_to_explicit_output(report_repo, tmp_path):
before = _files(report_repo)
output = tmp_path / "reports" / "graph.md"
result = _run(report_repo, "--output", str(output))
assert result.returncode == 0, result.stderr
assert result.stdout == ""
_assert_report(output.read_text(encoding="utf-8"))
assert f"wrote {output}" in result.stderr
assert "strict: 2 module edges" in result.stderr
assert _files(report_repo) == before
@pytest.mark.parametrize("broken", ["manifest", "source"])
def test_untrustworthy_inputs_return_two_without_a_report(report_repo, broken):
if broken == "manifest":
(report_repo / "ouroboros/domains.toml").unlink()
else:
(report_repo / "ouroboros/a.py").write_text("def (\n", encoding="utf-8")
result = _run(report_repo)
assert result.returncode == 2, result.stderr
assert result.stdout == ""
assert ("missing manifest:" if broken == "manifest" else "cannot parse:") in result.stderr

View file

@ -195,4 +195,3 @@ def test_extension_extraction_size_bounds_have_meaningful_headroom():
assert BAND_PATHS[path], f"new extension band entry needs its rationale: {path}"
limit = BAND_MODULE_MAX_LINES
assert count <= limit, f"{path}: {count} lines exceeds its {limit}-line bound"
assert counts["ouroboros.extension_plugin_api"] >= 600

View file

@ -1,7 +1,7 @@
"""ABI 7.0 (ABI-3): per-alias removal pins for the five gateway compat aliases.
One test class per alias (F11 axes: declaration / producer / stored tolerance /
migration surface), per docs/v7next/ABI3_GATEWAY_ALIAS_INVENTORY.md. These pins
One test class per alias (declaration / producer / stored tolerance /
migration surface), per docs/architecture/11-frozen-contracts-v1.md. These pins
are the REMOVAL side; the read-tolerance side lives in
tests/test_cost_projection.py and the endpoint behavior in
tests/test_ui_preferences_api.py / tests/test_gateway_history.py.

View file

@ -3,14 +3,14 @@ fresh regeneration and their resolution invariants hold — staleness = red.
The generator half is ``python scripts/regenerate_inventories.py``:
- ``docs/v7next/FROZEN_CONTRACTS_INVENTORY.md`` — ARCHITECTURE §11.1 rows,
- ``docs/inventories/FROZEN_CONTRACTS_INVENTORY.md`` — ARCHITECTURE §11.1 rows,
every referenced owner/anchor path resolved against the tree, plus the
``ouroboros/contracts/`` package-coverage gap list (pinned here: growth of
the gap is red even after regeneration);
- ``docs/v7next/DATA_LAYOUT_INVENTORY.md`` — the ARCHITECTURE "Data layout"
- ``docs/inventories/DATA_LAYOUT_INVENTORY.md`` — the ARCHITECTURE "Data layout"
tree probed entry-by-entry against tracked paths / runtime source literals
(zero UNRESOLVED entries pinned here);
- ``docs/v7next/FACADE_INVENTORY.md`` — the AST-derived ``noqa: F401``
- ``docs/inventories/FACADE_INVENTORY.md`` — the AST-derived ``noqa: F401``
re-export facade inventory over the domain manifest population.
Synthetic tests prove the red branches (missing file, unresolvable entry,

View file

@ -1,14 +1,8 @@
"""Structural contracts for the semantic-no-op git tool extraction.
v7next transplant note (oracle ouroboros_v7_wip @ 9f691656): the reference pin
also freezes the tool-module inventory through ``ouroboros.tool_module_inventory``,
a v7 leaf absent from this tree — that clause returns with its owner. The
reference's ``_publish_git_error`` / ``_publish_review_blocked`` rows are the
typed-result cutover (F2 organ, not carried) and ``_refuse_capped_attempt``
was retired upstream by 386e9417 (Max Review Cycles), so neither appears in
the owner map below. Size bounds are re-based on tip bytes: the tip facade
retains the paid-cycle gate family, the deferred update entry points and the
catalog the oracle-era monolith did not have.
The facade retains the paid-cycle gate family, deferred update entry points
and tool catalog. Its leaves are non-catalog owners without import-time
backedges, and the facade re-exports every moved identity.
"""
from __future__ import annotations
@ -150,4 +144,4 @@ def test_git_extraction_size_bounds_have_meaningful_headroom():
assert counts["ouroboros.tools.git"] <= 1800
assert all(count <= 1000 for name, count in counts.items()
if name != "ouroboros.tools.git")
assert 700 <= counts["ouroboros.tools.git_review_cycle"] <= 1000
assert counts["ouroboros.tools.git_review_cycle"] <= 1000

View file

@ -68,12 +68,7 @@ def test_headless_leaves_are_non_catalog_owners_without_headless_backedges():
for node in ast.walk(tree)
)
# v7next transplant note: the reference test (ouroboros_v7_wip @ 9f691656)
# additionally proves the three modules stay out of the frozen tool-module
# inventory via ouroboros.tool_module_inventory; that leaf belongs to the
# tools domain and is not on this integration branch yet — the clause
# returns with its lane. The static guarantee it rested on is kept above:
# none of the three modules defines get_tools, so no catalog can adopt them.
# None of these modules defines get_tools, so they remain non-catalog owners.
def test_headless_public_export_list_preserves_extraction_and_terminal_file_helpers():
@ -122,7 +117,7 @@ def test_headless_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.headless"] <= 1000
assert all(count <= 1000 for count in counts.values())
assert 400 <= counts["ouroboros.workspace_patch_capture"] <= 1000
assert counts["ouroboros.workspace_patch_capture"] <= 1000
def test_terminal_file_helper_preserves_legacy_ready_without_finalized_timestamp(tmp_path, monkeypatch):

View file

@ -123,10 +123,6 @@ def test_no_runtime_or_settings_surface_still_names_either_key():
# later chapter split cannot silently turn a record of a removal into a
# live surface finding.
"docs/ARCHITECTURE.md",
"ADOPTION_v7next.md", # ...and adopted: the D04 row names
# what it retired, same as the already
# skipped docs/v7next/ ledger. A record
# of a removal is not a live surface
}
from ouroboros.reference_books import book_entrypoint_for
@ -139,7 +135,7 @@ def test_no_runtime_or_settings_surface_still_names_either_key():
# is still documentation and not a live surface.
if book_entrypoint_for(rel) in allowed:
continue
if rel in allowed or rel.startswith(("venv", "node_modules", "docs/v7next/", "docs/archive/")):
if rel in allowed or rel.startswith(("venv", "node_modules", "docs/archive/")):
continue
try:
text = path.read_text(encoding="utf-8")

View file

@ -43,7 +43,7 @@ _LEAVES = (
llm_pricing,
# Not a mixin and not an extraction: the probe transport arrived whole from
# upstream. It is an llm_* leaf all the same, so the leaf rules bind it —
# never import the parent, no cycles, real weight.
# never import the parent and never form cycles.
llm_probe,
llm_stream,
)
@ -259,8 +259,6 @@ def test_llm_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.llm"] <= 750
assert all(count <= 1000 for count in counts.values()), counts
# Every leaf carries real weight; a 40-line leaf would be a seam, not an owner.
assert all(count >= 200 for count in counts.values()), counts
def test_llm_leaf_import_graph_is_acyclic_and_shallow():

View file

@ -483,14 +483,12 @@ def test_lockfileex_refusals_classify_by_the_win32_error():
def test_the_design_note_names_the_exact_kernel_refusal_sets():
"""Round 5.2 corrected the busy set in the code, in its pin and in the
review packet, and left the RATIFIED design note saying EACCES means
contention — the negation of what that same pin asserts. A reader who
implements the note re-opens the finding: a genuine access-denied would
re-contend for the whole 45 s monetary timeout instead of failing closed.
"""EACCES must not be described as contention: a genuine access-denied
would re-contend for the whole 45 s monetary timeout instead of failing
closed if the design note contradicted the implemented errno sets.
So the note names both sets and this compares them, member for member, by
the numbers (EWOULDBLOCK and ENOTSUP are aliases on Linux, not everywhere)."""
note = pathlib.Path(__file__).resolve().parents[1] / "docs" / "v7next" / "DESIGN_USAGE_COMPACTION.md"
note = pathlib.Path(__file__).resolve().parents[1] / "docs" / "USAGE_COMPACTION.md"
spelled = re.findall(r"are exactly ((?:`[A-Z]+`/)+`[A-Z]+`)", note.read_text(encoding="utf-8"))
assert len(spelled) == 2, spelled
unsupported, held = ({getattr(errno, name.strip("`")) for name in group.split("/")} for group in spelled)

View file

@ -1,18 +1,10 @@
"""Facade-identity contract for the v7 L-B loop.py leaf owners (D01 lane).
"""Facade-identity contract for the loop.py leaf owners.
Every member the L-B split moved out of ``ouroboros/loop.py`` keeps a loop.py
re-export under its historical name, so existing callers and monkeypatching
tests keep working unchanged: the loop binding IS the leaf's object, and the
sibling leaves' D33 call-time handle reads (``_loop().X``) resolve through this
module as the family rendezvous.
v7next transplant note: the reference (ouroboros_v7_wip @ 9f691656) later spent
the private half of this facade (its L3 package, RETIRED_FROM_LOOP) by
re-homing every loop-private test import to its leaf owner. That trimming is a
consumer-rebind wave, not part of the byte-preserving relocation, and does NOT
ride with the D01 lane: on this tree the tip consumer surface still addresses
every moved name at ``ouroboros.loop``, so the FULL re-export surface is the
contract here (see docs/v7next/LEDGER_CORRECTIONS.md, D01 lane).
``ouroboros/loop.py`` re-exports its leaf owners' members under the names
existing callers and monkeypatching tests use. The loop binding IS the leaf's
object, and sibling leaves' call-time handle reads (``_loop().X``) resolve
through this module as the family rendezvous. The full re-export surface is
required while those consumers address the names through ``ouroboros.loop``.
"""
from __future__ import annotations

View file

@ -1,6 +1,6 @@
"""CPL-5 pins: the ``model-visible ⟺ logged`` invariant at the model_send seam.
"""Pins the ``model-visible ⟺ logged`` invariant at the model_send seam.
Design contract: ``docs/v7next/DESIGN_MODEL_VISIBLE_LOGGED.md`` (F15-narrowed).
Design contract: ``docs/MODEL_SEND_OBSERVABILITY.md``.
Forward — every physical attempt seals its exact send copy before dispatch and
the seam reconstructs that durable record and byte-compares it ON THE CALL;
a mismatch is a typed durable fact, never a second dispatch gate. Exclusions
@ -425,7 +425,7 @@ def test_a_compacted_attempt_is_recorded_history_not_an_orphan_seal(data_root):
archive segment, on purpose. Asking the live file alone would make every
folded attempt a durable orphan_seal fact on the monetary/dispatch
invariant, at every startup, for history that is perfectly well recorded
(``docs/v7next/DESIGN_USAGE_COMPACTION.md`` §10: the verdict consults the
(``docs/USAGE_COMPACTION.md`` §10: the verdict consults the
union)."""
from ouroboros import usage_compaction as uc

View file

@ -27,8 +27,9 @@ half below has no such bound and holds on the working tree forever.
The entrypoint preamble is deliberately NOT part of the byte proof: it was
replaced by one merged/authored paragraph plus the `## Chapters` membership
list, and `docs/reference-books-migration.md` records what it said before. Its
H1 line IS pinned here, because that line is the release version carrier.
list. The historical `docs/reference-books-migration.md` blob at the split
commit records the former preamble; that file is no longer needed in HEAD.
The H1 line IS pinned here, because it is the release version carrier.
"""
import hashlib
@ -44,8 +45,8 @@ REPO = pathlib.Path(__file__).resolve().parents[1]
# The integration base this migration was cut from.
MIGRATION_BASE = "5585133db86419c1a28673e498de4fb13c6b2d1e"
# Found by content, not pinned: the commit that ADDED the operator transfer
# table is the split commit.
# Historical lookup key, not a current-file dependency: the commit that
# ADDED the former operator transfer table is the split commit.
TRANSFER_TABLE = "docs/reference-books-migration.md"
# Recorded at the base commit by the split itself (and mirrored in the transfer
@ -174,16 +175,3 @@ def test_no_chapter_body_was_reheaded_into_a_duplicate_title():
titles = [h.title for source in (book.entrypoint, *book.chapters) for h in source.headings]
duplicates = sorted({t for t in titles if titles.count(t) > 1})
assert not duplicates, f"{book_id}: ambiguous section titles {duplicates}"
def test_the_transfer_table_is_committed_and_is_not_a_book_member():
table = REPO / TRANSFER_TABLE
assert table.is_file(), "the operator transfer table must be reviewable"
text = table.read_text(encoding="utf-8")
assert MIGRATION_BASE in text
for book_id in BOOK_ENTRYPOINTS:
book = load_reference_book(REPO, book_id)
assert TRANSFER_TABLE not in [c.source_path for c in book.chapters]
for chapter in book.chapters:
assert f"`{chapter.source_path}`" in text, chapter.source_path
assert OLD_MONOLITHS[book_id]["moved_sha256"] in text

View file

@ -1,7 +1,6 @@
"""ABI-4 ``ResolvedModelTarget`` — the typed resolved-model destination.
The suite name is fixed by docs/v7next/DESIGN_RESOLVED_MODEL_TARGET.md: it pins
frozen-ness, value identity, construction at each existing resolution seam
Pins immutability, value identity and construction at each existing resolution seam
(the cross-model fallback ladder, the reviewer model lists, the delegated
route), and the consumer-sweep grep pins (no comma/at re-parsing beside a seam
that already yields the dataclass).

View file

@ -195,5 +195,5 @@ def test_review_helpers_extraction_size_bounds_have_meaningful_headroom():
# the next addition is a governance-document leaf beside
# `review_prompt_text` and `review_file_pack`, not a smaller docstring.
assert counts["ouroboros.tools.review_helpers"] <= 925
assert 300 <= counts["ouroboros.tools.review_prompt_text"] <= 1000
assert 400 <= counts["ouroboros.tools.review_file_pack"] <= 1000
assert counts["ouroboros.tools.review_prompt_text"] <= 1000
assert counts["ouroboros.tools.review_file_pack"] <= 1000

View file

@ -192,6 +192,6 @@ def test_review_state_extraction_size_bounds_have_meaningful_headroom():
}
assert all(count <= 1000 for count in counts.values()), counts
assert counts["ouroboros.review_state"] <= 850
assert 300 <= counts["ouroboros.review_state_records"] <= 1000
assert 600 <= counts["ouroboros.review_state_model"] <= 1000
assert 150 <= counts["ouroboros.review_state_custody"] <= 600
assert counts["ouroboros.review_state_records"] <= 1000
assert counts["ouroboros.review_state_model"] <= 1000
assert counts["ouroboros.review_state_custody"] <= 600

View file

@ -133,4 +133,4 @@ def test_review_substrate_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.review_substrate"] <= 900
assert all(count <= 1000 for count in counts.values())
assert 300 <= counts["ouroboros.review_verdict"] <= 1000
assert counts["ouroboros.review_verdict"] <= 1000

View file

@ -139,4 +139,4 @@ def test_scope_review_extraction_size_bounds_have_meaningful_headroom():
}
assert all(count <= 1000 for count in counts.values()), counts
assert counts["ouroboros.tools.scope_review"] <= 1000
assert 500 <= counts["ouroboros.tools.scope_review_pack"] <= 1000
assert counts["ouroboros.tools.scope_review_pack"] <= 1000

View file

@ -76,11 +76,10 @@ _MOVED_OWNERS = {
# Process-scoped state and the composition itself: a leaf that needed one of
# these would have to import the parent back, so they must stay defined in
# server.py rather than arriving through an import. The restart transaction —
# the deferred drain record and the three functions around it — stays here too
# (HOT-DEFERRED): the upstream delegation train coupled the performer to
# ``main()`` through the written module global
# the deferred drain record and the three functions around it — stays here too:
# the performer and ``main()`` share the written module global
# ``_planned_delegate_restart_transaction_id``, so a byte-preserving relocation
# would fork that state (docs/v7next/LEDGER_CORRECTIONS.md, D11 lane).
# would fork that state.
_SERVER_OWNED = (
"_planned_delegate_restart_transaction_id",
"_pending_restart",
@ -200,8 +199,7 @@ def test_server_extraction_size_bounds_have_meaningful_headroom():
# server.py keeps the lifespan, the supervisor loop, the owner-command
# dispatch, the process state those three need, AND (on this tree) the
# deferred restart transaction plus post-cutoff upstream drift, so the
# bound is looser than the reference's 1500 until the delegation organ
# (F2) frees the restart rows.
# bound includes the restart transaction state owned by the composition root.
assert counts["server"] <= 1700
assert 400 <= counts["ouroboros.server_routing_context"] <= 1000
assert 400 <= counts["ouroboros.server_owner_routing"] <= 1000
assert counts["ouroboros.server_routing_context"] <= 1000
assert counts["ouroboros.server_owner_routing"] <= 1000

View file

@ -1,17 +1,9 @@
"""Structural contracts for the semantic-no-op shell tool extraction.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656) with the following
identity continuations to THIS tree's bytes:
1. Ten output-audit owners the reference placed in ``shell_outputs`` were
relocated by upstream itself into ``ouroboros/tools/shell_audit.py`` (a
post-cutoff upstream extraction); the ownership map below names the
upstream owner for those rows and the facade identity clause still holds
for every one of them.
2. The frozen-tool-inventory clauses are dropped: ``ouroboros.tool_module_
inventory`` is a D04-family v7 leaf absent from this tree; the
non-catalog-owner and no-backedge clauses keep the structural half of that
contract. The inventory clause returns with its leaf.
The output-audit helpers live in ``ouroboros.tools.shell_audit``. The owner
map names that module alongside the process, output and effects leaves;
the facade identity clause covers every moved helper. The leaves remain
non-catalog owners without backedges into the facade.
"""
from __future__ import annotations
@ -160,4 +152,4 @@ def test_shell_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.tools.shell"] <= 800
assert all(count <= 1000 for count in counts.values())
assert 400 <= counts["ouroboros.tools.shell_outputs"] <= 1000
assert counts["ouroboros.tools.shell_outputs"] <= 1000

View file

@ -149,4 +149,4 @@ def test_skill_review_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.skill_review"] <= 900
assert all(count <= 1000 for count in counts.values())
assert 300 <= counts["ouroboros.skill_review_prompt"] <= 1000
assert counts["ouroboros.skill_review_prompt"] <= 1000

View file

@ -1,20 +1,12 @@
"""Structural contracts for the semantic-no-op tool_access extraction.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656) with four disclosed
adaptations to THIS tree:
The no-backedge clause forbids module-level imports of the facade. Leaves
read parent-owned rebindable names through a call-time module handle, so
those reads do not introduce an import-time cycle.
1. The frozen-tool-inventory clause is dropped: ``ouroboros.tool_module_inventory``
is a v7 leaf this tree does not carry yet; the clause returns with that leaf.
2. The no-backedge clause asserts no MODULE-LEVEL (import-time) import of the
facade: on this tree the leaves deliberately read parent-owned rebindable
names through a call-time module handle (the owner-approved D18/D33
mechanical exception), which is not an import-time cycle.
3. The one-matrix clause checks identity through the facade re-export only:
the user_files leaf reads ``_POLICY`` through the call-time handle instead
of binding a module attribute, so the same-object guarantee holds by
construction (there is exactly one binding, on the facade).
4. The facade size bound is kept at the reference's 900; this tree's facade is
the tip monolith minus the moved spans and lands under it.
The policy matrix identity is checked through the facade re-export. The
user_files leaf reads ``_POLICY`` through its call-time handle instead of
binding another module attribute, preserving the same-object guarantee.
"""
from __future__ import annotations
@ -154,4 +146,4 @@ def test_tool_access_extraction_size_bounds_have_meaningful_headroom():
}
assert counts["ouroboros.tool_access"] <= 900
assert all(count <= 1000 for count in counts.values())
assert 200 <= counts["ouroboros.tool_access_user_files"] <= 1000
assert counts["ouroboros.tool_access_user_files"] <= 1000

View file

@ -1,7 +1,7 @@
"""CPL4-C6 pins: the seq-preserving compaction pass over the monetary ledger.
"""Pins the seq-preserving compaction pass over the monetary ledger.
Design contract: docs/v7next/DESIGN_USAGE_COMPACTION.md. The invariants
pinned here are monetary-authority invariants (owner sanction 1A):
Design contract: docs/USAGE_COMPACTION.md. The invariants
pinned here are monetary-authority invariants:
1. decimal-exact money before/after; the production projections render EQUAL;
2. in-flight (unsettled) rows never fold and stay transitionable;
@ -10,7 +10,7 @@ pinned here are monetary-authority invariants (owner sanction 1A):
6. idempotent kinds (subscription/external/legacy) never fold, so their replay dedup keeps working;
7. trigger policy: config SSOT threshold, thrash guard, verify-abort = no-op.
The reader side of the same organ — invariant 5 (the CPL-5 join across
The reader side of the same organ — invariant 5 (the model-send join across
chained compactions) and invariant 8 (baseline rows are legal only as the
leading block) — lives in ``tests/test_usage_compaction_archive.py``; the
fixtures both modules share live in ``tests/fixtures_usage_compaction.py``.

View file

@ -1,10 +1,9 @@
"""CPL4-C6 pins: the archive reader of the compacted monetary ledger.
"""Pins the archive reader of the compacted monetary ledger.
Design contract: docs/v7next/DESIGN_USAGE_COMPACTION.md. The invariants
pinned here are the reader-side half of the same monetary-authority set
(owner sanction 1A):
Design contract: docs/USAGE_COMPACTION.md. The invariants
pinned here are the reader-side half of the same monetary-authority set:
5. every pre-compaction attempt_id stays resolvable (live ∪ archive; the CPL-5 join) across chained compactions, tamper-evident;
5. every pre-compaction attempt_id stays resolvable (live ∪ archive; the model-send join) across chained compactions, tamper-evident;
8. baseline rows are legal only as the leading block.
The pass side — invariants 1, 2, 3, 4, 6 and 7 — lives in
@ -57,7 +56,7 @@ def _rewrite_header(data_root, header):
uc._CHAIN_UNION_CACHE.clear()
# --- 5: CPL-5 join surface ---------------------------------------------------
# --- 5: model-send join surface ----------------------------------------------
def test_every_attempt_id_stays_resolvable_across_chained_compactions(data_root):
_seed_mixed_ledger(data_root)
@ -468,7 +467,8 @@ def test_a_path_inspection_the_reader_cannot_make_is_typed_corruption(data_root,
"""pathlib re-raises every OSError but ENOENT/ENOTDIR/EBADF/ELOOP and turns a
symlink LOOP into RuntimeError, so the reader's bounds — both archive levels, the
named segment, its resolution — must type EACCES/EIO/a loop themselves or a bare
error escapes the CPL-5 sweep's UNKNOWN mapping. Real shape first: a segment directory readable but not searchable."""
error escapes the model-send reconciliation sweep's UNKNOWN mapping.
Real shape first: a segment directory readable but not searchable."""
_, segment = compacted
if not (platform_layer.IS_WINDOWS or getattr(os, "geteuid", lambda: 1)() == 0):
segment.parent.chmod(0o600)
@ -496,7 +496,7 @@ def test_unreadable_leading_row_is_typed_corruption_not_absence(data_root, compa
encoding="utf-8")
with pytest.raises(UsageLedgerCorrupt):
uc.archived_attempt_ids(data_root)
# The CPL-5 join must reach UNKNOWN, never "no attempt row" (orphan seal).
# The model-send join must reach UNKNOWN, never "no attempt row" (orphan seal).
with pytest.raises(UsageLedgerCorrupt):
uc.usage_attempt_recorded(data_root, folded[0], live_ids=set())

View file

@ -1,258 +0,0 @@
"""The v7next release bar, executed.
``scripts/v7next_adoption.py`` is the checker that says whether
``ADOPTION_v7next.md`` still describes the tree. Until this file existed the
checker was run by hand, which is how a whole upstream-train row (sync #2,
``TRAIN-F6b-f3fbfdbb``) could be deleted by a stale-base overwrite and leave
both validator modes at rc 0. This suite runs ``validate()`` on the live
manifest in both modes and drives one mutant per rule that the deletion
taught us to want, so the bar is executed by something automatic.
Deliberately NOT a CI-workflow change: ``.github/workflows/ci.yml`` is a
protected file and the default pytest lane already carries this file.
"""
from __future__ import annotations
import copy
import pathlib
import pytest
from scripts.v7next_adoption import (
DEFERRED_OUT_OF_V70,
OPERATOR,
OWNER,
REQUIRED_PHASE,
REQUIRED_TRAINS,
declared_deferral_authorities,
manifest_prose,
parse_rows,
validate,
)
REPO = pathlib.Path(__file__).resolve().parent.parent
MANIFEST = REPO / "ADOPTION_v7next.md"
@pytest.fixture(scope="module")
def rows() -> list[dict[str, str]]:
parsed, errors = parse_rows(MANIFEST.read_text(encoding="utf-8"))
assert not errors, errors
assert parsed, "the manifest table parsed to zero rows"
return parsed
def _without(rows: list[dict[str, str]], row_id: str) -> list[dict[str, str]]:
kept = [r for r in rows if r["id"] != row_id]
assert len(kept) == len(rows) - 1, f"{row_id} is not in the manifest"
return kept
def _mutate(rows: list[dict[str, str]], row_id: str, **cells: str) -> list[dict[str, str]]:
out = copy.deepcopy(rows)
for r in out:
if r["id"] == row_id:
r.update(cells)
return out
raise AssertionError(f"{row_id} is not in the manifest")
def _first_done(rows: list[dict[str, str]]) -> dict[str, str]:
for r in rows:
if r["status"] == "done" and "::" in r["verification hook"]:
return r
raise AssertionError("no done row carries a ::nodeid hook")
@pytest.fixture(scope="module")
def prose() -> str:
return manifest_prose(MANIFEST.read_text(encoding="utf-8"))
def test_the_live_manifest_passes_both_modes(rows, prose):
"""The manifest on this tree is the thing the bar is about — table AND the
prose around it, the way ``main()`` runs it."""
assert validate(copy.deepcopy(rows), release=False, prose=prose) == []
assert validate(copy.deepcopy(rows), release=True, prose=prose) == []
def test_manifest_prose_is_everything_but_the_table(prose):
assert "Notes:" in prose
assert not [line for line in prose.splitlines() if line.startswith("|")]
@pytest.mark.parametrize("train_id", sorted(REQUIRED_TRAINS))
@pytest.mark.parametrize("release", [False, True])
def test_deleting_an_upstream_train_row_turns_the_bar_red(rows, train_id, release):
"""The mutant that actually happened: a whole-file overwrite drops a train
row. It must be red in BOTH modes — the deletion in 285ab66d survived
because the default mode was the one being run."""
errors = validate(_without(rows, train_id), release=release)
assert any(train_id in e for e in errors), errors
@pytest.mark.parametrize("release", [False, True])
def test_repointing_a_train_row_at_another_merge_turns_the_bar_red(rows, release):
"""A train row that no longer names its own upstream tip and merge is not a
record of that train."""
train_id = sorted(REQUIRED_TRAINS)[0]
errors = validate(_mutate(rows, train_id, what="upstream train, details elsewhere"),
release=release)
assert any(train_id in e for e in errors), errors
def test_a_bogus_hook_nodeid_turns_the_bar_red(rows):
"""A hook may name a test that does not exist only if nothing checks it.
Paths were already resolved; the ``::nodeid`` half was free text."""
victim = _first_done(rows)
bogus = "tests/test_smoke.py::test_no_such_pin_was_ever_written"
errors = validate(_mutate(rows, victim["id"], **{"verification hook": bogus}),
release=False)
assert any("test_no_such_pin_was_ever_written" in e for e in errors), errors
@pytest.mark.parametrize("hook", [
"the suites this row moved bytes in", # prose only
"tests/test_no_such_suite_was_ever_written.py", # missing file
"tests/../scripts/v7next_adoption.py", # escapes tests/
"tests/test_smoke.py::test_no_such_pin_was_ever_written", # bogus nodeid
])
def test_a_hook_error_does_not_claim_the_release_bar(rows, hook):
"""Hook resolution runs for every ``done`` row in BOTH modes — it is a
property of a shipped row, not of the ``--release`` invocation. A message
reported in the default mode must therefore not say ``release:``, or the
reader is told to look for a switch that has nothing to do with it."""
victim = _first_done(rows)
errors = validate(_mutate(rows, victim["id"], **{"verification hook": hook}),
release=False)
assert errors, "the hook shape was accepted in the default mode"
assert not [e for e in errors if e.startswith("release:")], errors
def test_a_hook_nodeid_that_names_a_real_test_stays_green(rows):
"""The AST read must accept what the manifest legitimately names, including
a data carrier (``tests/_shared.py::SETTINGS_WRITERS``) — a hook may point
at the inventory a pin closes, not only at a function."""
victim = _first_done(rows)
good = ("tests/_shared.py::settings_writers + tests/_shared.py::SETTINGS_WRITERS "
"+ tests/test_smoke.py::test_size_ratchet_transition_against_explicit_base")
assert validate(_mutate(rows, victim["id"], **{"verification hook": good}),
release=False) == []
@pytest.mark.parametrize("marker", ["NOT DONE", "OPEN RESIDUAL", "not integrated yet",
"still owed", "read pending"])
def test_a_done_row_that_says_it_is_not_done_turns_the_bar_red(rows, marker):
"""The contradiction this wave found six times: a row whose text says the
work is open while its status cell says ``done``."""
victim = _first_done(rows)
text = f"{victim['what']} — {marker} on this tree"
errors = validate(_mutate(rows, victim["id"], what=text), release=False)
assert any(victim["id"] in e and "done" in e for e in errors), errors
def test_a_named_residual_clause_is_the_escape_not_a_second_status(rows):
"""A shipped row may carry an open residual — that is what ``residual:``
declares. The rule refuses the contradiction, not the disclosure."""
victim = _first_done(rows)
text = f"{victim['what']} — NOT DONE for the review surfaces; residual: the migration is post-release"
assert validate(_mutate(rows, victim["id"], what=text), release=False) == []
def test_a_post_release_row_needs_a_recorded_deferral(rows):
"""post-release is the one state that leaves the release bar. An id nobody
recorded cannot take it."""
victim = _first_done(rows)
errors = validate(_mutate(rows, victim["id"], disposition="post-release",
status="deferred", phase="POST"), release=True)
assert any(victim["id"] in e and "DEFERRED_OUT_OF_V70" in e for e in errors), errors
def test_a_required_row_cannot_be_parked_post_release_by_the_operator(rows, monkeypatch):
"""The property the old frozenset carried: a row of the owner-approved
inventory leaves 7.0 only by an owner decision. Operator authority exists
for disclosures, and must not become a way past that."""
monkeypatch.setitem(DEFERRED_OUT_OF_V70, "ABI-8", OPERATOR)
errors = validate(copy.deepcopy(rows), release=True)
assert any("ABI-8" in e and "owner decision" in e for e in errors), errors
def test_prose_that_calls_a_row_rowless_turns_the_bar_red(rows):
"""The contradiction that stood two days past a green bar: the Notes said
W4-F3/W4-F4 «get no row» after d348ea46 had made them rows. The validator
read rows only; now the prose's ids are resolved against the table."""
victim = rows[0]["id"]
notes = f"Notes:\n- No-row ids: {victim} — a disclosed observation, not work owed."
errors = validate(copy.deepcopy(rows), release=False, prose=notes)
assert any(e.startswith("prose:") and victim in e and "No-row ids" in e
for e in errors), errors
def test_prose_that_names_a_ghost_id_needs_a_no_row_declaration(rows):
"""The mirror: prose naming an id the table does not have is red unless the
prose declares it rowless — the declared form is the escape, not phrasing."""
ghost = "DEFER-NO-SUCH-ROW"
assert all(r["id"] != ghost for r in rows)
notes = f"Notes:\n- {ghost} was folded into another row (see D02)."
errors = validate(copy.deepcopy(rows), release=False, prose=notes)
assert any(e.startswith("prose:") and ghost in e for e in errors), errors
assert validate(copy.deepcopy(rows), release=False,
prose=notes + f"\n- No-row ids: {ghost}.") == []
def test_prose_id_grammar_ignores_plan_decisions_and_lane_labels(rows):
"""`D-14` (a plan decision), `CPL4-C6` (a lane label) and the schema's own
pattern words (`Dnn`, `ABI-n`) are not ids and must not be reported."""
notes = "Notes:\n- D-14 sent CPL4-C6 here; `Dnn` and `ABI-n` are patterns."
assert validate(copy.deepcopy(rows), release=False, prose=notes) == []
def _post_release_rows(rows, authority):
return [r for r in rows if r["disposition"] == "post-release"
and DEFERRED_OUT_OF_V70.get(r["id"]) == authority]
def test_an_owner_deferral_row_must_carry_the_owner_quote(rows):
"""The record and the row tell one story. An OWNER value in
DEFERRED_OUT_OF_V70 beside a row with no ``owner verbatim «…»`` quote is
the drift the record's own comment block showed (E2/E3, spec §6.4)."""
victim = _post_release_rows(rows, OWNER)[0]
unquoted = victim["what"].replace("owner verbatim «", "owner said «")
errors = validate(_mutate(rows, victim["id"], what=unquoted), release=False)
assert any(victim["id"] in e and "owner deferral" in e for e in errors), errors
def test_the_notes_declare_the_deferral_authorities_the_register_records(prose):
"""The Notes carry the register's mirror in the one declared form, and it
agrees with ``DEFERRED_OUT_OF_V70`` id for id. Free prose about authority is
not read: the Notes called W4-F4 operator-disclosed for a day after the
register made it an owner deferral."""
assert declared_deferral_authorities(prose) == DEFERRED_OUT_OF_V70
@pytest.mark.parametrize("mutant", ["register_moves", "declaration_omits", "declaration_invents"])
def test_a_deferral_declaration_that_disagrees_with_the_register_turns_the_bar_red(
rows, prose, monkeypatch, mutant):
"""Both directions and both edges: the register moves under a standing
declaration, the declaration drops a recorded id, the declaration invents one."""
if mutant == "register_moves":
monkeypatch.setitem(DEFERRED_OUT_OF_V70, "W4-F4", OPERATOR)
text, needle = prose, "W4-F4 is owner while DEFERRED_OUT_OF_V70 records operator-disclosed"
elif mutant == "declaration_omits":
text, needle = prose.replace("W4-F4 owner, ", ""), "omits W4-F4"
else:
text, needle = prose.replace("W4-F4 owner,", "W4-F4 owner, DEFER-NO-SUCH-ROW owner,"), "declares DEFER-NO-SUCH-ROW"
assert text != prose or mutant == "register_moves"
errors = validate(copy.deepcopy(rows), release=False, prose=text)
assert any(e.startswith("prose: Deferral authorities") and needle in e for e in errors), errors
def test_an_operator_disclosure_row_must_not_carry_an_owner_quote(rows, monkeypatch):
"""The other direction: a quoted row recorded as operator-disclosed hides
an owner decision behind the weaker authority."""
victim = next(r for r in _post_release_rows(rows, OWNER)
if r["id"] not in REQUIRED_PHASE) # keep the required-inventory rule out of it
monkeypatch.setitem(DEFERRED_OUT_OF_V70, victim["id"], OPERATOR)
errors = validate(copy.deepcopy(rows), release=False)
assert any(victim["id"] in e and OPERATOR in e and "owner quote" in e
for e in errors), errors

View file

@ -1,880 +0,0 @@
"""The Ф0 transplant tool: mechanical D18/D33 module-handle extraction with proof.
Three REAL cases drive the fixtures:
* ``supervisor/queue.py`` -> ``queue_snapshot`` leaf (handle ``_queue``): two
symbols stable since the v7 cut are byte-exact against the reference leaf;
two drifted symbols prove the proof property and the declared-set
recalculation workflow.
* ``ouroboros/loop.py`` -> ``loop_messages`` leaf (handle ``_loop``): the
declared name ``_record_owner_directive`` is ALSO a moved symbol — its own
``def`` stays plain while the sibling reads it through the handle, so parent
monkeypatching keeps intercepting (the v7 re-export pattern).
* ``supervisor/git_ops.py`` -> ``git_ops_remotes`` leaf (handle ``_go``):
three stable symbols, two of them declared-and-moved.
Byte-exact comparisons against the v7_wip reference tree are skipped when the
sibling worktree is absent (set OUROBOROS_V7_REF to point elsewhere); every
proof-property and fail-closed test runs from this repository alone.
"""
from __future__ import annotations
import ast
import importlib.util
import os
import pathlib
import subprocess
import sys
import textwrap
import pytest
REPO = pathlib.Path(__file__).resolve().parents[1]
TOOL_PATH = REPO / "scripts" / "v7next_transplant.py"
_spec = importlib.util.spec_from_file_location("v7next_transplant", TOOL_PATH)
tp = importlib.util.module_from_spec(_spec)
sys.modules[_spec.name] = tp
_spec.loader.exec_module(tp)
V7_REF = pathlib.Path(os.environ.get(
"OUROBOROS_V7_REF",
str(pathlib.Path.home() / "ouro" / "subagent_worktrees" / "v7_wip")))
needs_ref = pytest.mark.skipif(
not (V7_REF / "supervisor" / "queue_snapshot.py").exists(),
reason="v7_wip reference worktree not present (set OUROBOROS_V7_REF)")
def _read(path: pathlib.Path) -> str:
return path.read_text(encoding="utf-8")
def _pinned_upstream(base_sha: str, rel_path: str) -> str:
"""The exact pre-split monolith bytes the real extraction ran against.
Empty when the object is unreachable (a shallow clone without that commit)
or when git itself is not on PATH, and the probes that need it SKIP. Both
reach the same honest marker: catching only a nonzero exit code left the
no-git host failing COLLECTION on OSError at import time, which reads as a
broken suite rather than as the disclosed gap this corpus has. The rest of
the file runs from this repository alone. The corpus is the probe's INPUT: each
real case feeds it to the tool and compares the tool's output against the
landed leaf. Reconstructing the corpus by inverse-normalizing that same
leaf — the earlier fallback — made the tool prove its own transformation
against its own output: the byte comparisons could not fail, `transplant`
could not report a drifted body, and the declared-set recalculation loop
had nothing to recalculate. A green run then proved nothing about the
transplant, and no marker said so.
"""
try:
done = subprocess.run(
["git", "-C", str(REPO), "show", f"{base_sha}:{rel_path}"],
capture_output=True, text=True)
except OSError:
return ""
return done.stdout if done.returncode == 0 else ""
def _span_text(source: str, symbol: str) -> str:
return tp.extract_spans(source, [symbol])[symbol].text
def _recalculate(upstream: str, symbols, declared, handle: str, parent_module: str,
preamble: str, max_rounds: int = 4):
"""The campaign's declared-set recalculation loop, driven by tool reports."""
declared = frozenset(declared)
rounds = []
for _ in range(max_rounds):
try:
result = tp.transplant(upstream, list(symbols), declared, handle,
parent_module=parent_module, preamble=preamble)
return result, declared, preamble, rounds
except tp.TransplantError as exc:
rounds.append(exc)
if exc.kind == "unresolved_names":
suggestions = exc.details["suggestions"]
unknown = {n for n, s in suggestions.items() if s["kind"] == "unknown"}
assert not unknown, f"unresolvable names: {unknown}"
declared |= {n for n, s in suggestions.items() if s["kind"] == "parent_global"}
imports = sorted({s["import"] for s in suggestions.values()
if s["kind"] == "parent_import"})
if imports:
preamble = preamble.rstrip("\n") + "\n" + "\n".join(imports) + "\n"
continue
if exc.kind == "unused_declared":
declared -= set(exc.details["unused"])
continue
raise
pytest.fail("declared-set recalculation did not converge")
# ---------------------------------------------------------------------------
# real case 1: supervisor/queue.py -> queue_snapshot leaf (_queue handle)
#
# The F2.2 lane landed the queue_snapshot/queue_timeouts split, so the LIVE
# queue.py no longer carries these defs. The probe keeps its real-case shape on
# the PRE-SPLIT monolith bytes of the lane base (the D10 recipe for probes
# pinned to live monoliths), and skips when that object is unreachable.
_QUEUE_BASE_SHA = "2878560ed298c4173e65068f16b6d09e672ba19f"
QUEUE_UPSTREAM = _pinned_upstream(_QUEUE_BASE_SHA, "supervisor/queue.py")
needs_queue_corpus = pytest.mark.skipif(
not QUEUE_UPSTREAM,
reason=f"pre-split supervisor/queue.py bytes not reachable via "
f"`git show {_QUEUE_BASE_SHA}`")
# The v7 ledger's declared set for supervisor/queue_snapshot.py (D18).
QUEUE_LEDGER_DECLARED = frozenset({
"ACCEPTANCE_FENCES", "DRIVE_ROOT", "PENDING", "RUNNING",
"_queue_lock", "append_jsonl", "atomic_write_text", "enqueue_task",
})
QUEUE_PREAMBLE = '''"""Queue snapshot leaf (test preamble)."""
from __future__ import annotations
import datetime
import json
import logging
import pathlib
import time
from typing import Optional
def _queue():
"""The parent module, read at call time."""
from supervisor import queue
return queue
log = logging.getLogger(__name__)
'''
@needs_ref
@needs_queue_corpus
def test_queue_stable_symbols_match_the_v7_leaf_byte_for_byte():
"""Symbols whose upstream bodies did not change since the v7 cut transform
into exactly the reference leaf's spans."""
result = tp.transplant(
QUEUE_UPSTREAM, ["_kept_service_pids", "parse_iso_to_ts"],
{"DRIVE_ROOT"}, "_queue", parent_module="supervisor.queue",
preamble=QUEUE_PREAMBLE)
ref_leaf = _read(V7_REF / "supervisor" / "queue_snapshot.py")
for symbol in ("_kept_service_pids", "parse_iso_to_ts"):
assert _span_text(result.leaf_source, symbol) == _span_text(ref_leaf, symbol), symbol
@needs_queue_corpus
def test_queue_drifted_symbols_need_a_recalculated_declared_set():
"""The ledger's declared set no longer covers the drifted upstream bodies:
the tool fails closed naming the new dependencies, classifies each one, and
the mechanical recalculation loop converges to a proven transplant."""
symbols = ["_kept_service_pids", "persist_queue_snapshot",
"parse_iso_to_ts", "restore_pending_from_snapshot"]
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(QUEUE_UPSTREAM, symbols, QUEUE_LEDGER_DECLARED, "_queue",
parent_module="supervisor.queue", preamble=QUEUE_PREAMBLE)
exc = excinfo.value
assert exc.kind == "unresolved_names"
flat = {n for names in exc.details["unresolved"].values() for n in names}
assert {"QUEUE_SNAPSHOT_PATH", "BUDGET_ROOT_FENCES", "utc_now_iso",
"restore_terminalization_retry_rows", "sort_pending",
"QUEUE_SEQ_COUNTER_REF"} <= flat
sugg = exc.details["suggestions"]
# rebindable parent state, only ever assigned under `global` in init():
assert sugg["QUEUE_SNAPSHOT_PATH"]["kind"] == "parent_global"
assert sugg["sort_pending"]["kind"] == "parent_global"
assert sugg["QUEUE_SEQ_COUNTER_REF"]["kind"] == "parent_global"
assert sugg["restore_terminalization_retry_rows"] == {
"kind": "parent_import",
"import": "from supervisor.task_admission import restore_terminalization_retry_rows",
"hint": sugg["restore_terminalization_retry_rows"]["hint"],
}
assert sugg["utc_now_iso"]["kind"] == "parent_import"
result, declared, _preamble, rounds = _recalculate(
QUEUE_UPSTREAM, symbols, QUEUE_LEDGER_DECLARED, "_queue",
"supervisor.queue", QUEUE_PREAMBLE)
assert len(rounds) >= 1
assert QUEUE_LEDGER_DECLARED <= declared
assert {"QUEUE_SNAPSHOT_PATH", "sort_pending", "QUEUE_SEQ_COUNTER_REF"} <= declared
proof = result.proof
assert proof["ok"]
for symbol in symbols:
entry = proof["symbols"][symbol]
assert entry["ast_equal"] and entry["tokens_equal"] and entry["byte_identical"], symbol
assert proof["unread_declared"] == []
# the drifted body now reads the drifted dependencies through the handle
drifted = _span_text(result.leaf_source, "restore_pending_from_snapshot")
assert "_queue().QUEUE_SNAPSHOT_PATH" in drifted
assert "_queue().sort_pending()" in drifted
# and an independent re-verification agrees (the --check path, in-process)
recheck = tp.verify_transplant(QUEUE_UPSTREAM, result.leaf_source, symbols,
declared, "_queue")
assert recheck["ok"]
# ---------------------------------------------------------------------------
# real case 2: ouroboros/loop.py -> loop_messages leaf (_loop handle)
#
# The D01 lane landed the L-B split, so ouroboros/loop.py no longer carries
# these defs. The probe keeps its real-case shape on the PRE-SPLIT monolith
# bytes of the lane base (the D10 lane's recipe for probes pinned to live
# monoliths), and skips when that object is unreachable.
_D01_BASE = "a56bb76a38ca92b39a659b4b6e63e07a76243a4f"
LOOP_UPSTREAM = _pinned_upstream(_D01_BASE, "ouroboros/loop.py")
needs_loop_corpus = pytest.mark.skipif(
not LOOP_UPSTREAM,
reason=f"pre-split ouroboros/loop.py bytes not reachable via "
f"`git show {_D01_BASE}`")
LOOP_PREAMBLE = '''"""Loop messages leaf (test preamble)."""
from __future__ import annotations
import json
from typing import Any, Dict, List
def _loop():
"""The parent loop module, read at call time."""
from ouroboros import loop
return loop
'''
@needs_loop_corpus
def test_loop_declared_name_that_is_also_a_moved_symbol():
"""`_record_owner_directive` moves into the leaf AND stays in the declared
set: its own def is emitted verbatim while `_initialize_owner_directives`
reads it through `_loop()` — patching the parent keeps intercepting."""
result = tp.transplant(
LOOP_UPSTREAM, ["_record_owner_directive", "_initialize_owner_directives"],
{"_record_owner_directive"}, "_loop", parent_module="ouroboros.loop",
preamble=LOOP_PREAMBLE)
assert len(result.rewrites) == 1
assert result.rewrites[0].symbol == "_initialize_owner_directives"
init_span = _span_text(result.leaf_source, "_initialize_owner_directives")
assert "_loop()._record_owner_directive(" in init_span
record_span = _span_text(result.leaf_source, "_record_owner_directive")
assert record_span == _span_text(LOOP_UPSTREAM, "_record_owner_directive")
assert result.proof["ok"]
@needs_ref
@needs_loop_corpus
def test_loop_stable_symbols_match_the_v7_leaf_byte_for_byte():
result = tp.transplant(
LOOP_UPSTREAM, ["_record_owner_directive", "_initialize_owner_directives"],
{"_record_owner_directive"}, "_loop", parent_module="ouroboros.loop",
preamble=LOOP_PREAMBLE)
ref_leaf = _read(V7_REF / "ouroboros" / "loop_messages.py")
for symbol in ("_record_owner_directive", "_initialize_owner_directives"):
assert _span_text(result.leaf_source, symbol) == _span_text(ref_leaf, symbol), symbol
# ---------------------------------------------------------------------------
# real case 3: supervisor/git_ops.py -> git_ops_remotes leaf (_go handle)
#
# The D10 lane landed the G1 split, so the LIVE git_ops.py is now the facade
# (the three probe symbols are re-exports there, not spans). The corpus this
# probe transplants is the pre-split monolith, pinned at the lane's base SHA —
# the exact bytes the real extraction ran against.
_GO_BASE_SHA = "a56bb76a38ca92b39a659b4b6e63e07a76243a4f"
GO_UPSTREAM = _pinned_upstream(_GO_BASE_SHA, "supervisor/git_ops.py")
needs_go_corpus = pytest.mark.skipif(
not GO_UPSTREAM,
reason=f"pre-split supervisor/git_ops.py bytes not reachable via "
f"`git show {_GO_BASE_SHA}`")
# Ledger set minus BRANCH_DEV: only push_to_remote reads it, and that body
# drifted upstream, so this fixture moves the three stable symbols.
GO_DECLARED = frozenset({
"REPO_DIR", "_configure_credential_helper", "_has_remote",
"configure_remote", "ensure_official_update_remote", "git_capture",
})
GO_PREAMBLE = '''"""Git remotes leaf (test preamble)."""
from __future__ import annotations
import logging
from typing import List, Optional, Tuple
def _go():
"""The parent module, read at call time."""
from supervisor import git_ops
return git_ops
log = logging.getLogger("supervisor.git_ops")
'''
GO_SYMBOLS = ["configure_remote", "configure_personal_remote", "_configure_credential_helper"]
@needs_go_corpus
def test_git_ops_declared_and_moved_symbols_prove():
result = tp.transplant(GO_UPSTREAM, GO_SYMBOLS, GO_DECLARED, "_go",
parent_module="supervisor.git_ops", preamble=GO_PREAMBLE)
assert result.proof["ok"]
assert result.proof["unread_declared"] == []
# configure_remote is itself moved, yet configure_personal_remote reads it
# through the handle (re-export pattern), same for _configure_credential_helper
personal = _span_text(result.leaf_source, "configure_personal_remote")
assert "_go().configure_remote(" in personal
remote = _span_text(result.leaf_source, "configure_remote")
assert "_go()._configure_credential_helper(" in remote
assert "_go()._has_remote(" in remote
@needs_go_corpus
def test_the_f_string_deferred_git_ops_spans_transplant():
"""The two G1 rows the f-string gate deferred (ledger D10 entry 2:
``safe_restart`` / ``prepare_managed_update``, whose messages read the
rebindable BRANCH_DEV/BRANCH_STABLE inside f-strings). The declared set is
the tool's own recalculation, so this pins the report loop too."""
result, declared, _preamble, _rounds = _recalculate(
GO_UPSTREAM, ["safe_restart"], set(), "_go", "supervisor.git_ops",
GO_PREAMBLE)
span = _span_text(result.leaf_source, "safe_restart")
assert {"BRANCH_DEV", "BRANCH_STABLE"} <= declared
assert 'return False, f"Failed checkout {_go().BRANCH_DEV}: {err}"' in span
assert 'return True, f"OK: fell back to {_go().BRANCH_STABLE}"' in span
assert result.proof["ok"] and result.proof["unread_declared"] == []
result2, declared2, _p2, _r2 = _recalculate(
GO_UPSTREAM, ["prepare_managed_update"], set(), "_go",
"supervisor.git_ops", GO_PREAMBLE)
span2 = _span_text(result2.leaf_source, "prepare_managed_update")
assert "BRANCH_DEV" in declared2
assert 'f"Managed updates require the local {_go().BRANCH_DEV!r} branch."' in span2
assert result2.proof["ok"] and result2.proof["unread_declared"] == []
@needs_ref
@needs_go_corpus
def test_git_ops_stable_symbols_match_the_v7_leaf_byte_for_byte():
result = tp.transplant(GO_UPSTREAM, GO_SYMBOLS, GO_DECLARED, "_go",
parent_module="supervisor.git_ops", preamble=GO_PREAMBLE)
ref_leaf = _read(V7_REF / "supervisor" / "git_ops_remotes.py")
for symbol in GO_SYMBOLS:
assert _span_text(result.leaf_source, symbol) == _span_text(ref_leaf, symbol), symbol
# ---------------------------------------------------------------------------
# synthetic corpus: scope precision, byte preservation, fail-closed behavior
SYN = textwrap.dedent('''\
"""Synthetic upstream."""
import functools
import json
PENDING = []
RUNNING = {}
LIMIT = 5
def helper(x):
return x + 1
def uses(x):
with_lock = PENDING
return json.dumps([with_lock, RUNNING, helper(x)])
def shadows(PENDING, flag=True):
RUNNING = "local"
data = [PENDING for PENDING in range(3)]
def inner():
return RUNNING
return PENDING, RUNNING, data, inner, LIMIT
def strings_and_comments():
# PENDING in a comment stays a comment
s = "PENDING and RUNNING in a string"
return s, PENDING
def kwarg_positions(obj_attr):
return dict(PENDING=PENDING, RUNNING=2), obj_attr.PENDING
def mystery():
return NEVER_DEFINED
class Widget:
kind = "w"
def total(self):
return LIMIT + len(PENDING)
@functools.lru_cache(maxsize=None)
def cached():
return LIMIT
async def fetch():
return RUNNING
A = B = []
''')
SYN_PRE = '"""Leaf."""\n\nfrom __future__ import annotations\n\nimport functools\nimport json\n'
def _syn(symbols, declared, preamble=SYN_PRE):
return tp.transplant(SYN, symbols, declared, "_up", parent_module="synmod",
preamble=preamble)
def test_shadows_are_never_rewritten():
result = _syn(["shadows"], {"LIMIT"})
span = _span_text(result.leaf_source, "shadows")
assert "_up().PENDING" not in span # parameter and comprehension target
assert "_up().RUNNING" not in span # function local + closure free var
assert "return PENDING, RUNNING, data, inner, _up().LIMIT" in span
assert result.proof["ok"]
def test_module_reads_rewritten_strings_and_comments_untouched():
result = _syn(["uses", "strings_and_comments"], {"PENDING", "RUNNING", "helper"})
uses = _span_text(result.leaf_source, "uses")
assert "with_lock = _up().PENDING" in uses
assert "[with_lock, _up().RUNNING, _up().helper(x)]" in uses
strings = _span_text(result.leaf_source, "strings_and_comments")
assert '"PENDING and RUNNING in a string"' in strings
assert "# PENDING in a comment stays a comment" in strings
assert "return s, _up().PENDING" in strings
def test_keyword_names_and_attribute_accesses_untouched():
result = _syn(["kwarg_positions"], {"PENDING"})
span = _span_text(result.leaf_source, "kwarg_positions")
assert "dict(PENDING=_up().PENDING, RUNNING=2)" in span
assert "obj_attr.PENDING" in span
assert "obj_attr._up()" not in span
def test_class_methods_rewritten_class_attribute_untouched():
result = _syn(["Widget"], {"LIMIT", "PENDING"})
span = _span_text(result.leaf_source, "Widget")
assert "return _up().LIMIT + len(_up().PENDING)" in span
assert 'kind = "w"' in span
def test_decorated_and_async_symbols_move_with_bytes_preserved():
result = _syn(["cached", "fetch"], {"LIMIT", "RUNNING"})
cached = _span_text(result.leaf_source, "cached")
assert cached.startswith("@functools.lru_cache(maxsize=None)\n")
assert "return _up().LIMIT" in cached
assert "return _up().RUNNING" in _span_text(result.leaf_source, "fetch")
def test_unresolved_names_fail_closed_with_classified_suggestions():
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(SYN, ["uses", "mystery"], set(), "_up", parent_module="synmod")
exc = excinfo.value
assert exc.kind == "unresolved_names"
assert set(exc.details["unresolved"]["uses"]) == {"PENDING", "RUNNING", "helper", "json"}
assert exc.details["unresolved"]["mystery"] == ["NEVER_DEFINED"]
sugg = exc.details["suggestions"]
assert sugg["PENDING"]["kind"] == "parent_global"
assert sugg["helper"]["kind"] == "parent_global"
assert sugg["json"] == {"kind": "parent_import", "import": "import json",
"hint": sugg["json"]["hint"]}
assert sugg["NEVER_DEFINED"]["kind"] == "unknown"
def test_unused_declared_names_fail_closed():
with pytest.raises(tp.TransplantError) as excinfo:
_syn(["uses"], {"PENDING", "RUNNING", "helper", "LIMIT"})
assert excinfo.value.kind == "unused_declared"
assert excinfo.value.details["unused"] == ["LIMIT"]
def test_global_rebinding_of_declared_name_fails_closed():
src = SYN + "\ndef bump():\n global LIMIT\n LIMIT = LIMIT + 1\n"
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["bump"], {"LIMIT"}, "_up", parent_module="synmod",
preamble=SYN_PRE)
assert excinfo.value.kind == "violation"
assert "global LIMIT" in excinfo.value.message
def test_global_rebinding_of_unmoved_state_fails_closed():
src = SYN + "\ndef flip():\n global RUNNING\n RUNNING = {}\n"
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["flip"], set(), "_up", parent_module="synmod", preamble=SYN_PRE)
assert excinfo.value.kind == "violation"
assert "did not move" in excinfo.value.message
def test_import_time_reads_fail_closed():
for extra, symbol in [
("\nCONST = LIMIT + 1\n", "CONST"), # module level
("\ndef defaulted(x=LIMIT):\n return x\n", "defaulted"), # default argument
("\nclass Bad:\n size = LIMIT\n", "Bad"), # class body
]:
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(SYN + extra, [symbol], {"LIMIT"}, "_up",
parent_module="synmod", preamble=SYN_PRE)
assert excinfo.value.kind == "violation", symbol
assert "import-time read" in excinfo.value.message, symbol
SYN_FSTR = SYN + '\ndef show():\n return f"limit={LIMIT}"\n'
def test_fstring_reads_of_declared_names_are_rewritten_and_proved():
"""F5 tool row: a declared name read inside an f-string is an ordinary
call-time read. The literal halves — including a nested string subscript
that spells the same name — stay byte-identical, and the format spec is
an expression like any other."""
src = SYN + (
'\ndef show(k, d):\n'
' return f"limit={LIMIT} {k!r} {d[\'LIMIT\']} {LIMIT:>{LIMIT}}"\n')
result = tp.transplant(src, ["show"], {"LIMIT"}, "_up", parent_module="synmod",
preamble=SYN_PRE)
span = _span_text(result.leaf_source, "show")
assert ('f"limit={_up().LIMIT} {k!r} {d[\'LIMIT\']} '
'{_up().LIMIT:>{_up().LIMIT}}"') in span
entry = result.proof["symbols"]["show"]
assert entry["ast_equal"] and entry["tokens_equal"] and entry["byte_identical"]
assert entry["ast_inverse_equal"]
assert result.proof["ok"]
def test_fstring_debug_specs_fail_closed():
"""`f"{X=}"` PRINTS the expression text, which CPython derives from the very
bytes the handle rewrite changes: `f"{_up().LIMIT=}"` inverts byte-perfectly
while its output silently became `_up().LIMIT=`. Only the tree-level inverse
sees that Constant, so it is the check that refuses the span."""
src = SYN + '\ndef show():\n return f"{LIMIT=}"\n'
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["show"], {"LIMIT"}, "_up", parent_module="synmod",
preamble=SYN_PRE)
assert excinfo.value.kind == "proof"
entry = excinfo.value.details["proof"]["symbols"]["show"]
assert entry["byte_identical"] and not entry["ast_inverse_equal"]
def test_proof_detects_tampering_inside_an_fstring_literal():
result = tp.transplant(SYN_FSTR, ["show"], {"LIMIT"}, "_up",
parent_module="synmod", preamble=SYN_PRE)
tampered = result.leaf_source.replace('f"limit=', 'f"cap=')
report = tp.verify_transplant(SYN_FSTR, tampered, ["show"], {"LIMIT"}, "_up")
assert not report["ok"]
entry = report["symbols"]["show"]
assert not entry["tokens_equal"] and not entry["byte_identical"]
def test_wildcard_imports_fail_closed():
src = "from os.path import *\n\ndef f():\n return join('a', 'b')\n"
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["f"], set(), "_up", parent_module="synmod")
assert excinfo.value.kind == "wildcard_import"
def test_handle_name_collision_fails_closed():
src = "def _up():\n return 1\n\ndef f():\n return _up()\n"
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["f"], set(), "_up", parent_module="synmod")
assert excinfo.value.kind == "handle_collision"
def test_multi_target_assign_moves_whole_or_not_at_all():
with pytest.raises(tp.TransplantError) as excinfo:
_syn(["A"], set())
assert excinfo.value.kind == "extraction"
assert "also move" in excinfo.value.message
result = _syn(["A", "B"], set())
assert result.leaf_source.count("A = B = []") == 1
def test_conditional_and_missing_symbols_fail_closed():
src = "if True:\n def cond():\n return 1\n"
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["cond"], set(), "_up", parent_module="synmod")
assert excinfo.value.kind == "extraction"
def test_statements_sharing_a_line_cannot_round_trip():
src = "C = 1; D = 2\n"
with pytest.raises(tp.TransplantError) as excinfo:
tp.transplant(src, ["C", "D"], set(), "_up", parent_module="synmod")
assert excinfo.value.kind == "round_trip"
def test_preamble_must_not_bind_declared_names():
pre = SYN_PRE + "\nLIMIT = 99\n"
with pytest.raises(tp.TransplantError) as excinfo:
_syn(["cached"], {"LIMIT"}, preamble=pre)
assert excinfo.value.kind == "preamble"
def test_preamble_requires_future_annotations():
with pytest.raises(tp.TransplantError) as excinfo:
_syn(["helper"], set(), preamble='"""Leaf."""\nimport json\n')
assert excinfo.value.kind == "preamble"
assert "__future__" in excinfo.value.message
def test_proof_detects_comment_and_code_tampering():
result = _syn(["uses", "strings_and_comments"], {"PENDING", "RUNNING", "helper"})
# comment tampering: AST-equal but the token proof catches it
tampered = result.leaf_source.replace(
"# PENDING in a comment stays a comment", "# tampered comment")
report = tp.verify_transplant(SYN, tampered, ["uses", "strings_and_comments"],
{"PENDING", "RUNNING", "helper"}, "_up")
assert not report["ok"]
entry = report["symbols"]["strings_and_comments"]
assert entry["ast_equal"] and not entry["tokens_equal"]
# code tampering: both proofs catch it
tampered = result.leaf_source.replace("with_lock = _up().PENDING",
"with_lock = list(_up().PENDING)")
report = tp.verify_transplant(SYN, tampered, ["uses"], {"PENDING", "RUNNING", "helper"},
"_up")
assert not report["ok"]
assert not report["symbols"]["uses"]["ast_equal"]
def test_proof_rejects_bare_handle_use_and_undeclared_reads():
result = _syn(["uses"], {"PENDING", "RUNNING", "helper"})
bare = result.leaf_source.replace("_up().helper(x)", "_up().json.dumps(x)")
report = tp.verify_transplant(SYN, bare, ["uses"], {"PENDING", "RUNNING", "helper"}, "_up")
assert not report["ok"]
assert "undeclared" in (report["symbols"]["uses"]["detail"] or "")
def test_emitted_leaf_parses_and_carries_the_handle_def():
result = _syn(["uses"], {"PENDING", "RUNNING", "helper"})
tree = ast.parse(result.leaf_source)
handles = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "_up"]
assert len(handles) == 1
assert any(isinstance(n, (ast.Import, ast.ImportFrom)) for n in ast.walk(handles[0]))
def test_cli_emit_and_check_roundtrip(tmp_path):
upstream = tmp_path / "up.py"
upstream.write_text(SYN, encoding="utf-8")
leaf = tmp_path / "leaf.py"
argv = [sys.executable, str(TOOL_PATH),
"--upstream", str(upstream), "--symbols", "uses,helper",
"--declared", "PENDING,RUNNING", "--handle", "_up",
"--parent-module", "synmod", "--out", str(leaf)]
pre = tmp_path / "pre.py"
pre.write_text(SYN_PRE, encoding="utf-8")
emit = subprocess.run(argv + ["--preamble-file", str(pre)],
capture_output=True, text=True)
assert emit.returncode == 0, emit.stderr
check_argv = [sys.executable, str(TOOL_PATH), "--check",
"--upstream", str(upstream), "--leaf", str(leaf),
"--symbols", "uses,helper", "--declared", "PENDING,RUNNING",
"--handle", "_up"]
check = subprocess.run(check_argv, capture_output=True, text=True)
assert check.returncode == 0, check.stderr
corrupted = leaf.read_text(encoding="utf-8").replace("return x + 1", "return x - 1")
leaf.write_text(corrupted, encoding="utf-8")
check = subprocess.run(check_argv, capture_output=True, text=True)
assert check.returncode == 2
# ---------------------------------------------------------------------------
# Mutation tests (audit 2026-08-30): the proof must FAIL on what tokens miss.
# ---------------------------------------------------------------------------
_MUT_UP = "def f(a, b):\n return a + b + PARENT\n"
_MUT_FN = "def f(a, b):\n return a + b + _h().PARENT\n"
# A COMPLETE, runnable leaf (F0 review: verify the whole module, not a fragment).
_MUT_LEAF_OK = ('"""doc"""\nfrom __future__ import annotations\n'
"from ouroboros import config as _parent\n\n\n"
"def _h():\n return _parent\n\n\n" + _MUT_FN)
def test_mutation_whitespace_change_fails_byte_proof():
"""Inter-token whitespace edits are invisible to the token proof; the
mandatory byte round trip must catch them."""
from scripts.v7next_transplant import verify_transplant
fn_ws = "def f(a, b):\n return a + b + _h().PARENT\n" # collapsed spaces
leaf_ws = _MUT_LEAF_OK.replace(_MUT_FN, fn_ws)
rep = verify_transplant(_MUT_UP, leaf_ws, ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert "byte-identical" in (rep["symbols"]["f"]["detail"] or "")
rep_ok = verify_transplant(_MUT_UP, _MUT_LEAF_OK, ["f"], {"PARENT"}, "_h")
assert rep_ok["ok"] is True and rep_ok["symbols"]["f"]["byte_identical"] is True
def test_mutation_extra_top_level_def_fails():
leaf = _MUT_LEAF_OK + "\n\ndef smuggled():\n return 1\n"
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, leaf, ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("smuggled" in e for e in rep["undeclared_top_level"])
def test_mutation_import_time_side_effect_fails():
leaf = _MUT_LEAF_OK + "\nprint('boom')\n"
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, leaf, ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("Expr" in e or "line" in e for e in rep["undeclared_top_level"])
def test_complete_leaf_passes():
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _MUT_LEAF_OK, ["f"], {"PARENT"}, "_h")
assert rep["undeclared_top_level"] == [] and rep["leaf_invariants"] == []
assert rep["ok"] is True
# --- F0 phase-review CRITICAL: whole-leaf invariants a span proof cannot see ---
def test_missing_handle_def_fails():
"""A leaf that reads _h().PARENT but never defines _h is not runnable."""
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _MUT_FN, ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("defined 0 times" in e for e in rep["leaf_invariants"])
def test_handle_returning_none_fails():
from scripts.v7next_transplant import verify_transplant
bad = _MUT_LEAF_OK.replace("def _h():\n return _parent",
"def _h():\n return None")
rep = verify_transplant(_MUT_UP, bad, ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("module reference" in e for e in rep["leaf_invariants"])
def test_declared_and_preamble_bound_overlap_fails():
"""PARENT both declared (read via handle) and imported in the preamble =
ambiguous ownership."""
from scripts.v7next_transplant import verify_transplant
bad = _MUT_LEAF_OK.replace("from ouroboros import config as _parent\n",
"from ouroboros import config as _parent\nimport PARENT\n")
rep = verify_transplant(_MUT_UP, bad, ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("ambiguous ownership" in e for e in rep["leaf_invariants"])
def test_unread_declared_name_fails():
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _MUT_LEAF_OK, ["f"], {"PARENT", "UNUSED"}, "_h")
assert rep["ok"] is False
assert any("never read through" in e for e in rep["leaf_invariants"])
def test_projection_only_leaf_without_handle_passes():
"""A leaf with zero handle reads and zero declared names (pure projection,
e.g. context_runtime_facts.py) legitimately carries no handle def."""
from scripts.v7next_transplant import verify_transplant
up = "def g():\n return 1\n"
leaf = ('"""doc"""\nfrom __future__ import annotations\n\n\n'
"def g():\n return 1\n")
rep = verify_transplant(up, leaf, ["g"], set(), "_h")
assert rep["leaf_invariants"] == []
assert rep["ok"] is True
def _leaf_with_handle(handle_src: str) -> str:
return ('"""doc"""\nfrom __future__ import annotations\n'
"from ouroboros import config as _parent\n\n\n"
+ handle_src + "\n\n" + _MUT_FN)
def test_async_handle_fails():
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _leaf_with_handle(
"async def _h():\n return _parent"), ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("sync def" in e for e in rep["leaf_invariants"])
def test_posonly_param_handle_fails():
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _leaf_with_handle(
"def _h(x, /):\n return _parent"), ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("no parameters" in e for e in rep["leaf_invariants"])
def test_constant_return_handle_fails():
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _leaf_with_handle(
"def _h():\n return 42"), ["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("module reference" in e for e in rep["leaf_invariants"])
def test_nested_only_return_handle_fails():
from scripts.v7next_transplant import verify_transplant
rep = verify_transplant(_MUT_UP, _leaf_with_handle(
"def _h():\n def inner():\n return _parent\n pass"),
["f"], {"PARENT"}, "_h")
assert rep["ok"] is False
assert any("module reference" in e for e in rep["leaf_invariants"])
def test_dotted_attribute_return_handle_passes():
from scripts.v7next_transplant import verify_transplant
leaf = ('"""doc"""\nfrom __future__ import annotations\nimport ouroboros.config\n\n\n'
"def _h():\n return ouroboros.config\n\n\n" + _MUT_FN)
rep = verify_transplant(_MUT_UP, leaf, ["f"], {"PARENT"}, "_h")
assert rep["leaf_invariants"] == []
assert rep["ok"] is True
def test_tuple_target_assignment_of_requested_symbols_passes():
"""`A, B = 500, 10` with both names requested is a legitimate moved span,
not an undeclared top-level extra (D12 lane false positive)."""
from scripts.v7next_transplant import verify_transplant
up = "A, B = 500, 10\n"
leaf = ('"""doc"""\nfrom __future__ import annotations\n\n\nA, B = 500, 10\n')
rep = verify_transplant(up, leaf, ["A", "B"], set(), "_h")
assert rep["undeclared_top_level"] == []
assert rep["ok"] is True
def test_tuple_target_with_foreign_name_still_fails():
from scripts.v7next_transplant import verify_transplant
up = "A, B = 500, 10\n"
leaf = ('"""doc"""\nfrom __future__ import annotations\n\n\n'
"A, B = 500, 10\nX, Y = 1, 2\n")
rep = verify_transplant(up, leaf, ["A", "B"], set(), "_h")
assert rep["ok"] is False
assert any("X" in e for e in rep["undeclared_top_level"])
def test_nested_tuple_target_with_unrequested_names_fails_closed():
"""`A, (X, Y) = ...` with only A requested now fails CLOSED at extraction
(the recursive unfold makes extract_spans see every bound name) — the
one-level unfold used to let X/Y ride silently (wave-2 conformance)."""
import pytest
from scripts.v7next_transplant import verify_transplant, TransplantError
up = "A, (X, Y) = 1, (2, 3)\n"
leaf = ('"""doc"""\nfrom __future__ import annotations\n\n\nA, (X, Y) = 1, (2, 3)\n')
with pytest.raises(TransplantError) as exc:
verify_transplant(up, leaf, ["A"], set(), "_h")
assert "X" in str(exc.value) and "Y" in str(exc.value)
def test_attribute_target_is_complex_and_fails():
"""`A, obj.attr = ...` mutates foreign state at import time — always an
undeclared extra even when A is requested."""
from scripts.v7next_transplant import verify_transplant
leaf = ('"""doc"""\nfrom __future__ import annotations\nimport os\n\n\n'
"A, os.environ_x = 1, 2\n")
rep = verify_transplant("A = 1\n", leaf, ["A"], set(), "_h")
assert rep["ok"] is False
assert any("complex target" in e for e in rep["undeclared_top_level"])
def test_nested_tuple_all_requested_passes():
from scripts.v7next_transplant import verify_transplant
up = "A, (X, Y) = 1, (2, 3)\n"
leaf = ('"""doc"""\nfrom __future__ import annotations\n\n\nA, (X, Y) = 1, (2, 3)\n')
rep = verify_transplant(up, leaf, ["A", "X", "Y"], set(), "_h")
assert rep["undeclared_top_level"] == []
assert rep["ok"] is True