Skip to content

T020, U-1: the governed host for the governed suites that drive the gate verbs (plan 038, P4F-5) - #39

Merged
brettheap merged 1 commit into
mainfrom
r2/t020-host-registration
Oct 6, 2026
Merged

brettheap merged 1 commit into
mainfrom
r2/t020-host-registration

Conversation

@brettheap

@brettheap brettheap commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6013608299)

Arc: neutral-product-standalone-operability

T020, U-1: host registration (HR) (plan 038, feature 038-opendox-document-tool-self-maintenance)

After set met: T004 (opensoft/openxFactory#1245) landed as 260234b5 at 2026-10-06T14:31Z.

tests/conftest.py gains a governed-suite list beside HOST_PLANE_SUITES, GOVERNED_HOST_SUITES, with its own guard in tests/test_host_plane.py. For the length of each test of those eight suites it registers the governed host, and puts openDox's registry back afterwards, as the T086 stand-in fixture does:

  • Composed run (openxFactory's scripts/ on the path, R1Q23 (a)): openxFactory's own composite, opendox_host.profile(). That is the object openxFactory's process-start call registers. It carries openXdox's gate and projection columns and openxFactory's lane column, whose POST /actions/refresh two cases request. It is found the way :413-421 finds corpus_adapter_openxfactory: only a missing opendox_host itself counts as "absent".
  • Elsewhere: GovernedStandInHost, which carries openXdox's three governed columns and nothing else (cli_gate.GateSubcommands, serve_gate.GateRoutesExtension, serve_projection.ProjectionRoutesExtension). Each facet is built on first read.

Three design points, each guarded:

  1. Only the composite is registered, never register_openxfactory(). That call also fills openxFactory's process-wide seams. openxFactory's host has no call that takes those seams back, so they would outlive the test into every later module of a multi-file run. It also refuses while openXdox's registry holds the profile the root conftest.py registers. Measured: registering the composite alone gives the same node outcomes, node for node, as calling register_openxfactory() with an openXdox-registry swap. Both were tried in a prototype; neither was committed.
  2. openxFactory's reach stays out of the process. Importing openxFactory's lane column runs carved_reach.install(), which puts openxFactory's pinned openXdox/code/src at the head of sys.path. openxdox is a namespace package. Measured before this change: after install(), importlib.util.find_spec('openxdox.cli_gate').origin resolves to openxFactory's nested openXdox/code/src/openxdox/cli_gate.py, not to this checkout. So the fixture:
    • imports openXdox's three columns from this checkout first;
    • reads the composite's built facets with sys.path saved, then restores it;
    • refuses the case by name if any openxdox.* module came from anywhere else.
  3. One registration helper for both lists (a_hosts_plane). A module on both lists runs under the governed host, which is also a host's plane (plane_host_for). The four overlapping modules are test_gateway_provenance.py, test_session_confinement.py, test_session_gates.py and test_session_verbs.py.

The contract, from tasks.md

  • Realizes: 12.5 (part).
  • Falsifier: the composed run of the named nodes (R2-INV-P4F § host-reg; part-oneoffs § Group H), quoted.
  • Ruled: R2Q8. Decisions: P4F-5 ("A governed-suite list with its own guard; the composite registered only in the composed run."), ruled with plan 038 by Brett Heap, everything as recommended (#656 6013547504).
  • After: T004.
  • Files: tests/conftest.py (first in the chain T020 → T021 → T073 → T074) and tests/test_host_plane.py. Nothing else is changed.

The falsifier

Base measured.

  • openXdox-code 56e1c238 (origin/main).
  • openDox-code a9ac96f9 (main), force-reinstalled with --no-deps over openXdox's pin, as 12.5's falsifier does.
  • openxFactory 92010d3e (main), with openDox, openXdox and openXwallet initialized recursively and its scripts/ on PYTHONPATH.
  • Python 3.12.3 and pytest 8.4.2, installed by the CI command python -m pip install -e ".[test]".
  • LANG=C.UTF-8, no GIT_* or XF_* variable, and no colour forcing.

Head: c52900e9.

The command. One process per suite file, over the 69 named nodes: host-reg 62 and Group H 7, read from R2-INV-P4F's node list:

PYTHONPATH="$OPENXFACTORY/scripts" python -m pytest -q -p no:cacheprovider -rA <that suite's named nodes>

RED at main 56e1c238:

tests/test_branch_session.py: 5 failed in 6.17s
tests/test_gateway_provenance.py: 10 failed in 8.64s
tests/test_session_confinement.py: 15 failed in 17.78s
tests/test_session_gates.py: 9 failed in 12.97s
tests/test_session_lifecycle.py: 10 failed in 7.58s
tests/test_session_notebook.py: 9 failed in 6.33s
tests/test_session_verbs.py: 11 failed in 12.07s
PASSED nodes: 0
FAILED-or-ERROR nodes: 69

The causes at main: 41 nodes end in SystemExit: 2 after argparse.ArgumentError: argument command: invalid choice: 'gate'. The others read {'error': 'unknown_action', 'message': 'no such action route'}, a 404 where 200 or 403 is expected, or (Group H's create_document_offers_the_same_repository_flag_as_its_siblings) no gate subparser at all.

GREEN at head c52900e9:

tests/test_branch_session.py: 5 passed in 6.45s
tests/test_gateway_provenance.py: 10 passed in 17.59s
tests/test_session_confinement.py: 15 passed in 19.34s
tests/test_session_gates.py: 2 failed, 7 passed in 11.59s
tests/test_session_lifecycle.py: 10 passed in 6.40s
tests/test_session_notebook.py: 9 passed in 10.87s
tests/test_session_verbs.py: 2 failed, 9 passed in 13.52s
PASSED nodes: 65
FAILED-or-ERROR nodes: 4

The four that stay red have each moved past the host to a cause another task owns:

  • test_session_gates.py::test_the_cli_offers_no_bypass_flag_for_the_endings: AttributeError: module 'opendox.cli' has no attribute 'cmd_gate_abandon_session'. This is AL, T026.
  • test_session_verbs.py::test_the_cli_offers_no_inline_content_and_no_bypass_flag: ... 'cmd_gate_edit_document'. AL, T026.
  • test_session_verbs.py::test_the_save_cli_offers_no_token_and_no_bypass_flag: ... 'cmd_gate_open_pr'. AL, T026.
  • test_session_gates.py::test_explicit_release_has_cli_and_http_parity: {'error': 'gate_refused', 'message': "the gate-action record schema could not be loaded from .../contracts/schemas/gate-action-record.schema.yaml"}. This is GA, T021.

An erratum to R2-INV-P4F. Its host-reg line "59 of 62 clear" was measured with HR, GA and RP simulated together. With HR alone it is 58: test_explicit_release_has_cli_and_http_parity needs T021's GA as well. Its failure moves from invalid choice: 'gate' to the schema load above. T021's falsifier carries it.

The 16 suites, composed, each alone (12.5's loop)

suite main 56e1c238 head c52900e9
test_branch_session.py 5 failed, 127 passed 132 passed
test_doxbench_mutation_boundary.py 80 passed 80 passed
test_doxbench_share.py 2 failed, 32 passed 2 failed, 32 passed
test_gate_loop_views.py 74 passed 74 passed
test_gateway_provenance.py 10 failed, 14 passed 24 passed
test_hosted_actor.py 5 passed 5 passed
test_session_commits.py 22 passed 22 passed
test_session_confinement.py 20 failed, 33 passed 5 failed, 48 passed
test_session_gates.py 27 failed, 54 passed 20 failed, 61 passed
test_session_lifecycle.py 10 failed, 43 passed 53 passed
test_session_notebook.py 10 failed, 29 passed 1 failed, 38 passed
test_session_records.py 29 passed 29 passed
test_session_runbook.py 9 failed, 2 passed 9 failed, 2 passed
test_session_transaction.py 4 failed, 53 passed 4 failed, 53 passed
test_session_verbs.py 14 failed, 51 passed 5 failed, 60 passed
test_staging_workbench.py 59 failed, 20 passed, 4 errors 59 failed, 20 passed, 4 errors
total 170 failed, 4 errors, 668 passed 105 failed, 4 errors, 733 passed
  • The red set at main equals R2-INV-P4F's 174 rows, node for node. So no node has moved since the inventory, at these tips.
  • At head, 65 of the 174 turn green: host-reg 58 and Group H 7.
  • No node green at main is red at head.
  • The 109 still red are the buckets of T021–T026: schema 16, moved-name 16, hosted-index 3, display-js 55, one-off 15, and host-reg 4 (above).

Group T's second cause and the host half of the four cmd_gate_* nodes, by simulation

Each of these is masked by a first cause that another slice owns:

So they were proved in two scratch copies outside every clone, now deleted. Each copy had T024's three-line shim and the eight cli_mod.cmd_gate_ → cli_gate.cmd_gate_ respellings applied. The two copies differ only in tests/conftest.py: one at main, one at this head.

conftest at main:
FAILED test_session_gates.py::test_the_abandon_cli_verb_enforces_the_human_gate_itself - SystemExit: 2
FAILED test_session_gates.py::test_the_cleanup_cli_verb_enforces_the_human_gate_and_deletes_the_branch - SystemExit: 2
FAILED test_session_verbs.py::test_the_cli_verb_enforces_the_human_gate_itself - SystemExit: 2
FAILED test_session_verbs.py::test_the_save_cli_verb_enforces_the_human_gate_itself - SystemExit: 2
FAILED test_session_gates.py::test_the_cli_offers_no_bypass_flag_for_the_endings - SystemExit: 2
FAILED test_session_verbs.py::test_the_cli_offers_no_inline_content_and_no_bypass_flag - SystemExit: 2
FAILED test_session_verbs.py::test_the_save_cli_offers_no_token_and_no_bypass_flag - SystemExit: 2
FAILED test_session_transaction.py::test_a_lock_contention_through_the_CLI_is_the_same_refusal - SystemExit: 2
conftest at head:
PASSED (6 of the 7 above), and test_a_lock_contention_through_the_CLI_is_the_same_refusal PASSED
FAILED test_session_gates.py::test_the_cleanup_cli_verb_enforces_the_human_gate_and_deletes_the_branch - assert 1 == 0
   (stderr: "cleanup-abandoned-branch refused: the gate-action record schema could not be loaded ..." -- GA, T021)

Mutants of the new guards, all killed

Each mutant was applied to a copy of the head and run against tests/test_host_plane.py in a lone checkout. Every one exits 1:

M1-drop-a-listed-suite: KILLED
M2-add-an-unneeded-suite: KILLED
M3-stand-in-wins-on-both-lists: KILLED
M4-no-sys-path-restore: KILLED
M5-no-registry-restore: KILLED
M6-no-shadow-refusal: KILLED
M7-swallow-any-missing-module: KILLED
M8-never-the-composite: KILLED
M9-no-pre-import: KILLED
M10-gate-verbs-as-a-route: KILLED
M11-drop-a-12.5-marker: KILLED
M12-facets-not-read-in-window: KILLED

The suite as CI runs it, and the protected suites

  • The required job's own command (python -m pytest -q --junitxml=… --deselect "$LEFT_OUT"), in a venv built exactly as CI builds it (openDox at the pyproject.toml pin dede32b4), at this head: selected=1105 passed=1101 skipped=4 failures=0 errors=0. That is +6 on both floors (the six new cases in tests/test_host_plane.py), with skipped exactly 4. None of the new cases skips, lone or composed. validate.yml is not this task's file, so its floors are not raised here.
  • The governed set is unchanged at 16. The new code spells 12.5's three markers only in tests/conftest.py, which 12.5's pathspec tests/test_*.py does not match. So the check that holds the list does not join the set it scans.
  • No protected file is edited. T059's oracle, as 12.5 calls it, over this commit:
    $ python3 scripts/protected_suites.py --landings="c52900e9eb80324fcecceaf6705a06c057a4d243" --suites="$(cat governed.txt)"
    ok: 0 protected edit(s), each entered and holding
    

CI and Copilot at the head c52900e9

  • Checks: validate pass, SonarCloud Code Analysis pass (2 of 2).
  • Copilot (copilot-pull-request-reviewer[bot], requested through the reviewer API): review at c52900e9, state COMMENTED, "Findings: None". Its overview reads "Needs a closer look" for one stated reason only: "The draft remains dependent on open cross-repository prerequisite T004 and composed-host integration." That is this PR's After line (T004), not a finding. Review threads: 0, so 0 unresolved.

Files changed

  • tests/conftest.py: GOVERNED_HOST_SUITES, governed_suites(), plane_host_for(), GOVERNED_COLUMNS, GovernedStandInHost, openxfactory_host(), governed_host(), a_hosts_plane(), and the autouse fixture _the_governed_host_for_the_governed_suites. The T086 fixture now uses a_hosts_plane and yields to the governed host for the four overlapping modules.
  • tests/test_host_plane.py: six lone cases holding the list, the precedence, the stand-in, the composed registration and its sys.path window, the shadow refusal, and the absent-host guard.

Not done here

  • GA (T021), RP (T022), the J and L one-offs (T023, T024), openDox web (T025) and the allow-list (T026) are other slices.
  • The permanent composed workflow is T029.
  • In a run where opendox_host is absent, these eight suites cannot be imported today (doc_health, declared). So GovernedStandInHost is reached in no current run. It is exercised by the guard's lone cases, and measured composed in a prototype: host-reg 58 and Group H 5 of 7. The two refresh nodes need openxFactory's own lane route.

🤖 Generated with Claude Code

…ate verbs (plan 038)

tests/conftest.py registers THE GOVERNED HOST for the length of each test
of the eight governed suites that drive openXdox's gate verbs
(GOVERNED_HOST_SUITES): openxFactory's composite, opendox_host.profile(),
where the run is composed (R1Q23 (a)), and GovernedStandInHost, which
carries openXdox's three governed columns, elsewhere. The composite is
registered with openDox's registry alone, so no openxFactory seam outlives
the test. Its facets are read with sys.path saved and restored, because
importing openxFactory's lane column runs carved_reach.install(), which
would otherwise put openxFactory's pinned openXdox leg ahead of this
checkout's namespace package. An openxdox module from anywhere else
refuses the case by name.

tests/test_host_plane.py holds the list to its scan (12.5's governed set
that names a gate verb of cli_gate.py) and the mechanism to its word, all
in a lone checkout. Both lists share one registration helper, a_hosts_plane;
a module on both runs under the governed host.

Composed, 12.5's sixteen suites go from 174 red to 109: host-reg 58 and
one-off group H 7 turn green, and no green node turns red.

Refs opensoft/openxFactory#656 (CLAIMED 6013608299).

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 10:42
@sonarqubecloud

sonarqubecloud Bot commented Oct 6, 2026

Copy link
Copy Markdown

@sourcery-ai

sourcery-ai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces an autouse governed-host fixture for the eight gate-verb suites, using openxFactory’s composite profile in composed runs and a minimal openXdox stand-in elsewhere, while protecting registry state and import-path provenance. New host-plane tests enforce suite-list completeness, precedence, facet composition, cleanup, and cross-checkout isolation.

File-Level Changes

Change Details Files
Adds governed-suite discovery and host-selection logic for gate-verb tests.
  • Defines the eight governed suites and derives the 12.5 scan set from source markers.
  • Resolves governed versus stand-in host precedence for overlapping suite lists.
  • Adds lazy stand-in facets for openXdox gate, gate-route, and projection extensions.
tests/conftest.py
tests/test_host_plane.py
Registers the appropriate governed host while isolating composed openxFactory runs.
  • Registers only the composite profile when opendox_host is available, otherwise uses the governed stand-in.
  • Pre-imports local openXdox columns, restores sys.path, and rejects modules loaded from another openXdox checkout.
  • Restores the prior registry after each test and preserves existing stand-in behavior for token-reading suites.
tests/conftest.py
Adds focused guards for suite coverage, host precedence, composition, isolation, and failure handling.
  • Verifies the governed list matches suites that drive gate verbs and is not accidentally expanded or reduced.
  • Tests lazy facet construction, composite registration, registry and path restoration, shadow-module refusal, and strict missing-dependency handling.
tests/test_host_plane.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The draft remains dependent on open cross-repository prerequisite T004 and composed-host integration.

Review effort: Balanced
Findings: None

What changed in this PR

Adds test infrastructure ensuring governed gate-verb suites run with the correct host profile while preserving registry and import-path isolation.

Changes:

  • Registers either openxFactory’s composite host or a lazy local stand-in.
  • Adds guards covering suite membership, precedence, restoration, and module shadowing.
File Description
tests/​conftest.py Adds governed-host selection, registration, and isolation fixtures.
tests/​test_host_plane.py Tests governed-host behavior and invariants.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

brettheap added a commit that referenced this pull request Oct 6, 2026
…'s cases (plan 038)

Brett Heap's ruling at opensoft/openxFactory#656 comment 6016648451
("Widen the spans, served display (Recommended)") widens R-1 (a)'s named
spans to six: _CREATE_HARNESS, _SESSION_HARNESS and _HOSTILE_HARNESS, and
the helpers _run_create, _run_session and _hostile_descriptors. The helpers
copy the model's siblings and hand each harness the SERVED governed display.

* scripts/protected_suites.py: a span may now be one module-level helper
  function as well as one module-level constant. A function's span starts at
  its first decorator, as a test's does. A class, an import, a test or a name
  bound twice is still never a span, and no span is added.
* tests/test_staging_workbench.py, ten entries chained after the seven:
  - the three helpers copy display.js and the siblings the harness imports
    (swb-create.js or swb-session.js, with the helpers.js and dispose.js
    they import), with a package.json that makes node read them as modules,
    and hand the harness /capabilities["display"], by the statement
    serve.build_server() makes under the registered host;
  - the three harnesses install it (setDisplay(readDisplay(...))) before
    anything derives, as the shell does, and import CREATE_ROUTE and
    sessionRoute from the columns RULED Q3 moved them to;
  - three in-test respellings of the CF-4 kind, which T026's measurement
    found outside the seventeen (comment 6016415390 point 2): the gate
    verbs read from openxdox.cli_gate in two tests, and the sync script
    read at its one home on the import path.
* tests/test_protected_suite_check.py: the span kind's cases (T026's Files
  gain it, the holder's answer on the same ruling).
* tests/protected_suite_respellings.yaml: the header names the six spans,
  the helper-function form, and batch Q's --chains for 12.5's call.

T020's registration of tests/test_staging_workbench.py (its
tests/conftest.py and the scan in tests/test_host_plane.py) is not in this
commit: it is made after #39 lands.

Refs opensoft/openxFactory#656 (CLAIMED 6015602254; questions 6016415390;
ruling 6016648451).

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap changed the title DRAFT: T020, U-1: the governed host for the governed suites that drive the gate verbs (plan 038, P4F-5) T020, U-1: the governed host for the governed suites that drive the gate verbs (plan 038, P4F-5) Oct 6, 2026
@brettheap
brettheap marked this pull request as ready for review October 6, 2026 16:07
@sourcery-ai

sourcery-ai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 day and 2 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap

Copy link
Copy Markdown
Contributor Author

READY at c52900e

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Holder, at READY (T020, U-1, plan 038; authored by lane openXfactory-3).

@brettheap
brettheap merged commit c6d15b2 into main Oct 6, 2026
4 checks passed
@brettheap

Copy link
Copy Markdown
Contributor Author

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

LANDED — lane openxfactory-4, 2026-10-06T16:10:55Z, PR #39 → c6d15b2 (opensoft/openXdox-code main; plain gate)

Brett: plan 038 RULED incl. lane openxfactory-4 lands every PR (#656 6013547504); Start release 2 (#656 6001702967)

brettheap added a commit that referenced this pull request Oct 6, 2026
T020 landed by squash with the tree of c52900e, which this branch already
carries. tests/conftest.py conflicted only where T020's identical lines sit
beside T021's and T073's, so this branch's file is kept. Its T020 lines
equal main's byte for byte.

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
…its scan reads a gate command line (plan 038)

Brett Heap's ruling at opensoft/openxFactory#656 comment 6016648451 has
T026 make this edit to T020's two files once #39
landed (c6d15b2):

* tests/conftest.py: test_staging_workbench.py joins GOVERNED_HOST_SUITES.
  It parses its gate-off CLI descriptors with the real parser, and its node
  harnesses run in the display /capabilities serves, which openDox reads
  off the registered host. With no host, every harness helper refuses
  ProfileNotRegistered and the descriptors read invalid choice: 'gate'.
* tests/test_host_plane.py: the scan that holds the list to the governed
  suites also reads a gate verb spelled as a command line (a string that
  opens with gate and the verb), as this suite's descriptors spell it.
  Over the 16 governed suites only test_staging_workbench.py matches it
  and not the argument-vector spelling.

Refs opensoft/openxFactory#656 (CLAIMED 6015602254; ruling 6016648451).

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
T020's squash c6d15b2 has the same tree as its head c52900e, which this
branch already carries. The one conflict, at the end of tests/conftest.py,
is T021's own append after T020's last hunk; main's side of it is empty. It
is resolved to this branch's side, so the merged tree equals ecf0ba9's.

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
…opendox.session_git (plan 038, CF-3) (#38)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6013609696)

Arc: neutral-product-standalone-operability

## T024, U-5: the H-1 shim (plan 038, feature `038-opendox-document-tool-self-maintenance`)

After set met: T004 (opensoft/openxFactory#1245) landed as 260234b5 at 2026-10-06T14:31Z.

Base brought up to date with main (T020 #39 c6d15b2) by a merge commit; CI green at 65c3910; the composed falsifier was measured at 873792a, before the merge, and was not re-run (the merge brought only T020's host registration).

A new `scripts/ideation_dashboard/session_git.py`, with NO `__init__.py` beside it:

```python
import sys

from opendox import session_git as _m

sys.modules[__name__] = _m
```

(the task's three statements; the file carries a comment block above them saying why).

### Contract, from `tasks.md`

- **Realizes**: 12.5 (part).
- **Falsifier**: the 4 nodes, composed, quoted; `test_dependency_direction.py` green (it does not scan `scripts/`).
- **Ruled**: R2Q8. **Decisions**: CF-3 (H-1), confirmed by Brett Heap's ruling of plan 038, everything as recommended (#656 `6013547504`).
- **After**: T004.
- **Files**: openXdox-code new `scripts/ideation_dashboard/session_git.py`. One file, nothing else.

### Why

The carve moved `ideation_dashboard/session_git.py` verbatim to openDox-code as `opendox.session_git`. `tests/test_session_transaction.py:296` still spells the old path, in the module-level string `LOCK_HOLDER` that a rival process runs (`from ideation_dashboard import session_git as sg`). A protected suite's module-level text lies inside no test, so no batch C entry can hold an edit to it; the shim restores the spelling instead, and edits no protected file. The trace is lane 3's R2-INV-P4F part, Group L.

With no `__init__.py`, `scripts/ideation_dashboard/` stays a portion of the `ideation_dashboard` namespace package, so in the composed run it merges with openxFactory's own `scripts/ideation_dashboard/` (which has none either) and does not shadow it. That follows from Python's namespace-package rule (ADV-39). `sys.modules[__name__] = _m` makes `ideation_dashboard.session_git` and `opendox.session_git` the same module object.

### The falsifier

**Base measured**: openXdox-code `56e1c238` (origin/main), openDox-code `a9ac96f9` (force-reinstalled with `--no-deps` over openXdox's pin, as 12.5's falsifier does), openxFactory `92010d3e` with `openDox`, `openXdox` and `openXwallet` initialized recursively and its `scripts/` on `PYTHONPATH` (R1Q23 (a)'s form). Python 3.12.3, pytest 8.4.2, the clone's CI install command (`python -m pip install -e ".[test]"`). **Head**: `873792a9`.

The four nodes, all in `tests/test_session_transaction.py`:

```
test_a_separate_process_holding_the_lock_refuses_the_in_process_action
test_a_lock_contention_through_the_ROUTE_is_a_409_carrying_the_engines_reason
test_step_lock_a_rival_process_holding_it_refuses_and_unwinds_the_open
test_a_lock_contention_through_the_CLI_is_the_same_refusal
```

RED at main (`56e1c238`), composed:

```
FAILED tests/test_session_transaction.py::test_a_separate_process_holding_the_lock_refuses_the_in_process_action
FAILED tests/test_session_transaction.py::test_a_lock_contention_through_the_ROUTE_is_a_409_carrying_the_engines_reason
FAILED tests/test_session_transaction.py::test_step_lock_a_rival_process_holding_it_refuses_and_unwinds_the_open
FAILED tests/test_session_transaction.py::test_a_lock_contention_through_the_CLI_is_the_same_refusal
4 failed in 2.90s
```

Their cause is `assert '' == 'held'` (the rival child dies on `ImportError: cannot import name 'session_git' from 'ideation_dashboard'`; the first node's `finally` then raises `BrokenPipeError`).

GREEN at head (`873792a9`), composed:

```
FAILED tests/test_session_transaction.py::test_a_lock_contention_through_the_CLI_is_the_same_refusal
1 failed, 3 passed in 3.42s
```

**Three of the four close.** The fourth, `test_a_lock_contention_through_the_CLI_is_the_same_refusal`, passes its first cause (`LOCK_HOLDER`) and stops at its second, at `tests/test_session_transaction.py:1148`:

```
opendox: error: argument command: invalid choice: 'gate' (choose from 'generate', 'generate-and-open', 'create', 'edit', 'model-binding', 'runtime')
```

That is the missing `gate` registration: `tests/test_session_transaction.py` is not in `HOST_PLANE_SUITES`, so no host carrying openXdox's `cli_gate.GateSubcommands` is registered for it. `tests/conftest.py` is T020's file and is NOT touched here. **The fourth node closes when T020 lands.** To show that, a throwaway pytest plugin kept outside the clone registered a host carrying `GateSubcommands` for that one suite only (the pattern at `tests/test_hermeticity_gate_verbs.py:48-52`), and the same four nodes at the same head read:

```
4 passed in 3.01s
```

**The composed whole, 66 files.** Each of the 66 files in `tests/declared_exclusion.yaml`, run alone and composed, at main and at head: 2571 cases at both, 720 red at main and 717 at head. The set that moved is exactly the three nodes above (red at main, green at head); no node is newly red.

**`test_dependency_direction.py`**: 17 passed in the CI form (the lone run). Composed it reads 16 passed, 1 failed, and the one is `test_first_attribute_access_refuses_and_names_the_layering`. It fails the same way at main (`56e1c238`), composed, so the shim does not cause it: that node asserts a refusal when `ideation_dashboard` cannot be imported, and composed the module is importable. It is a property of the composed form, noted here for the T029 composed workflow.

### Mutants

- **Drop the `sys.modules[__name__] = _m` line**: all four nodes go red again (`4 failed`, with the throwaway registration in place, so the registration is not what is being tested). The shim body then imports but exposes no `SessionGit`, and the rival child dies.
- **Add an `__init__.py`** beside the shim (a regular package): `ideation_dashboard` resolves to one portion, and `import ideation_dashboard.serve_openxfactory_lanes` raises `ModuleNotFoundError`. As shipped, it has two portions and the openxFactory lane module resolves. That is the shadowing the missing `__init__.py` prevents.

### The protected-suite oracle

`scripts/protected_suites.py`, called with the head commit as the landing and every `tests/test_*.py` as the suites, exits 0: `ok: 0 protected edit(s), each entered and holding`. The branch touches no path under `tests/`, and no `__init__.py` is added anywhere under `scripts/`.

### The full suite, as CI runs it

`python -m pytest -q --junitxml=pytest-report.xml --deselect "$LEFT_OUT"` after `python -m pip install -e ".[test]"`: `1095 passed, 4 skipped, 1 deselected in 83.41s`. The "Pin the triple" step's own arithmetic over that report: `selected=1099 passed=1095 skipped=4 failures=0 errors=0`, against the pins `selected>=1099 passed>=1095 skipped==4`. The shim adds no test, so the triple does not move.

### CI, Copilot, threads (at head `873792a9`)

- **CI**: 2 of 2 checks green. `validate` pass (1m30s; the full suite, with the "Pin the triple" step); `SonarCloud Code Analysis` pass (Quality Gate passed, 0 new issues, 0 security hotspots).
- **Copilot**: reviewed at this exact head. State COMMENTED, `Findings: None`. Its summary line reads "Needs a closer look: the draft explicitly depends on pending cross-repository sequencing and composed validation", which is this PR's own draft note and its After set (T004), not a finding.
- **Threads**: 0 review threads, 0 unresolved.
- **At the new head `65c3910f`** (the merge of `main` `c6d15b27`, brought in by GitHub's update-branch as a merge commit): `validate`, `SonarCloud Code Analysis`, `Sourcery review` and `copilot-pull-request-reviewer` all completed success (4 of 4); Copilot review 5431537774, state COMMENTED, "Approval recommended", `Findings: None`; 0 review threads, 0 unresolved. Against `873792a9` the merge changed only `tests/conftest.py` and `tests/test_host_plane.py`, T020's two files.

### What is NOT done

- `tests/conftest.py` and the `gate` registration: T020's. The fourth node closes there.
- No protected suite is edited, and no allow-list entry is added (U-7, T026, is the allow-list's).
- The shim reintroduces the retired `ideation_dashboard.*` spelling in openXdox-code's `scripts/`. `tests/test_dependency_direction.py` does not scan `scripts/`, so nothing refuses it; this is the reintroduction CF-3 confirms. Hosting the shim in openxFactory instead would contradict the carve manifest's `moved_verbatim` row.


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
…tes again (plan 038) (#42)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6013609326)

Arc: neutral-product-standalone-operability

## T023, U-4: `swb-session.js` spells the model's two names at its call sites again (plan 038, feature `038-opendox-document-tool-self-maintenance`)

After set met: T004 (opensoft/openxFactory#1245) landed as 260234b5 at 2026-10-06T14:31Z.

Base brought up to date with main (T020 #39 c6d15b2, then T024 #38 8b64fae) by two merge commits; CI green at e8a067f; the composed falsifier was measured at c3c6a47, before the merges, and was not re-run (the merges brought only T020's host registration and T024's shim; INFERRED, not measured: the composed falsifier reads the same at the new head).

### Contract, from `tasks.md`

- **Realizes**: 12.5 (part).
- **Falsifier**: the 2 nodes, composed, quoted.
- **Ruled**: R2Q8. (The entry names no Decisions line.)
- **After**: T004.
- **Files**: openXdox-code `src/openxdox/web/views/swb-session.js`, plus ONE new test file, `tests/test_swb_session_transport.py`. The test file is not in the task's `tasks.md` entry: lane openxfactory-4 added it to T023's Files by its ruling on Copilot's first finding on this PR (`lane4-to-lane3.log`, 12:52:51Z, "FIX-NOW, as in T022 (a)"; recorded on #656 in lane 4's latest comment). No protected suite is edited, and the file is not one of the 16 `HOST_PLANE_SUITES`.

### What and why

`submitSession` took its two model calls from locals, `const headersOf = bound ? bound.consoleHeaders : consoleHeaders;` and `const repairWith = ...`, called as `headersOf(caps)` and `repairWith(send, repair)`. That is the `bound`-namespace refinement of Copilot review round 3, which arrived with the S5 carve in #18. `tests/test_session_confinement.py` pins both write transports (`swb-session.js` and `swb-create.js`) to the literal spellings `headers: consoleHeaders(caps)` and `withConsoleRepair(send, repair)`, so two nodes went red in the composed run. The pre-shed openxFactory copy spelled both exactly; `swb-create.js` still does. The trace is lane 3's R2-INV-P4F part, Group J.

The fix shadows the two module-level names inside `submitSession` instead of respelling the call sites:

```js
const MODULE_TRANSPORT = {
  consoleHeaders: (...args) => consoleHeaders(...args),
  withConsoleRepair: (...args) => withConsoleRepair(...args),
};
// inside submitSession:
const { consoleHeaders, withConsoleRepair } = bound || MODULE_TRANSPORT;
//   headers: consoleHeaders(caps),   ...   return withConsoleRepair(send, repair);
```

With a `bound` namespace the calls go to it, as before. With none, `MODULE_TRANSPORT` reaches the CURRENT install at call time, exactly as the two module-level consts do. The comment above it does not quote the pinned needles, because a needle quoted in its own source satisfies itself; each pinned needle occurs once in the file, in code (counted).

### The new test file, `tests/test_swb_session_transport.py`

The pinned spellings are text assertions, so they cannot tell the right model from the wrong one. Two node probes run the shipped module out of the composed bundle and assert on the request that reaches the injected fetcher:

- **bound**: two `firstEditTransport`s over two models that differ in exactly `consoleHeaders` and `withConsoleRepair`, both built before either is called; each request must carry its own model's header (from its own `caps`) and go through its own model's repair policy.
- **unbound**: a mounted session form, submit pressed; the request must carry the header the mounted model's `consoleHeaders` built and go through its `withConsoleRepair`. (The outcome panel the column writes after the request needs the shell's `page-overlay` host, which a stub DOM lacks, so the handler rejects after the fetcher is called; the probe records that and asserts on the request only.)

Both pass at main's own code (checked in the base clone), so they describe the behaviour the respelling preserves. They run in the lone CI form too: no openxFactory module, node only, as `test_gate_loop_probes.py` does.

### The falsifier

**Base measured**: openXdox-code `56e1c238` (origin/main), openDox-code `a9ac96f9` (plain install, `--no-deps`, over openXdox's pin, as R2-INV-P4F did), openxFactory `92010d3e` with `openDox`, `openXdox` and `openXwallet` initialized recursively and its `scripts/` on `PYTHONPATH` (R1Q23 (a)'s form). Python 3.12.3, pytest 8.4.2, the CI install command (`python -m pip install -e ".[test]"`). **Head**: `c3c6a47` (the fix is `f19e05c`; the test file is `c3c6a47`). Run with `FORCE_COLOR`, `CLICOLOR` and `COLORTERM` unset and `TERM=dumb`.

RED at main (`56e1c238`), composed:

```
FAILED tests/test_session_confinement.py::test_the_renderer_presents_the_console_token_from_the_capability_probe
FAILED tests/test_session_confinement.py::test_the_renderer_repairs_a_stale_token_by_re_reading_it_never_by_reloading
2 failed in 0.45s
```

(each: `AssertionError: swb-session.js`, from `'headers: consoleHeaders(caps)' in body` at `:290` and `'withConsoleRepair(send, repair)' in body` at `:383`)

GREEN at head `c3c6a47`, same environment:

```
PASSED tests/test_session_confinement.py::test_the_renderer_presents_the_console_token_from_the_capability_probe
PASSED tests/test_session_confinement.py::test_the_renderer_repairs_a_stale_token_by_re_reading_it_never_by_reloading
2 passed in 0.67s
```

### What else reads `swb-session.js`, and did not move

Every test file in the tree (`tests/test_*.py` and `tests/integration`, named on the command line so the declared exclusion does not drop them), composed as above, run SERIALLY on one idle container with `--continue-on-collection-errors` (`test_gate_console.py` fails to collect in both, for want of `scripts/proposal-support.py`):

| | nodes | failed | errors | passed | skipped |
|---|---|---|---|---|---|
| main `56e1c238` | 3671 | 550 | 94 | 3021 | 6 |
| head `c3c6a47` | 3673 | 548 | 94 | 3025 | 6 |

Compared node by node: the 3671 ids at main all appear at the head, and EXACTLY TWO of them change status, both red to green, the two above. The two extra nodes are the new file's, both passing. Nothing goes green to red.

The suites that read `swb-session.js`, red nodes at main and at the head: `test_gate_loop_probes.py` 0 / 0, `test_renderer.py` 25 / 25, `test_doxbench_save.py` 3 / 3, `test_session_confinement.py` 20 / 18 (the two above), `test_staging_workbench.py` 63 / 63, `test_doxchat_model_intake.py` 10 / 10, `test_doxbench_transport.py` 6 / 6, `test_assembled_bundle.py` 4 / 4, `test_doxbench_scope.py` 1 / 1, `test_gate_loop_views.py` 0 / 0. The three suites the trace names read 28 red on both sides, the same node ids, as the trace's trial found. Earlier pairs, run overlapping and while the container was out of process slots, showed `Resource temporarily unavailable` and `can't start new thread` errors in git-fixture and thread-spawning suites; they were discarded and re-run serially after the container was restarted, which is the pair above.

### Mutants of the fix

Spelling mutants, killed by the pinned nodes (composed, over `test_session_confinement.py`, `test_gate_loop_probes.py`, `test_doxbench_save.py` and `test_renderer.py`, compared with the head's own red set):

| Mutant | Result |
|---|---|
| M1: respell the `headers:` call as `(bound ? bound.consoleHeaders : MODULE_TRANSPORT.consoleHeaders)(caps)` (behaviour kept) | KILLED: both target nodes go red (the stale-token node pins the same headers spelling) |
| M2: respell the return as `(bound ? bound.withConsoleRepair : MODULE_TRANSPORT.withConsoleRepair)(send, repair)` | KILLED: the repairs-a-stale-token node goes red |

Behaviour mutants, which keep every pinned spelling and which only the new file can kill (run composed AND in the lone CI venv, same result in both):

| Mutant | Killed by |
|---|---|
| M3: drop `bound ||`, so the module pair is always used | `test_js_each_built_transport_sends_through_its_own_models_header_and_repair` (1 failed, 1 passed) |
| M4: `MODULE_TRANSPORT.consoleHeaders` calls `withConsoleRepair` | `test_js_a_mounted_form_sends_through_the_mounted_models_header_and_repair` (1 failed, 1 passed) |
| M6: `MODULE_TRANSPORT.withConsoleRepair` calls `consoleHeaders` | `test_js_a_mounted_form_sends_through_the_mounted_models_header_and_repair` (1 failed, 1 passed) |

Before the new file, M3 and M4 survived every committed suite (the finding on the first head).

### Other gates

- **CI form, lone** (`python -m pytest -q --deselect "$LEFT_OUT"` from the root, the CI venv, no `PYTHONPATH`) at `c3c6a47`: `1097 passed, 4 skipped, 1 deselected`; the workflow's triple reads `selected=1101 passed=1097 skipped=4 failures=0 errors=0`, which meets its pins (floors 1099 / 1095, skipped exactly 4, 0 / 0). At `f19e05c` and at main it read 1099 / 1095.
- **Protected suites:** `python3 scripts/protected_suites.py --landings=<both commits> --suites=<the 98 tests/test_*.py at origin/main>` prints `ok: 0 protected edit(s), each entered and holding`, with and without `--chains`. The new file is not in `HOST_PLANE_SUITES`.
- **Already red at main, composed, not from this change:** `tests/test_dependency_direction.py::test_first_attribute_access_refuses_and_names_the_layering` (the T024 writer attributes it to `ideation_dashboard` being importable when composed; I did not trace it); red in both the base and head runs.
- **CI at the head:** `validate` pass and `SonarCloud Code Analysis` pass (2 of 2) at `c3c6a47`.
- **Copilot:** two reviews, one per head. At `f19e05c8` (review 5428398958): 1 finding, that no committed test distinguishes the bound and unbound transports. Fixed by `c3c6a47`; thread answered and resolved. At `c3c6a470` (review 5428827782, COMMENTED, body "Changes recommended"): 2 findings, both on the new file.
  - "the declared one-file scope": this body said one file and "no test is added"; the body is corrected (Files above), the ruling that widened Files is cited, and the thread is answered and resolved.
  - "update the workflow coverage floors": see below. An accepted limit, answered and resolved.
  - 0 unresolved threads at the head. The holder rules on the "Changes recommended" label.
  - Holder ruling #656 `6017860539`: both Copilot findings, the "Changes recommended" label included, are ACCEPTED as they stand; the CI floors are left to T029.
- **At the new head `e8a067f3`** (two update-branch merges of `main`, both merge commits: `3b6cd13b` took T020 #39 `c6d15b27`, then `e8a067f3` took T024 #38 `8b64fae0`): every check-run completed success; Copilot review 5432366190, state COMMENTED, "Approval recommended", `Findings: None` (requested through the reviewer API after about 10 minutes without an auto-review); 3 review threads in all, all resolved, 0 unresolved, none new. Against `c3c6a470` the merges changed only `tests/conftest.py`, `tests/test_host_plane.py` and `scripts/ideation_dashboard/session_git.py`, the files of T020 and T024.
- **SonarCloud:** the `SonarCloud Code Analysis` check passes (quality gate); I did not list individual issues.

### What is NOT done

- **The CI floors are not raised** (`MIN_SELECTED` / `MIN_PASSED` in `.github/workflows/validate.yml`, 1099 / 1095; this PR's head selects 1101 and passes 1097). `validate.yml` is not in T023's Files, plan 038 gives its floors in this repository to no task (T017 and T051 re-pin openDox-code's), and the open sibling PRs that add tests (#39, #40, #41) would collide with this one on the same two lines. Floors are minimums, so the check passes and the margin is two wider than it was. The workflow's own comment says the commit that adds a test SHOULD raise them with the reason; whether and in which PR is for the holder to sequence.
- The other groups of R2-INV-P4F's one-off bucket (H, L, T, W, S) are other tasks.


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
…s it, nothing vendored (plan 038, P4F-4) (#40)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6013608628)

Arc: neutral-product-standalone-operability

## T021, U-2: the gate console's schemas (GA), resolved without vendoring (plan 038, feature `038-opendox-document-tool-self-maintenance`)

After set met: T004 #1245 → 260234b5; T020 #39 → c6d15b2.

**The branch.**
- T021's own commits:
  - `07361582`, U-2;
  - three Copilot fixes: `ecf0ba9b`, `542a5c40` and `a183dc46`.
- Main came in by merges, never a rebase:
  - `72633f93` (T020, #39 → `c6d15b27`);
  - `22a77c16` (T024, #38 → `8b64fae0`);
  - `122bacce` (T023, #42 → `d45a0939`), the head.
- The diff against `main` is T021's three files.
- The first merge had one conflict, at the end of `tests/conftest.py`: T021's own append after T020's last hunk, with main's side empty. It was resolved to this branch's side, so the merged tree equalled `ecf0ba9b`'s byte for byte.

### What changes

Before this change, `gate_console._validate_contract_document` read both of its schemas from `Path(__file__).resolve().parents[2] / "contracts" / "schemas"`. That was openxFactory's own tree before the carve; in this leg it is the checkout root, which holds no `contracts/`. Now each schema is read where its owner keeps it:

- **`gate-action-record.schema.yaml` (openXdox-spec's)** is read from this distribution's own packaged copy, through `openxdox.contracts.verified_bytes("gate-action-record")`. That call checks the copy against `copies.yaml`'s digest before a byte is parsed. The copy record already holds this schema; nothing is added to it.
- **`demotion-execution-receipt.schema.yaml` (openxFactory's)** is read from a source the HOST registers with the new seam, `gate_console.register_contract_schema_source(directory)`. It follows openDox's seam idiom:
  - registering the same directory again is a no-op;
  - a different one raises `ContractSchemaSourceRefused`;
  - a directory that lacks the schema is refused when it is registered.

  `unregister_contract_schema_source()` and `contract_schema_source_registered()` complete it, so T030's host wiring can fill and take back the seam the way `opendox_host` treats openDox's. **With nothing registered, validating a receipt refuses by name** (R1Q27 (a)):

  ```
  GateRefused: the demotion execution receipt cannot be validated: its schema, demotion-execution-receipt.schema.yaml, is the host's contract, and no contract schema source is registered. A host registers the directory it keeps its contract schemas in, once, at process start, with openxdox.gate_console.register_contract_schema_source(<directory>) (R1Q27 (a): a kind is validated only where the tree it runs from supplies its schema).
  ```

- **A host's schema is checked before it is used** (`_schema_errors`, from the Copilot rounds below). It is parsed, checked against the validator's own meta-schema, then applied. ANY failure at any of the three steps refuses as `GateRefused`, naming the step and the exception's class; a `SchemaError` keeps its own "is not a valid JSON Schema" message. This matters because the demotion verb writes its receipt after it has moved files, and it reports only a `GateRefused` there as "DEMOTION EXECUTED BUT RECEIPT NOT WRITTEN". Any other exception ended the verb with a traceback.
- **`tests/conftest.py`**, one registration placed after U-1's: where the run is composed, it registers openxFactory's `contracts/schemas/` from the composed tree at process start, as openxFactory's host wiring will from T030. A lone checkout registers none.

**Line numbers kept.** `tests/test_no_hardcoded_status_words.py`'s `PROSE_EXEMPT` pins seven prose f-strings in `gate_console.py` by line number (315 to 1851). So:
- the seam and the schema helpers sit under their own header near the end of the module, before the PEP 562 section;
- `openxdox.contracts` is imported inside `_packaged_schema_text`;
- `_validate_contract_document` and its two call sites keep their exact line counts.

No line before 1851 moves. That test file is not this task's, so it is unchanged.

### The contract, from `tasks.md`

- **Realizes**: 12.5 (part).
- **Falsifier**: the 16 schema nodes (R2-INV-P4F § schema), composed, quoted; a lone-checkout test that the receipt refuses by name; `test_packaged_validator.py` unchanged and green.
- **Ruled**: R2Q8. **Decisions**: P4F-4 ("`gate-action-record` is read from openXdox-code's own package; `demotion-execution-receipt` from a schema source the HOST registers (the composed conftest, then host wiring at T030); a lone checkout refuses by name. Nothing vendored."), ruled with plan 038 by Brett Heap, everything as recommended (#656 `6013547504`).
- **After**: T020 (the conftest), met: #39 → `c6d15b27`. T004 #1245 → `260234b5`.
- **Files**: `src/openxdox/gate_console.py` (first: T021 → T074), `tests/conftest.py` (second: T020 → T021 → T073 → T074), and a new `tests/test_gate_console_schema_source.py`.
  - **Note for the holder:** plan 038's single-writer table (`plan.md` § "Parallel slices") has no row for the new `tests/test_gate_console_schema_source.py`. Lane 3 flagged this to lane 4 as a RECHECK FIX (lane3-to-lane4 log, 2026-10-06T00:30:30Z). The file is created here because T021's Files line names it, and no other task writes it.

### The falsifier, at the head `122bacce`

**Environment.**
- openXdox-code: `main` `d45a0939`, against the head `122bacce`.
- openDox-code `34c20641`, force-reinstalled with `--no-deps`, as 12.5's falsifier does.
- openxFactory `51456835`, with `openDox` (`e1e3a3c3`, spec `f7ee3c76`), `openXdox` and `openXwallet` initialized recursively, and its `scripts/` on `PYTHONPATH` (R1Q23 (a)).
- Python 3.12.3, pytest 8.4.2, CI's install command, `LANG=C.UTF-8`, no colour forcing.
- Two mains moved during the measurement, and neither was taken into it:
  - openxFactory moved to `e6380965`: one commit, with an empty diff over `scripts/`, the submodule gitlinks and `contracts/`;
  - openDox-code moved to `84f8ed83`: #91, T025, the staging model, `staging-workbench.js` and their tests, none of them T021's.

**1. The 16 schema nodes, composed**, with `test_explicit_release_has_cli_and_http_parity`. That node needs this GA as well as T020's HR (#39's erratum to R2-INV-P4F). All are in `tests/test_session_gates.py`:

```
PYTHONPATH="$OPENXFACTORY/scripts" python -m pytest -q -p no:cacheprovider -rA <the 17 nodes>
```

| at | result |
|---|---|
| main `56e1c238` (first round) | `17 failed` |
| T020 `c52900e9` (first round) | `17 failed` |
| T021 `07361582`, `ecf0ba9b` | `17 passed` |
| main `c6d15b27` / head `72633f93` | `17 failed` / `17 passed` |
| main `8b64fae0` / head `542a5c40` | `17 failed` / `17 passed` |
| **main `d45a0939` / head `122bacce`** | **`17 failed` / `17 passed`** |

The causes at main:
- `{'error': 'gate_refused', 'message': "the gate-action record schema could not be loaded from <checkout>/contracts/schemas/gate-action-record.schema.yaml'"}`;
- `FileNotFoundError: … '<checkout>/contracts/schemas/gate-action-record.schema.yaml'`;
- the knock-on `assert 409 == 200`.

The 16 nodes:
- `test_the_abandon_reason_survives_the_fr028_branch_cleanup`
- `test_cleanup_deletes_the_branch_once_the_proposal_exists`
- `test_matching_proposal_evidence_from_before_abandonment_requires_fresh_release`
- `test_a_later_edit_elsewhere_in_the_change_folder_does_not_freshen_the_origin`
- `test_a_second_matching_record_recorded_after_abandonment_releases_cleanup`
- `test_branch_advanced_after_abandonment_requires_explicit_current_head_release`
- `test_active_declared_origin_releases_cleanup_without_a_pick_edge`
- `test_archived_exact_origin_releases_cleanup_after_the_tile_disappears`
- `test_execution_receipt_releases_cleanup_for_the_exact_destination`
- `test_legacy_manifest_requires_an_exact_returned_artifact`
- `test_explicit_human_release_cleans_a_true_orphan_and_records_why`
- `test_non_staged_orphans_use_the_explicit_human_release_lane[cluster-cl-orphan]`
- `test_non_staged_orphans_use_the_explicit_human_release_lane[possible-pos-orphan]`
- `test_cleanup_unwinds_its_record_when_branch_deletion_fails`
- `test_cleanup_restores_the_exact_ref_when_record_finalization_fails`
- `test_the_durable_ending_marker_is_the_second_admissible_proof`

**2. The lone-checkout test**, the new `tests/test_gate_console_schema_source.py`, 6 cases.

The console imports openxFactory's `doc_health` when it loads (R1Q6 (d)), though nothing the cases run calls it. So each case runs the console in a child interpreter, beside a stand-in `doc_health` that answers the three names the import reads: `corpus.STATUS_SCAN_LINES`, `lines.split_keepends` and `lines.join_rows`. The CI process never imports the console through it. The cases:
- with no source registered, a receipt refuses, naming `register_contract_schema_source(<directory>)`;
- the record is validated against the packaged copy:
  - a record the schema does not admit refuses;
  - a registered source whose own `gate-action-record.schema.yaml` admits nothing is never read for a record;
  - a packaged copy that drifted from its digest refuses;
- a receipt is validated against the registered source's schema;
- the seam holds one source, as above;
- a host schema the console cannot use refuses as `GateRefused`. That is any of: empty; `required: 1`; an unresolvable `$ref`; an unclosed flow sequence; a 5000-deep nesting; an impossible date; a cyclic mapping alias; a cyclic list alias;
- the module reads no `parents[...]` index (by AST).

Red at the base `c52900e9`: `5 failed in 1.38s`. The causes:
- `AttributeError: module 'openxdox.gate_console' has no attribute 'register_contract_schema_source'`;
- `GateRefused: the gate-action record schema could not be loaded from <scratch>/contracts/schemas/…`;
- `gate_console.py reads a parents index at lines [642]`.

Green at every T021 head; `6 passed` at `122bacce`.

**3. `tests/test_packaged_validator.py` is unchanged and green.** `git diff main -- tests/test_packaged_validator.py src/openxdox/contracts/` is empty.

### The 16 suites, composed, each alone (12.5's loop), at `122bacce` against `main` `d45a0939`

| | passed | failed | errors | red |
|---|---|---|---|---|
| main `d45a0939` | 739 | 99 | 4 | 103 |
| head `122bacce` | 756 | 82 | 4 | 86 |

- **No node green at main is red at the head: 0 regressions.**
- The 17 nodes the head clears are exactly the 17 falsifier nodes above.
- No node is red outside R2-INV-P4F's 174, at main or at the head.
- No log in either run shows `Errno 11` or `cannot fork`.
- Since the first round (main 174, T020 109, T021 92), the landings of T024 (#38) and T023 (#42) cleared 4 and 2 nodes. They cleared the same nodes at main and at the head.

### Mutants of the new guards, all killed

**Round 1, at `ecf0ba9b`.** Each mutant was applied to a copy of the head and run against `tests/test_gate_console_schema_source.py` in a lone checkout. Every one exits 1:

```
M1-receipt-read-beside-the-module: KILLED
M2-record-read-from-the-host-source: KILLED
M3-no-digest-check: KILLED
M4-second-source-replaces: KILLED
M5-no-schema-presence-check: KILLED
M6-same-source-by-identity: KILLED
M7-unregister-keeps-it: KILLED
M8-registered-query-lies: KILLED
M9-no-source-means-skip-validation: KILLED
M10-no-meta-schema-check: KILLED
M11-application-errors-escape: KILLED
```

M10-no-composed-registration drops the conftest's registration. Run composed, it is KILLED: `1 failed, 16 passed`. The failing node is `test_execution_receipt_releases_cleanup_for_the_exact_destination`, the only one of the 16 that validates a receipt.

**The Copilot fixes**, each killed by the same case:
- at `542a5c40`, dropping `RecursionError`, or dropping `ValueError`, from the parse step's catch;
- at `a183dc46`, which replaced that tuple with a catch-all:
  - parse-catches-YAMLError-only: KILLED (`ESCAPED RecursionError`);
  - no-check-catch-all: KILLED (`ESCAPED RecursionError`).

### The suite as CI runs it, and the protected suites

- **The required job's own command**, in a venv built as CI builds it (openDox at the `pyproject.toml` pin `dede32b4`), at `122bacce`: `selected=1113 passed=1109 skipped=4 failures=0 errors=0`. All three pins hold: selected at least 1099, passed at least 1095, skipped exactly 4. None of the new cases skips.
- **The governed set is unchanged at 16**, and the new file names none of 12.5's markers.
- **No protected file is edited.** T059's oracle, as 12.5 calls it, over a simulated squash of `122bacce`'s tree onto `d45a0939` (`c3260498`; it touches the three files above). The merge head itself would be diffed against its first parent, which is no landing:
  ```
  $ python3 scripts/protected_suites.py --landings="<the simulated squash>" --suites="$(cat governed.txt)"
  ok: 0 protected edit(s), each entered and holding
  ```

### Copilot, by round

- **At `07361582`: r4194497045**, medium, "Validate host schemas before demotion to prevent uncaught errors".
  - **Fix-now**: a real bypass with a simple payload. Measured at `07361582`, all three cases escaped:
    - empty: `AttributeError: 'NoneType' object has no attribute 'get'`;
    - `required: 1`: `TypeError: 'int' object is not iterable`;
    - an unresolvable `$ref`: `_WrappedReferencingError`.
  - Fixed in `ecf0ba9b` by checking the schema with `check_schema()` before applying it. Answered and resolved.
- **At `ecf0ba9b`**: "Findings: None".
- **At `72633f93`: r4197914632**, "Translate all YAML parser failures to GateRefused".
  - **Fix-now**: `yaml.safe_load` raises past `YAMLError`. A 5000-deep `[[…]]` raised `RecursionError`, and `x: 2001-02-30` raised `ValueError`.
  - Fixed in `542a5c40`. Answered (4198580351) and resolved.
- **At `542a5c40`: r4198638746**, "Catch non-SchemaError exceptions from check_schema".
  - **Fix-now**: the cyclic mapping `properties: &p {a: {properties: *p}}` loads, and `check_schema` then raised `RecursionError`. Copilot's own payload, `allOf: &loop [*loop]`, is a `SchemaError` under the jsonschema CI resolves today (4.26.0, from `>=4.18`).
  - Fixed in `a183dc46`: this was the third finding on the boundary, so each step now refuses any failure. Answered (4199311149) and resolved.
- **At `122bacce` (the head; review 5433288963, COMMENTED): r4199337845**, "Avoid hard-coding jsonschema's version-specific cyclic failure".
  - **ACCEPTED LIMIT, under the convergence rule (#656 `5988818366`), for the holder at READY.** It is not a bypass: production behaviour is correct whatever any jsonschema release does with the cycle. The finding concerns the test's precision only.
  - The assertion `could not be checked: RecursionError` pins the outcome under the jsonschema CI resolves today (4.26.0) on purpose, because it shows the check step's own catch is what refuses. `pyproject.toml` allows `jsonschema>=4.18`, so a later release that refuses the cycle as a `SchemaError` would fail this case loudly, and its message would name the new outcome.
  - The one-line remedy is to assert only the `GateRefused: ` prefix, as the cyclic-list case does. The dropped-catch mutant stays killed either way. Answered (4199346014) and resolved.

**At the head `122bacce`:**
- Checks: `validate` pass, `SonarCloud Code Analysis` pass (2 of 2).
- Review threads: 4, all answered and resolved. 0 unresolved.

### Files changed (against `main`)

- `src/openxdox/gate_console.py`:
  - the two readers, `_packaged_schema_text` and `_host_schema_text`;
  - the schema checker, `_schema_errors`;
  - the seam: `register_contract_schema_source`, `unregister_contract_schema_source`, `contract_schema_source_registered` and `ContractSchemaSourceRefused`;
  - `GATE_ACTION_RECORD_SCHEMA` and `DEMOTION_RECEIPT_SCHEMA`.

  `_validate_contract_document` now takes the schema's text.
- `tests/conftest.py`: the composed run's registration of openxFactory's `contracts/schemas/`.
- `tests/test_gate_console_schema_source.py`: new, 6 lone cases.

### Not done here

- openxFactory's host wiring registers the same source at T030 (lane 4). It becomes an entry in `opendox_host.seams()`, with `contract_schema_source_registered` and `unregister_contract_schema_source` as its take-back pair.
- The runbook (T022) and the allow-list respellings (T026) are other slices. `test_the_cleanup_cli_verb_enforces_the_human_gate_and_deletes_the_branch` needs T020's HR, T026's AL and this GA together.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
…to T022

T020 (#39 c6d15b2), T024 (#38 8b64fae), T023 (#42 d45a093) and T021
(#40 f70bc9c) have landed. This branch carried T021's ecf0ba9 versions of
src/openxdox/gate_console.py and tests/test_gate_console_schema_source.py,
and main holds T021's final squash. Both conflicts are resolved to main's
side, since neither file is T022's. Against main the branch now differs only
in T022's three files: .gitignore, scripts/composed_placements.py and
tests/test_composed_placements.py.

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 6, 2026
…span kind (plan 038, H-2) (#43)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6015602254)

Arc: neutral-product-standalone-operability

## T026, U-7: the allow-list for 12.5's governed suites, with R-1 (a)'s span kind (plan 038)

The After set has landed:
- T020 (#39, at c6d15b2). This branch is up to date with `main` f70bc9c, which also holds T024 (#38), T023 (#42) and T021 (#40). `main` was merged in at e6d99691, 3226d5c, 9e0979c and 9c29855, never by rebase;
- T004 (opensoft/openxFactory#1245, at 260234b5);
- T005's batch Q (opensoft/openxFactory#1248, at 91e961a0), which gives 12.5's oracle call `--chains`. openxFactory `main`'s `add-neutral-product-standalone-operability/tasks.md:3055` now reads `python3 scripts/protected_suites.py --chains --landings="$(cat "$W/x-arc.txt")" --suites="$(cat "$W/governed.txt")"`.

- **Realizes**: 12.5 (part).
- **Falsifier** (tasks.md): the oracle prints `ok: … each entered and holding`; the nodes, composed, quoted.
- **Ruled**:
  - R2Q8, CF-4 (H-2), R-1 and W-1, with plan 038 by Brett Heap, everything as recommended (opensoft/openxFactory#656 comment 6013547504).
  - Brett's ruling on this task's four points (comment 6016648451, every answer the recommended one):
    1. 12.5's call takes `--chains`, and T026 lands as one PR.
    2. and 3. R-1 (a)'s spans widen to six, the helpers hand each harness the SERVED display, and T020's host registration covers `tests/test_staging_workbench.py`, edited by T026 in T020's two files once #39 landed.
    4. T026's Files gain `tests/test_protected_suite_check.py`.
  - The holder respelled the 'cluster' node to `group neighbourhood`, as measured.

### What this PR does

32 entries are appended to `tests/protected_suite_respellings.yaml`, giving 51 in all. Each is one edit inside its named test or span, with its exact old and new text, and the entries for each suite are chained by blob.

| suite | entries | what |
|---|---|---|
| `test_session_gates.py` | 3 | CF-4: `cmd_gate_*` read from `openxdox.cli_gate` |
| `test_session_verbs.py` | 5 | CF-4: `cmd_gate_*` 4, and Group W `openxdox.generator.live_topic_health` |
| `test_session_confinement.py` | 3 | CF-4: `serve_projection.hosted_index` |
| `test_doxbench_share.py` | 2 | CF-4: each swept module at its one home; the record schema read through `openxdox.contracts.verified_bytes`, digest-checked against `copies.yaml` (Copilot's finding) |
| `test_staging_workbench.py` | 19 | see below |

The 19 staging entries, in chain order (allow-list entries 33 to 51):
- **33**: the 'cluster' node, respelled to `group neighbourhood`.
- **34, 37, 38 and 39**: CF-4 Group S2, the four slice-S5 spellings of the create and session columns.
- **35 and 36**: R-1 (a)'s two in-test relocations, `:1291` and `:1333`. Each route is one `export const …_ROUTE` of `swb-session.js`, and neither the view nor the model names one.
- **40 to 49**, R-1 (a)'s six spans (seven `span:` entries) and three in-test respellings:
  - `_CREATE_HARNESS` (two entries), `_SESSION_HARNESS` and `_HOSTILE_HARNESS`: each imports its route from the column RULED Q3 moved it to (`CREATE_ROUTE` from `swb-create.js`, `sessionRoute` from `swb-session.js`) and the reader from `display.js`. Each installs `setDisplay(readDisplay({ display: cases.display }))` before anything derives, as the shell does. `BRAINSTORM_AREA` and `STAGING_AREA` become the model's `capturedArea()` and `organizedArea()`.
  - `_run_create`, `_run_session` and `_hostile_descriptors`: each copies the sibling files its harness imports (and what they import), with a `package.json` that makes node read them as modules, as `test_session_confinement.py`'s harness does. Each hands its harness `/capabilities["display"]`, by the statement `serve.build_server()` makes, replayed as `test_gate_loop_views.py`'s `_served_display` replays it.
  - Three in-test respellings, **flagged below**.
- **50 and 51**: the docstrings of the two relocated tests now say what their assertions hold (Copilot's findings).

**`scripts/protected_suites.py`, R-1 (a)'s span kind.** An `admitted` entry may name, in place of `test`, a `span`: one module-level constant, or one module-level helper function (its decorators included). The entry's `old` text must lie inside that statement before the landing, and its `new` text inside it at the landing. Three further rules apply:
- **Counted fail-closed.** The span's name must have exactly one binding in the module, counting every binding form in every scope: assignment, augmented or annotated assignment, `for` or `with` target, walrus, `del`, `def` or `class`, an import or a star import, an `except … as` alias, a `match` capture, a type parameter and a parameter.
- **Alone on its lines.** No other statement may share a line with the span.
- **Never a span:** a class, an import, a test, two targets, an annotation alone, or an added name. Only an admitted entry may name a span.

The module's usage block and help now give 12.5's call with `--chains`, as batch Q has it.

**`tests/test_protected_suite_check.py`**: 33 span cases (87 in the module).

#### FLAGGED for the holder's word: three in-test respellings beyond the plan's 17

These are CF-4's kind. T026's measurement reported them at comment 6016415390 (point 2), and comment 6016648451 ruled that point. They close the last three of the suite's nodes, which the served display alone does not reach. Each is entered as a `respelling` naming that history, and each stands or falls on your word:

1. `test_gate_off_descriptor_is_the_real_cli_invocation`: `cli.cmd_gate_create_document` becomes `cli_gate.cmd_gate_create_document`, imported in the test from `openxdox.cli_gate`. The old spelling raised `AttributeError`.
2. `test_gate_off_session_affordances_are_the_real_cli_invocations`: `cli.cmd_gate_edit_document`, `cmd_gate_open_pr` and `cmd_gate_abandon_session` become `cli_gate.…`, likewise.
3. `test_the_notebook_refresh_is_a_descriptor_in_BOTH_gate_postures`: `REPO_ROOT / "scripts" / "sync-notebooklm-books.py"` becomes the one directory on the import path that holds the script, which is openxFactory's `scripts/` in the composed run (R1Q23 (a)). A run that finds it in none, or in more than one, fails. The old path raised `FileNotFoundError`.

No assertion is weakened in any of the three, and every argument and flag assertion stays.

### T020's two files, edited once #39 landed (commit 242889b)

T020's registration has to cover `tests/test_staging_workbench.py`. Without it every harness helper refuses `ProfileNotRegistered`, as measured below. The edit is to T020's `tests/conftest.py` and to the scan in `tests/test_host_plane.py`:
- `"test_staging_workbench.py"` joins `GOVERNED_HOST_SUITES`, with a comment giving the reason, kept as a block of its own apart from T021's lines;
- the scan also recognises a `gate` verb spelled as a command line (`_GATE_COMMAND = re.compile(r"""["']gate ([a-z][a-z-]*)""")`), the way this suite's descriptors spell it. Measured over the 16 governed suites, only `test_staging_workbench.py` matches it and not the argument-vector spelling.

T021 (#40) also edits `tests/conftest.py`. Whichever of the two lands second merges `main`.

### The falsifier

**The oracle, in 12.5's call as `main` now has it (`--chains`), at a simulated squash landing of this head.** The landing is one local commit on `main` f70bc9c carrying exactly this branch's tree and the `Arc:` trailer, and it is never pushed. The inputs are 12.5's own:
- the governed set by its `git grep`: 16, the same 16;
- the `Arc:` landings since `e28930bf`: 14, T020's, T024's, T023's and T021's among them.

```
admitted: c41063d tests/test_gate_loop_views.py, by entry 1 of tests/protected_suite_respellings.yaml
admitted: 839492d tests/test_gate_loop_views.py, by entry 2 of tests/protected_suite_respellings.yaml
admitted: 56e1c23 tests/test_branch_session.py, by entry 18 of tests/protected_suite_respellings.yaml
admitted: 2f7003912aa7 tests/test_doxbench_share.py, by entries 31, 32, in that order, of tests/protected_suite_respellings.yaml
admitted: 2f7003912aa7 tests/test_session_confinement.py, by entries 28, 29, 30, in that order, of tests/protected_suite_respellings.yaml
admitted: 2f7003912aa7 tests/test_session_gates.py, by entries 20, 21, 22, in that order, of tests/protected_suite_respellings.yaml
admitted: 2f7003912aa7 tests/test_session_verbs.py, by entries 23, 24, 25, 26, 27, in that order, of tests/protected_suite_respellings.yaml
admitted: 2f7003912aa7 tests/test_staging_workbench.py, by entries 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, in that order, of tests/protected_suite_respellings.yaml
ok: 8 protected edit(s), each entered and holding
```

rc=0. In the pre-batch-Q form, which passes no `--chains`, a landing of this branch reads `FAIL: the arc edited protected suites outside the reviewed allow-list: tests/test_doxbench_share.py, …` with rc=1. That is why batch Q was ruled.

**The oracle on mutants of the real list** (`--chains`), each refused:

```
A a chain step's before_blob broken: rc=2
    FAIL: tests/protected_suite_respellings.yaml breaks its own rules, so nothing is subtracted: entry 44: its before_blob is not the after_blob of the entry before it for tests/test_staging_workbench.py, so the two do not chain
B the last staging step dropped: rc=1
    refused:  f7ebecbcc37e tests/test_staging_workbench.py: entries 33-50: tests/test_staging_workbench.py at the landing is 66d16b7, not the chain's 54852fa5250b2c4d0630088154aa4bac6517bffe; entries 3…
    FAIL: the arc edited protected suites outside the reviewed allow-list: tests/test_staging_workbench.py
C a span entry names another span: rc=1
    refused:  f7ebecbcc37e tests/test_staging_workbench.py: entries 33-51: step 10: the entry's old text is not inside _run_session before the landing; entries 34-51: tests/test_staging_workbench.py before the landing is 655afc8…
    FAIL: the arc edited protected suites outside the reviewed allow-list: tests/test_staging_workbench.py
D a span entry names a test instead: rc=1
    refused:  f7ebecbcc37e tests/test_staging_workbench.py: entries 33-51: step 10: the entry's old text is not inside test_toggle_keyword_is_order_preserving_and_idempotent before the landing; entries 34-51: tests/test_staging_workbe…
    FAIL: the arc edited protected suites outside the reviewed allow-list: tests/test_staging_workbench.py
E a respelling entry names a span: rc=2
    FAIL: tests/protected_suite_respellings.yaml breaks its own rules, so nothing is subtracted: entry 43: names a span, and only an admitted entry may (R-1 (a)); a respelling entry names its test
```

**R-1 (a)'s span guards: 22 mutants** of `scripts/protected_suites.py`, each run against the committed `tests/test_protected_suite_check.py`. 21 are killed. M4 survives because it is equivalent: a nested binder also lies inside its compound statement's lines, so the shared-line guard refuses it too. M22, which removes both guards, is killed.

```
M1 a respelling may name a span: killed (first: test_a_span_entry_that_breaks_the_rules_subtracts_nothing[respelling])
M2 a span may be spelled as a test: killed (first: test_a_span_entry_that_breaks_the_rules_subtracts_nothing[a-test])
M3 a name bound twice may be a span: killed (first: test_what_is_never_a_span_is_refused[shadowed])
M4 a block-only binding may be a span: SURVIVED
M5 a del is not a binding: killed (first: test_a_second_binding_in_any_form_or_scope_is_never_a_span[del])
M6 an except alias is not a binding: killed (first: test_a_second_binding_in_any_form_or_scope_is_never_a_span[except-alias])
M7 a match capture is not a binding: killed (first: test_a_second_binding_in_any_form_or_scope_is_never_a_span[match-capture])
M8 a star import is not a binding: killed (first: test_a_second_binding_in_any_form_or_scope_is_never_a_span[star-import])
M9 a parameter is not a binding: killed (first: test_a_second_binding_in_any_form_or_scope_is_never_a_span[parameter])
M10 a class may be a span: killed (first: test_what_is_never_a_span_is_refused[class])
M11 a helper function is never a span: killed (first: test_an_edit_inside_a_named_helper_function_is_admitted)
M12 two targets may be a span: killed (first: test_what_is_never_a_span_is_refused[two-targets])
M13 an annotation alone may be a span: killed (first: test_what_is_never_a_span_is_refused[annotation-only])
M14 a helper's decorators lie outside it: killed (first: test_a_helpers_decorator_is_inside_its_span)
M15 a shared line is allowed: killed (first: test_a_span_that_shares_a_line_with_another_statement_is_refused)
M16 old outside the span is not checked: killed (first: test_an_old_text_outside_the_span_is_refused)
M17 new outside the span is not checked: killed (first: test_a_new_text_that_leaves_the_span_is_refused)
M18 an added span is not refused: killed (first: test_what_is_never_a_span_is_refused[class])
M19 the span's first line is outside it: killed (first: test_an_edit_spanning_a_whole_named_constant_is_admitted)
M20 a span entry keeps the test key: killed (first: test_this_repositorys_allow_list_keeps_its_rules)
M21 span entries take the test path: killed (first: test_an_edit_spanning_a_whole_named_constant_is_admitted)
M22 M4 and M15 together: killed (first: test_what_is_never_a_span_is_refused[block-only])
M0 unmutated: 87 passed in 9.05s
```

**The served display, measured in the test process** (Brett's (2)+(3): "INFERRED … MEASURE it"). This was an uncommitted probe test, run composed at 3eead0fe (242889b on the integration of that time; the helpers' statement is unchanged since):
- with no host registered, the helpers' statement refuses `ProfileNotRegistered`;
- under the governed host (`OpenxFactoryProfile`), it reads `host_facet: declared` and `host_profile: openxfactory-engineering`, with areas `captured: ideation/brainstorm/` and `organized: ideation/staging/`;
- that result equals the `display` of a live `GET /capabilities` from `serve.build_server()` under the same registration: `EQUAL: True`, 3163 bytes of sorted JSON.

**The nodes, composed**, in R1Q23 (a)'s form:
- openxFactory `main` 92010d3e, with its submodules initialized recursively and its `scripts/` on `PYTHONPATH`;
- openDox-code#91 45f5c55d installed (W-1 (A));
- T022's runbook placement linked (its script, #41), the colour variables unset and `TERM=dumb`;
- each suite run alone;
- openXdox-code `main` f70bc9c (T020, T021, T023 and T024 landed) as the base, and this head merged onto it on a local branch that is never pushed (0478b5f1).

| run | 16 governed suites | `test_host_plane.py` | `test_protected_suite_check.py` |
|---|---|---|---|
| without this PR (`main` f70bc9c) | 842 nodes: **800 passed, 42 red** (staging 29, verbs 5, confinement 3, gates 3, share 2) | 9 passed | 54 passed |
| with this head 9c29855 (0478b5f1) | 842 nodes: **842 passed** | 9 passed | 87 passed |
| earlier, this PR WITHOUT the edit to T020's files (at 856047a, on `main` 56e1c23 with #41 and #42 merged) | `test_staging_workbench.py`: 37 failed, 46 passed, every failure `ProfileNotRegistered` | 9 passed | n/a |

42 nodes go red to green, none goes green to red, and none is still red.

### CI at the head 9c29855

- **Checks**: `validate` success (run 37517336710), SonarCloud success, `copilot-pull-request-reviewer` success.
- **CI's own pytest step, run alone** (CI's install, openDox at its pin dede32b4): `selected=1146 passed=1142 skipped=4 failures=0 errors=0`, against the floors `MIN_SELECTED 1099` and `MIN_PASSED 1095` and `EXPECT_SKIPPED 4`. The holder ruled (lane log, 13:57Z) that T029 raises the floors once, and T020 to T026 leave them.
- **Copilot**: reviewed at 9c29855, "Needs a closer look" (a governance-sensitive validator and a large chained allow-list), **Findings: None**. 9 threads over its reviews, each answered with its fixing commit and resolved; 0 unresolved.

### Files changed

`scripts/protected_suites.py`, `tests/protected_suite_respellings.yaml`, `tests/test_session_gates.py`, `tests/test_session_verbs.py`, `tests/test_session_confinement.py`, `tests/test_doxbench_share.py`, `tests/test_staging_workbench.py` (T026's seven), plus three files the ruling at comment 6016648451 adds to T026's Files:
- `tests/test_protected_suite_check.py`, the holder's answer to point 4;
- T020's `tests/conftest.py` and `tests/test_host_plane.py`, Brett's points 2 and 3, edited once #39 landed.

### What remains, and its trigger

- **The holder's word on the three flagged respellings.** Nothing else in this PR waits.
- If `main` moves before landing, the lander merges it, never by rebase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 7, 2026
…thing vendored (plan 038, P4F-3) (#41)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6013608959)

Arc: neutral-product-standalone-operability

## T022, U-3: the runbook placed from the composed tree (RP) (plan 038, feature `038-opendox-document-tool-self-maintenance`)

After set met: T004 #1245 → 260234b5; T020 #39 → c6d15b2; T021 #40 → f70bc9c.

**The branch.** The head is `57b4f80a`, GitHub's update-branch merge of `main` `e7815ac5`. T022's last commit before it is `1f592766`, the merge of `main` `da736090`, and T022's three files are byte-identical between the two. The diff against `main` is T022's three files.
- T022's own commits:
  - `c40af91b`: U-3;
  - `7a149b70`: the committed tests, under the holder's Files ruling;
  - `dc879beb`, `146ae923`, `74f3a4fb`, `1f2d6858` and `2c86dcca`: the review fixes below.
- Main came in by merges, never a rebase: `42d7ea97` (`f70bc9c3`), `c22b5df7` (`36bbef79`), `49c1026d` (`e2340bb6`), `214f3613` (`fc1115d6`), `60c56a47` (`6f259156`) and `1f592766` (`da736090`).
- At `42d7ea97` this branch still carried T021's older `ecf0ba9b` versions of `src/openxdox/gate_console.py` and `tests/test_gate_console_schema_source.py`. Both conflicts were resolved to main's side, since neither file is T022's.

### What changes

`tests/test_session_runbook.py` (its module constant `RUNBOOK`) and `tests/test_session_notebook.py` read `docs/ideation-dashboard-session-runbook.md` at the checkout root. The carve sent the runbook to openDox-spec, so this checkout has none, and both suites are protected.

- **New `scripts/composed_placements.py`** links the runbook, for a composed run, from the composed tree into `docs/`. The source is `openDox/spec/docs/ideation-dashboard-session-runbook.md` under the openxFactory checkout.

  ```
  python3 scripts/composed_placements.py --openxfactory "$OPENXFACTORY"            # place
  python3 scripts/composed_placements.py --openxfactory "$OPENXFACTORY" --remove   # take back
  ```

  Before it places anything, it refuses (exit 2), naming the remedy, unless all of these hold:
  - **Each leg is its own work tree.** The openDox root and the spec leg each have their own `.git`, and `git rev-parse --show-toplevel` resolves to the directory itself. So no enclosing repository answers for them, and no `core.worktree` points the checks elsewhere.
  - **The spec leg is at the pin.** Its HEAD is the commit the root's `spec` gitlink pins.
  - **The bytes are the pinned bytes.** The runbook is tracked, no component from the spec leg down to the file is a symbolic link, and its raw bytes are the pinned blob: `git hash-object --no-filters` equals `HEAD:<path>`. So no index flag, clean filter or EOL normalization can hide an edit.
  - **The destination is ignored by the repository-root `.gitignore` itself**, read through `git check-ignore -v`. A local exclude alone (`.git/info/exclude`, `core.excludesFile`) is refused, and so is a negated pattern.
  - **Nothing is overwritten.** Anything already at the destination must be the script's own link to that same source.

  Every git call drops the ambient repository variables (`GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE` and kin, as `openxdox.domain_corpus_adapter` drops them), so a hook cannot redirect the checks. The script places all or nothing. `--remove` takes back its own links and nothing else. It needs only the standard library and `git`.
- **`.gitignore`** names `/docs/ideation-dashboard-session-runbook.md`, so the checkout stays clean and nothing placed can be committed.
- **New `tests/test_composed_placements.py`**, 30 cases, holds those guards in the suite CI runs. Each case runs the script in a child interpreter: a copy of this checkout's script, inside a scratch checkout that carries this checkout's own `.gitignore`. It runs against a scratch openxFactory/openDox/spec tree pinned to commit A, so no case writes into this checkout. No case skips, and the file carries none of 12.5's governed-suite markers.
- **Nothing is vendored.** `src/openxdox/contracts/copies.yaml` is untouched (P4F-3; ADV-10).

The composed workflow (T029) and the documented local composed run call the script. Neither is in this PR.

### The contract, from `tasks.md`

- **Realizes**: 12.5 (part).
- **Falsifier**: the 10 nodes, composed, quoted.
- **Ruled**: R2Q8. **Decisions**: P4F-3 ("The runbook is PLACED from the composed tree (`openDox/spec/docs/…` at the root's pinned spec commit) by T022's script, git-ignored; nothing vendored."), ruled with plan 038 by Brett Heap (#656 `6013547504`).
- **After**: T021, met as above.
- **Files**: new `scripts/composed_placements.py` and `.gitignore`.
  - **Amended** by the holder's ruling on opensoft/openxFactory#656, comment `6016356225`, option (a): add the new `tests/test_composed_placements.py`.
  - The `check-ignore` source fix (Copilot r4196244480, raised on #45) is ruled into this PR by #656 `6026275158`, item 2.
  - Nothing else is changed.

### The falsifier, composed

**Environment.**
- openxFactory `51456835`, with its submodules initialized recursively and `scripts/` on `PYTHONPATH` (R1Q23 (a)).
- The openDox root `e1e3a3c3` pins openDox-spec `f7ee3c76`, the spec checkout's HEAD.
- openDox-code `34c20641`, force-reinstalled with `--no-deps`.
- Python 3.12.3, pytest 8.4.2, `LANG=C.UTF-8`, no colour forcing.

At the head, the runbook is placed, then removed afterwards, which leaves the checkout clean:

```
python3 scripts/composed_placements.py --openxfactory "$OPENXFACTORY"
PYTHONPATH="$OPENXFACTORY/scripts" python -m pytest -q -p no:cacheprovider -rA <the 10 runbook nodes, one process per suite file>
```

| at | the 10 runbook nodes |
|---|---|
| main `56e1c238` (first round) | `9 failed` + `1 failed` |
| main `f70bc9c3`, `36bbef79`, `6f259156` | "PASSED nodes: 0 / FAILED-or-ERROR nodes: 10" |
| **head `1f592766`, placed** | **"PASSED nodes: 10 / FAILED-or-ERROR nodes: 0"** |

The cause at main is `FileNotFoundError: … '<checkout>/docs/ideation-dashboard-session-runbook.md'`.

The 10 nodes:
- `test_session_runbook.py`:
  - `test_the_runbook_start_command_parses_with_the_real_cli_parser`
  - `test_the_runbook_names_the_two_required_flags_and_what_they_are_for`
  - `test_the_runbook_claims_no_flag_generate_and_open_does_not_have`
  - `test_the_runbook_says_what_a_wrong_repo_root_does`
  - `test_the_runbook_does_not_call_the_unvalidated_snapshot_routine`
  - `test_the_runbook_lists_the_posture_readings_the_page_can_produce`
  - `test_the_derived_artifact_inventory_names_every_container_the_code_writes`
  - `test_the_runbook_names_the_pull_the_refusal_names`
  - `test_the_runbook_states_what_a_session_may_rewrite`
- `test_session_notebook.py`:
  - `test_the_runbook_names_the_bound_by_its_production_constants`

**The 16 suites, each alone (first round, at `c40af91b`/`7a149b70` against main `56e1c238`):**
- 82 red of R2-INV-P4F's 174;
- 0 regressions;
- node for node the same at `7a149b70`.

T022 changes no code under `src/`. The only change it makes to a composed run is the placement.

### The script's guards: 30 cases and 23 mutants

`tests/test_composed_placements.py` reads `30 passed` at `1f592766`. Each later finding's cases were red against the script before its fix; the review rounds below quote them.

All 23 mutants were applied to this checkout's script or `.gitignore`, which the cases copy, and every one is killed by the case that names its guard:

| mutant | killed by |
|---|---|
| MA no pin comparison | `test_a_spec_checkout_off_the_opendox_roots_pin_is_refused` |
| MB content not compared | `test_a_modified_runbook_is_refused` |
| MW hash through clean filters | `test_an_edit_a_clean_filter_would_hide_is_refused[clean-filter]` |
| MS no link check | `test_a_runbook_the_pinned_commit_tracks_as_a_link_is_refused` |
| MX only the last component | `test_a_runbook_reached_through_a_linked_directory_is_refused` |
| MU root may be any repository | `test_an_opendox_root_answered_by_an_enclosing_repository_is_refused` |
| MV toplevel not compared | `test_a_spec_leg_whose_work_tree_is_configured_elsewhere_is_refused` |
| MO no pinned-root check | `test_an_opendox_root_without_a_spec_gitlink_is_refused` |
| MI spec leg is any directory | `test_an_uninitialized_spec_leg_is_refused_as_not_checked_out` |
| MC no tracked check | `test_a_runbook_the_spec_does_not_track_is_refused` |
| MD no ignore check | `test_a_destination_the_gitignore_does_not_name_is_refused` |
| MP any ignore source | `test_a_destination_only_a_local_exclude_names_is_refused[info/exclude]` |
| MQ a negation counts as ignored | `test_a_gitignore_that_negates_the_destination_is_refused` |
| MJ `.gitignore` drops the runbook | `test_a_pinned_unmodified_runbook_is_linked_and_leaves_the_checkout_clean` |
| MR ambient git variables kept | `test_ambient_git_repository_variables_do_not_redirect_the_checks[clean]` |
| ME overwrite a foreign file | `test_a_file_at_the_destination_is_refused_and_kept[place]` |
| MF remove any file | `test_a_file_at_the_destination_is_refused_and_kept[remove]` |
| MK place accepts any link | `test_a_link_elsewhere_at_the_destination_is_refused_and_kept[place]` |
| ML remove takes any link | `test_a_link_elsewhere_at_the_destination_is_refused_and_kept[remove]` |
| MM remove unlinks nothing | `test_remove_takes_back_its_own_link_and_nothing_else` |
| MN no already-placed short circuit | `test_placing_again_is_a_no_op` |
| MG no git crashes | `test_without_git_on_path_it_refuses` |
| MH place before checking all | `test_a_modified_runbook_is_refused` |

```
mutants: 23/23 killed; files restored
```

### The suite as CI runs it, and the protected suites

- **The required job's own command**, in a venv built as CI builds it (openDox at the `pyproject.toml` pin `dede32b4`), at `1f592766` with nothing placed: `selected=1195 passed=1191 skipped=4 failures=0 errors=0`. All three pins hold.
- **The governed set is unchanged at 16.**
- **No protected file is edited.** T059's oracle, as 12.5 calls it, over a simulated squash of `1f592766`'s tree onto `da736090`, which touches T022's three files: "ok: 0 protected edit(s), each entered and holding".

### Copilot, by round

- **At `c40af91b`:**
  - **r4194596249**, "Translate schema validation errors into GateRefused" (T021's code, carried then): fix-now, fixed in T021's `ecf0ba9b`, merged here at `8d02a1b3`.
  - **r4194596317**, "Add pytest coverage for filesystem-mutating script safeguards": first an accepted limit, put to the holder; the holder ruled (a) (#656 `6016356225`), making it fix-now. Fixed in `7a149b70` (the committed tests). That commit also fixed a misreport the new tests found: an uninitialized spec leg had been reported at the root's HEAD.
- **At `8d02a1b3`**: "Findings: None".
- **At `7a149b70`: r4195469835**, the PR description. Fixed in the body.
- **On #45 (`26fe63e7`), on this file: r4196244480**, `check-ignore` honours local excludes. The holder ruled it into this PR (#656 `6026275158`, item 2).
  - **Fixed in `dc879beb`**: the source must be the root `.gitignore`, and a negated pattern is refused.
  - Red before the fix: the `.git/info/exclude` and `core.excludesFile` cases (`2 failed, 16 passed`).
  - #45's thread is answered by citing `dc879beb` once #45 merges `main`.
- **At `dc879beb`: r4200975244**, ambient `GIT_DIR`/`GIT_WORK_TREE` redirect the checks. Fix-now, fixed in `146ae923`; red before: `2 failed, 18 passed`.
- **At `146ae923`/`c22b5df7`:**
  - **r4201032934**, `assume-unchanged`/`skip-worktree` hide an edit from `git diff`;
  - **r4201424060**, a symlinked runbook;
  - **r4201032886**, repository discovery inherited or redirected.

  All fix-now, fixed in `74f3a4fb`. Before it, four of the new cases PLACED the link: both index flags, the link tracked at the pinned commit, and the enclosing repository.
- **At `60c56a47`: r4201850292**, `hash-object` applies clean filters. Fix-now, fixed in `1f2d6858` (`--no-filters`). Before it, a hiding clean filter and a CRLF edit under `text` both PLACED the link.
- **At `1f2d6858`: r4201912214**, a symbolic link in a parent directory. Fix-now, fixed in `2c86dcca`. Before it, a linked `docs/` holding identical bytes PLACED the link.
- **r4200975330 and r4201032964**, the PR description: fixed by this body.
- **At the head `1f592766`**: review 5440606809, no new finding. Its overview reads "Needs a closer look" for one stated reason: "It remains a cross-repository draft stacked on prerequisites that have not landed". The After set above has landed, so this is not a finding.
- Each fix-now finding was a real bypass with a simple payload, under the convergence rule (#656 `5988818366`).
- **At `57b4f80a`**, GitHub's update-branch merge of `main` `e7815ac5` after the un-draft, with T022's three files byte-identical to `1f592766`: review 5440672686, two findings. Both are **ACCEPTED LIMITS under 5988818366, for the holder at READY**; the PR was un-drafted, so neither is pushed.
  - **r4205549424**, a symlinked destination parent. It is already refused. Measured: with `checkout/docs` a link outside, `git check-ignore` answers "pathspec ... is beyond a symbolic link" (exit 128), the ignore guard refuses (exit 2) before `mkdir` or `symlink_to`, and the outside directory stays empty. Only the message misnames the cause.
  - **r4205549516**, the pinned tree mode. The scenario needs a pin that tracks the runbook as a link, but openDox-spec tracks it as `100644 blob e88f06e6` at `f7ee3c76`. The one-line hardening is a `git ls-tree` mode check, as a follow-on.

**At the head `57b4f80a`** (T022's content as at `1f592766`):
- Checks: `validate` pass (run 37604221972), `SonarCloud Code Analysis` pass, Copilot success, Sourcery skipped. At `1f592766`: validate run 37603513031 passed.
- Review threads: 13, all answered and resolved. 0 unresolved.

### Not done here

- The permanent composed workflow that calls the script on every run is T029 (U-9).
- The "documented local composed run" is wherever T029 documents it. This PR adds the script's own usage docstring only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants