Skip to content

T021, U-2: the gate console's schemas, each read where its owner keeps it, nothing vendored (plan 038, P4F-4) - #40

Merged
brettheap merged 8 commits into
mainfrom
r2/t021-gate-console-schema-source
Oct 6, 2026
Merged

brettheap merged 8 commits into
mainfrom
r2/t021-gate-console-schema-source

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 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.

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: T020, U-1: the governed host for the governed suites that drive the gate verbs (plan 038, P4F-5) #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

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

Summary by Sourcery

Make gate-console contract validation use each schema owner’s source while refusing safely when host schema provisioning or validation is invalid.

New Features:

  • Read packaged gate-action record schemas from the distribution’s verified contract copies and host-owned demotion receipt schemas from an explicitly registered source.
  • Add a single-source registration seam with idempotent registration, validation, replacement protection, unregistration, and registration status APIs.
  • Reject malformed, invalid, or unappliable host schemas as GateRefused errors.

Bug Fixes:

  • Stop schema validation from depending on checkout-relative paths that are unavailable in standalone distributions.
  • Prevent malformed host schemas from escaping as unexpected exceptions during receipt validation.

Enhancements:

  • Keep schema ownership explicit by avoiding vendoring and requiring hosts to provide their own receipt schema directory.

Tests:

  • Add standalone and composed tests covering packaged versus host-owned schema loading, registration behavior, missing sources, schema failures, digest drift, and removal of checkout-relative schema access.

brettheap and others added 2 commits October 6, 2026 10:39
…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>
…s it, nothing vendored (plan 038)

gate_console._validate_contract_document read both of its schemas from
parents[2] / "contracts" / "schemas", which in this leg is the checkout
root and holds no contracts/. Now each schema is read where its owner
keeps it:
  * gate-action-record.schema.yaml, openXdox-spec's, from this
    distribution's own packaged copy, through
    contracts.verified_bytes(), which checks it against copies.yaml's
    digest first;
  * demotion-execution-receipt.schema.yaml, openxFactory's, from a source
    the HOST registers with the new register_contract_schema_source().
    The same directory again is a no-op, a different one is refused, and
    a directory without the schema is refused when it is registered.
    unregister_contract_schema_source() and
    contract_schema_source_registered() complete the seam. With nothing
    registered, a receipt refuses, naming the call (R1Q27 (a)).

Nothing is added to copies.yaml, and test_packaged_validator.py is
unchanged and green. The seam sits near the end of the module, so that
no line before it moves. tests/test_no_hardcoded_status_words.py pins
seven prose f-strings in this module by line number.

tests/conftest.py registers openxFactory's contracts/schemas/ from the
composed tree, at process start, where the run is composed; a lone
checkout registers none. tests/test_gate_console_schema_source.py runs
the console in a child interpreter beside a stand-in doc_health, so it
runs in a lone checkout.

Composed, the 16 schema nodes and test_explicit_release_has_cli_and_http_parity
turn green on T020's host. 12.5's suites read 92 red of the 174, and no
green node turns red.

Refs opensoft/openxFactory#656 (CLAIMED 6013608628).

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 11:02
@sourcery-ai

sourcery-ai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR removes checkout-relative schema loading from the gate console, reads the openXdox-owned schema from its verified package, obtains the openXfactory-owned receipt schema through a validated single-source host seam, wires that seam into composed tests, and adds lone-checkout and host-plane coverage for the behavior.

Sequence diagram for owner-specific gate schema validation

sequenceDiagram
    participant Caller
    participant GateConsole
    participant PackagedContracts
    participant HostSchemaSource

    Caller->>GateConsole: validate_gate_action_record(record)
    GateConsole->>PackagedContracts: verified_bytes(GATE_ACTION_RECORD_SCHEMA)
    PackagedContracts-->>GateConsole: digest-verified schema text
    GateConsole->>GateConsole: _validate_contract_document(record, schema_text, label)
    GateConsole-->>Caller: validation result

    Caller->>GateConsole: validate_demotion_execution_receipt(receipt)
    GateConsole->>HostSchemaSource: read demotion-execution-receipt.schema.yaml
    HostSchemaSource-->>GateConsole: host schema text
    GateConsole->>GateConsole: _validate_contract_document(receipt, schema_text, label)
    GateConsole-->>Caller: validation result
Loading

State diagram for the host contract schema source

stateDiagram-v2
    [*] --> Unregistered
    Unregistered --> Registered: register_contract_schema_source(source)
    Registered --> Registered: register_contract_schema_source(same source)
    Registered --> Unregistered: unregister_contract_schema_source()
    Unregistered --> Unregistered: register_contract_schema_source(missing schema) / ContractSchemaSourceRefused
    Registered --> Registered: register_contract_schema_source(different source) / ContractSchemaSourceRefused
    Unregistered --> Unregistered: validate_demotion_execution_receipt() / GateRefused
    Registered --> Registered: validate_demotion_execution_receipt()
Loading

File-Level Changes

Change Details Files
Route each contract schema lookup to its owning distribution or host-registered source instead of deriving both paths from the checkout layout.
  • Load the gate-action schema through the packaged, digest-verified contracts API.
  • Add a single-source registration seam for the host-owned demotion receipt schema, including idempotence, replacement refusal, presence validation, unregister, and status query.
  • Refactor validation to accept schema text and preserve the existing refusal and validation behavior at the call sites.
src/openxdox/gate_console.py
Register the host-owned schema during composed test setup while retaining clean lone-checkout behavior.
  • Register openXFactory's composed contracts/schemas directory when its host module is available.
  • Leave lone checkouts unregistered so receipt validation fails explicitly by contract name.
  • Extend host-plane composition helpers and tests to cover governed host setup, import isolation, and restoration of process-wide state.
tests/conftest.py
tests/test_host_plane.py
Add isolated tests proving schema ownership, source lifecycle, and removal of checkout-relative schema reads.
  • Run schema-source checks in child interpreters with a minimal doc_health stand-in.
  • Verify packaged-schema validation and digest refusal independent of a registered host source.
  • Verify receipt validation against the registered source, single-source semantics, missing-schema refusal, and unregister behavior.
  • Use AST coverage to ensure gate_console.py no longer reads a parents-indexed schema path.
tests/test_gate_console_schema_source.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

🟡 Changes recommended

Invalid host schemas can escape as uncaught exceptions after demotion execution instead of producing a controlled refusal.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Moves gate-console schema loading to each contract owner and adds the stacked governed-host test composition.

Changes:

  • Loads packaged schemas through digest verification and host schemas through registration.
  • Registers composed openxFactory host/schema sources in tests.
  • Adds host-plane and schema-source coverage.
File Description
src/​openxdox/​gate_console.py Adds owner-specific schema loading and registration APIs.
tests/​conftest.py Composes governed hosts and host schema registration.
tests/​test_gate_console_schema_source.py Tests schema ownership and registration semantics.
tests/​test_host_plane.py Tests governed-host selection and isolation.

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

Comment thread src/openxdox/gate_console.py Outdated
…Copilot r4194497045)

A registered source's receipt schema was checked only for existence. A
document that is valid YAML but no valid JSON Schema escaped as a raw
exception: an empty file raised AttributeError, `required: 1` raised
TypeError, and an unresolvable $ref raised jsonschema's referencing
error. cli_gate's demotion reports only GateRefused, BoundaryViolation or
OSError as "executed but receipt not written", after files have moved, so
any of these ended the verb with a traceback.

_schema_errors now parses the schema, checks it against the validator's
own meta-schema, and applies it. A failure at any of the three refuses as
GateRefused. _validate_contract_document keeps its line count, so no line
that test_no_hardcoded_status_words.py's PROSE_EXEMPT pins moves. A new
lone case covers the three schemas, and two more mutants are killed.

Refs opensoft/openxFactory#656 (CLAIMED 6013608628).

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 11:26

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

It remains a draft with stacked cross-repository dependencies and deferred production host wiring requiring human integration review.

Review effort: Balanced
Findings: None

Resolved since last review (1)

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>

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

🟡 Changes recommended

Some YAML parser failures can still escape GateRefused after demotion has moved files.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Comment thread src/openxdox/gate_console.py Outdated
brettheap and others added 2 commits October 6, 2026 17:26
Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d (Copilot r4197914632)

yaml.safe_load raises RecursionError, for a nesting deeper than the
interpreter allows, and ValueError, for an impossible date such as
2001-02-30, past YAMLError. _schema_errors caught only YAMLError, so a
registered receipt schema holding either payload escaped as a raw exception
after the demotion had moved files. It now catches the same three classes
openxdox.contracts' record reader does, and refuses as GateRefused, "could
not be parsed".

tests/test_gate_console_schema_source.py adds three payloads to its
cannot-apply case: an unclosed flow sequence (a YAMLError, the control), a
5000-deep sequence, and the impossible date. Before the fix the case reads
"ESCAPED RecursionError: maximum recursion depth exceeded". With it, all six
cases pass. Dropping RecursionError from the tuple, or dropping ValueError,
is killed by that same case.

Refs opensoft/openxFactory#656 (CLAIMED 6013608628).

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap
brettheap requested a balanced review from Copilot October 6, 2026 17:42
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>

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

🟡 Changes recommended

Recursive YAML aliases can still escape schema checking as an unhandled exception.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Comment thread src/openxdox/gate_console.py
brettheap and others added 2 commits October 6, 2026 17:50
…sed (Copilot r4198638746)

A cyclic YAML alias such as properties: &p {a: {properties: *p}} loads,
but the validator's meta-schema check never finishes it. check_schema
raised RecursionError, which escaped as a raw exception after the demotion
had moved files. This is the third finding on the same boundary, so the
boundary now refuses any failure, as the packaged validator's does. Each of
the three steps (parsed, checked, applied) turns any exception into
GateRefused, naming the step and the exception's class. A SchemaError keeps
its own "is not a valid JSON Schema" message.

tests/test_gate_console_schema_source.py adds the cyclic mapping, which
must refuse "could not be checked: RecursionError", and Copilot's cyclic
list. The pinned jsonschema 4.26.0 refuses the cyclic list as a
SchemaError already; it must refuse as GateRefused either way. Before the
fix the case reads "ESCAPED RecursionError: maximum recursion depth
exceeded"; with it, 6 passed. Two mutants are each killed: narrowing the
parse step back to YAMLError, and dropping the check step's catch.

Refs opensoft/openxFactory#656 (CLAIMED 6013608628).

Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openXfactory-3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 22 hours and 54 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap

Copy link
Copy Markdown
Contributor Author

READY (holder, manual landing) at 122bacc

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

Holder, at READY: T021 (U-2, the gate console's schemas), lane openXfactory-3's READY-CANDIDATE 6023469377.

  • Gates at the head:
    • 4 of 4 check-runs complete: validate, SonarCloud and Copilot succeed, and Sourcery is skipped;
    • the merge state is CLEAN against main d45a0939, which this ruleset requires;
    • 4 threads, all resolved;
    • 0 closing keywords in the body or commits.
  • The falsifier: the 17 nodes are red at main and green at the head. Composed, the count goes 103 → 86 red with 0 regressions, and the CI form reads 1113/1109/4.
  • Copilot r4199337845 ("Changes recommended" at 122bacce) is an ACCEPTED LIMIT under 5988818366. It is about test precision, not a bypass: one assertion in tests/test_gate_console_schema_source.py:257 pins jsonschema 4.26's failure mode for a cyclic object. The finding says the mutant stays covered either way. So this lands by the manual path.
  • Follow-on (lane 3's choice of carrier, blocking nothing): pyproject.toml allows jsonschema>=4.18 with no upper bound. A later jsonschema release could turn that assertion red in the required validate. The one-line remedy is the one Copilot names: assert only that nothing escapes except GateRefused, as the cyclic-list case already does. Apply it in a small PR after this lands, or in the next slice that edits this file.
  • Next: openXdox-code's ruleset is strict, so each queued PR (T026, U-7: the allow-list for 12.5's governed suites, with R-1 (a)'s span kind (plan 038, H-2) #43 T026, T095: the composed harness for the declared integration tests (plan 038, T094's R2-INV-R9) #46 T095, …) must merge main once this lands.

@brettheap
brettheap merged commit f70bc9c 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-06T19:07:21Z, PR #40 → f70bc9c (opensoft/openXdox-code main; plain gate)

T021 landed with Copilot r4199337845 an ACCEPTED LIMIT under 5988818366 (test precision on jsonschema's cyclic failure mode); its one-line remedy follows as a lane 3 follow-on. Queued openXdox-code PRs merge main next (strict ruleset).

brettheap added a commit that referenced this pull request Oct 6, 2026
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
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
…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
T021 landed as f70bc9c, the squash of #40, which this branch carried.
Its two files take main's side whole (src/openxdox/gate_console.py,
tests/test_gate_console_schema_source.py), and tests/conftest.py is main's
file plus T095's two hunks, byte for byte as at c2192e3.

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
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
…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 6, 2026
…38, T094's R2-INV-R9) (#46)

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

Arc: neutral-product-standalone-operability

## T095: the composed harness for the declared integration tests (plan 038; T094's map R2-INV-R9)

**Task.** T094's slice T095 (R2-INV-R9, "Slice outline"): the composed conftest. It owns `tests/conftest.py` (chain T020 → T021 → **T095** → T073 → T074) and `tests/test_host_plane.py` (T020 → **T095**). Lane openxfactory-4 records the slice in plan 038's `tasks.md` after T002 (#1251). Until then the contract is the outline row plus the holder's rulings below. After: T021 (#40), LANDED as `f70bc9c3`.

**Realizes.** HR, SR, CS and LI from the map, in `tests/conftest.py`, with their guard in `tests/test_host_plane.py`. It completes requirement 9's declared composed tests (T073, F9.1), so this PR carries the arc's line.

**Falsifier.** The 119 nodes the outline names (HR 105, HR+CS 4, CS 5, SR 1, LI 4), run composed, each file alone, must be red at the base and green at the head. 12.5's 16 governed suites, F5.2's 5 protected suites and `test_column_contributions_governed.py` (9 passed) must read unchanged, and `tests/test_declared_exclusion.py` must be green lone.

**Ruled.**
- openxFactory#656 comment 6021830531 (T094's rulings): OQ-R9-1, OQ-R9-2 and OQ-R9-4 at their defaults, and W2 (extend T086's Q4 (a) local-install fixture to `test_renderer.py` and `test_repo_root_guard.py`).
- openxFactory#656 comment 6022291206, the holder CONFIRMED T095's two departures from OQ-R9-1's default:
  1. T020's `plane_host_for` stays untouched. A separate autouse fixture registers the declared host inside T020's stand-in plane.
  2. `DECLARED_HOST_SUITES` (17) is guarded STRUCTURALLY: declared entries outside 12.5's set, and never `test_column_contributions_governed.py`. No text scan selects exactly the 17.

**Decisions.**
- **Every piece is composed-only.** Where `opendox_host` is absent, which is every lone checkout, nothing is registered, built or set. So a lone run, and the evidence `tests/test_declared_exclusion.py` holds each listed file to, are unchanged.
- **The edits are self-contained.** No T020, T021, T026 or T073 line is touched. The new block is appended after T021's. T086's launch-suite fixture changes in one line (W2).

### The interface (tests/conftest.py)

- **HR:**
  - `DECLARED_HOST_SUITES`, the 17 declared files;
  - `declared_host_for(suite)`;
  - the autouse fixture `_the_governed_host_for_the_declared_host_suites`. It requests T020's stand-in fixture, so the governed host (`governed_host()`) is the one registered for the test.
- **SR:** `HOST_SEAM_SUITES`, `HEALTH_CHECK_SEAM`, `BINDING_TRUST_SEAM`, `HOST_SEAM_HOLDERS`, `openxfactory_seams(wanted)` and `a_hosts_seam(module, call, what)`.
  - `test_authoring_agent.py` gets openxFactory's scoped health check, and `test_doxchat_model_intake.py` its binding-trust policy.
  - Both are read off `opendox_host.seams()` with `sys.path` put back, and an `openxdox` module from elsewhere refuses the case.
  - Each seam is put back after the test: a host's earlier registration again, openDox's default not.
- **CS:** `CONTRACTS_DIR`, a `Path` to `<farm>/contracts`, or None lone. It is built once at import, outside the checkout (a temporary directory removed at exit, and a temporary root inside the checkout is refused before anything is created), and exported as the `CONTRACTS_DIR` environment variable. The farm holds:
  - `contracts/schemas/`, 55 links: each carve row under `contracts/schemas/` through `carved_reach.sources_under`, then openxFactory's own;
  - `examples/ideation-dashboard/` beside it, 139 links: the three owners' YAML examples, less openDox's `opendox-snapshot-*`. Two owners naming one path is refused.

  The builders are `build_contracts_farm()` and `compose_the_contracts_farm()`. The reach must be the composed tree's own.
- **OQ-R9-4:** `openxfactory_root()`, the directory above `opendox_host.py`, or None lone.
- **LI (W2):** `LOCAL_INSTALL_SUITES` (`test_renderer.py`, `test_repo_root_guard.py`) and `runs_a_local_install(suite)`. T086's fixture now reads `runs_a_local_install`: the launch suite everywhere, as before, and the two suites only composed.

### The falsifier, measured

The composed form is R1Q23 (a):
- openXdox-code: the base is `main` `f70bc9c3` (T021 landed), and the head is this branch;
- openDox installed from openDox-code `main` `84f8ed83` (T025);
- openxFactory at `92010d3e` (#44's `tests/composed_host_pin.yaml`), with `openDox`, `openXdox` and `openXwallet` initialized recursively and its `scripts/` on `PYTHONPATH`;
- T022's one placement (the runbook link) made before each run and removed after;
- the colour variables unset, and `TERM=dumb`.

Each of the 66 declared entries ran alone, four at a time: `python -m pytest -q -p no:cacheprovider -rfE --junitxml=<file>.xml <file> < /dev/null`.

```
base f70bc9c: TOTAL over 66 files: 2001 passed, 496 failed, 72 errors, 2 skipped; 568 red
head c5d07c6: TOTAL over 66 files: 2120 passed, 377 failed, 72 errors, 2 skipped; 449 red
node for node (2571 nodes at both):
  the 119:  red -> passed 119  (HR 105, HR+CS 4, CS 5, SR 1, LI 4)
  green at base, red at head: 0
  any other node red -> passed: 0
head c6982e3 (adds only the in-checkout refusal and its case): the 17 falsifier files again,
  each alone: the 119 passed 119 of 119
```

**Unchanged, each line the same at base and head:**

```
12.5's 16:
tests/test_branch_session.py: 132 passed, 0 failed
tests/test_doxbench_mutation_boundary.py: 80 passed, 0 failed
tests/test_doxbench_share.py: 32 passed, 2 failed
tests/test_gate_loop_views.py: 74 passed, 0 failed
tests/test_gateway_provenance.py: 24 passed, 0 failed
tests/test_hosted_actor.py: 5 passed, 0 failed
tests/test_session_commits.py: 22 passed, 0 failed
tests/test_session_confinement.py: 50 passed, 3 failed
tests/test_session_gates.py: 78 passed, 3 failed
tests/test_session_lifecycle.py: 53 passed, 0 failed
tests/test_session_notebook.py: 39 passed, 0 failed
tests/test_session_records.py: 29 passed, 0 failed
tests/test_session_runbook.py: 11 passed, 0 failed
tests/test_session_transaction.py: 57 passed, 0 failed
tests/test_session_verbs.py: 60 passed, 5 failed
tests/test_staging_workbench.py: 54 passed, 29 failed
F5.2's 5:
tests/test_generator.py: 45 passed
tests/test_session_snapshot.py: 23 passed
tests/test_snapshot_determinism.py: 6 passed
tests/test_snapshot_registry.py: 40 passed
tests/test_snapshot_validation_launch.py: 9 passed
tests/test_column_contributions_governed.py: 9 passed, 0 failed (OQ-R9-1: off the declared-host list)
```

The governed reds are 12.5's own, node for node the same at both ends. T026 and the rulings own them.

**Lone:**
- `tests/test_declared_exclusion.py` in validate.yml's environment, the CI form: 207 passed.
- `tests/test_host_plane.py`: 28 passed lone and 28 composed at the head (T020's 9, plus 19 new); 9 composed at the base.
- The CI form of the whole suite at `c5d07c6b`: `selected=1131 passed=1127 skipped=4 failures=0 errors=0`. `c6982e34` adds one case. The floors are 1099 and 1095, and skipped is exactly 4. The floors are left alone, since only T029 raises them.

**Mutants: 35 of 35 killed.** Each is one exact replacement in a scratch clone, then restored.
- 33 were run at `70ed40c6`:
  - 30 are killed by `tests/test_host_plane.py` run lone:
    - **HR:** the composed condition dropped; membership dropped; the stand-in dependency dropped; `test_column_contributions_governed.py`, a governed suite, or an undeclared file added; the wrong parent; the fixture's check inverted.
    - **SR:** `sys.path` not put back; the elsewhere-refusal dropped; the exactly-one-row check weakened; the lone guard dropped; a host's earlier registration not restored; openDox's default restored; no unregister first; a governed suite added.
    - **CS:** `opendox-snapshot-*` kept; nested carve rows linked; a collision passed; openxFactory's own schemas dropped; examples not walked recursively; the environment not set; the reach check dropped; no removal at exit; `under` ignored; the lone guard dropped.
    - **LI:** the composed condition dropped; the launch suite dropped; T086's condition reverted; a non-`generate-and-open` suite added.
  - 3 are killed by a named node of the composed run: the seam fixture inverted; `CONTRACTS_DIR = None`; the declared host never registered.
  - The 30 lone mutants were re-run at `c2192e3e`, and 10 were spot-checked at `c5d07c6b`.
- 2 more at `c6982e34`: the in-checkout check dropped, and TMPDIR ignored.

### Review

- **CI at `c6982e34`:** `validate` passed (run 37540071853); SonarCloud's Quality Gate passed with 0 new issues. At `70ed40c6` SonarCloud read 5 smells in `tests/test_host_plane.py`, which `c2192e3e` fixed. Its sixth was in T021's file, now on `main`.
- **Copilot:**
  - At `c5d07c6b`: "Changes recommended", 1 finding, r4200978505 (a temporary root inside the checkout would build the farm in the working tree). FIX-NOW: `c6982e34` refuses it before anything is created, with a new case and 2 mutants. The thread is answered and resolved.
  - At `c6982e34`: COMMENTED, "Findings: None".
  - Threads: 1, unresolved 0.
  - At `c2192e3e` an earlier review listed one "previously missed" item in T021's `src/openxdox/gate_console.py`, which is on `main` and outside this PR's diff.

### Files changed

- `tests/conftest.py`
- `tests/test_host_plane.py`

### Not done

- Nothing in this slice. The other slices' nodes in these files stay red until those slices land: T096 to T103 code against this interface, and T104 rides the openDox pin.
- The EN node, `test_repo_root_guard.py`'s refusal-path case, is red under a local TMPDIR in the home directory only, and green on a runner.

🤖 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
Brings T024 (#38), T023 (#42), T021 (#40), T026 (#43) and T095 (#46). In
tests/conftest.py, T095's composed harness and T073's rail block were both
appended after T021's lines. Both are kept, T095's as main has it and
T073's after it, so this branch's diff against main is a pure append there.

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
…; T094's map R2-INV-R9) (#48)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (#656 6021862323)
Arc: neutral-product-standalone-operability

## T101 (plan 038): the doxBench texts

One of the R9 repair slices, T095 to T104, which repair every composed red outside 12.5's set in this leg's declared exclusion, so that T073's composed step 5a goes green (requirement 9's declared composed tests, T073, F9.1).

**Contract.** There is no `tasks.md` entry yet: lane 4 records T095 to T104 after T002 (#1251). The contract is T101's row in T094's map, `R2-INV-R9.md` § "Slice outline" (#656 `6021830607`), plus the holder's rulings:
- `6021830531`: W5, drop the manifest home; W6, rewrite over both owners.
- `6022291206`: T101's Files gain `tests/declared_exclusion.yaml` for one entry only, `test_doxbench_blank_reason.py`.

- **Realizes:** T094's row T101: PP 25 (threads 21, thread_wiring 2, knowledge_service 2), CP 6 (thread_wiring 1, transport 1, request_handling 1, doxchat 3), CS constants 5 (knowledge_service 2, doxchat 3) and HH 1, so 37 in all; plus 1 on W5 and 1 on W6.
- **Falsifier:** these files composed, each file alone, red at the base and green at the head; plus the unchanged set.
- **After:** T095 (#46) for `CONTRACTS_DIR` and `openxfactory_root()`, LANDED as `e2340bb6`; T021 (#40), LANDED as `f70bc9c3`. Met.

## What changes (test text only; no `src/` change)

| means | where | what |
|---|---|---|
| PP | `test_doxbench_threads.py` (21), `test_doxbench_thread_wiring.py` (2), `test_doxbench_knowledge_service.py` (2) | Pre-carve `scripts/ideation_dashboard/` reads respelled to their carved homes, through the conftest's `carved_module_path` (plan 034 T040's resolver). The push sweep also reads its three openxFactory-only modules (`nightly_lane.py`, `doxbench_status_exemption.py`, `serve_openxfactory_lanes.py`) from the composed tree, `openxfactory_root()`. Each swept module is read from **exactly one** of its three homes; a module in two or none refuses the sweep by name. |
| CP | `test_doxbench_thread_wiring.py` | Liveness is now asked through the scope seam (T084, openDox-code `e49b17c`): `self._is_live_session_ref(` → `column_seams.scope.current().is_live_session_ref(`. openXdox's registration there IS `doxbench_scope` (`column_contributions.SCOPE is doxbench_scope`). `live_session_branches(` stays absent. |
| CP | `test_doxbench_request_handling.py` | `compute_capabilities` for the local human is asked with this leg's own gate routes (`GateRoutesExtension().routes()`), since T084 makes `gate` true only where a contributed binding answers `POST /actions/gate/<verb>`. The pinned dict is unchanged. |
| CP | `test_doxbench_transport.py` | The save seam is re-pinned at its rulings' placements. Q3 (`5642758731`): the route is in `swb-session.js`, not openDox's model. Q10 (`5648065587`), S5 (openDox-code `8efb3cf`): no static import in `app.js`; one `.session.firstEditTransport(` call site with the `refusalTransport()` fallback; and `firstEditTransport` declared in the `gate.workbench.session` binding's `exports` (`VIEW_BINDING_SPECS`). This was the map's one INFERRED node, now measured. |
| CP | `test_doxchat_model_intake.py` (3) | Both auth-kind pins read the broker-enrolled kinds, every member but `none` (T080, openDox-code `1130e99`; #1144 16.3). The approval case, after approving, reads the model listed and NOT yet available. It then records this machine's trust as `opendox model-binding trust` does (`doxbench_trust.recording_for`, through the case's own `stand_in_host_trust`), and reads a fresh catalog in which the model is available (T100, openDox-code `38d3350`; #1144 16.3a). A factory takes its verdict when it is built, so each catalog comes from a factory built at that moment. |
| CS | `test_doxbench_knowledge_service.py` (2), `test_doxchat_model_intake.py` (3) | The chat-turn and gate-action-record schemas are read from T095's farm, `CONTRACTS_DIR`. |
| HH | `test_doxbench_transport.py` | The raw client sends ONE `Host` line (`skip_host=True`). openDox's loopback Host guard (openDox-code `390e2c2`) refuses a second one as `403 invalid_host`. |
| W5 | `test_doxbench_blank_reason.py` | The manifest home is dropped; the rule stays stated in its other three homes. |
| W5 | `tests/declared_exclusion.yaml` | One entry: `test_doxbench_blank_reason.py` → `reasons: [doc_health]`, and its note is dropped. `count` stays 66, and `openxfactory-contracts` keeps its four contracts-only entries. That reason carries no `only` list at `main`, so nothing moves there. |
| W6 | `test_doxchat_model_intake.py` | The v1.45 release test is read over both owners in the composed tree. Each row is in its owner's manifest and in no other of the three: `gate-action-record` in the openXdox root's, `xfactory-workbench-chat-turn` in the openDox root's. `release_member` and `contract_schema_version: 1` hold, and the digest is recomputed from the spec leg's bytes. The immutable half, the CHANGELOG entry and the digest inventory, is read from openxFactory's tree. The two `consumption_rule` asserts left with openxFactory's rows at contract-v4.0. |

## Falsifier (measured; quoted)

**Composed form, R1Q23 (a):**
- openxFactory `92010d3e` (`tests/composed_host_pin.yaml` in #44), with `openDox`, `openXdox` and `openXwallet` initialized recursively and its `scripts/` on `PYTHONPATH`.
- openDox installed from openDox-code `main` `84f8ed83`.
- The colour variables unset, and `TERM=dumb`.
- Each file alone: `python -m pytest -q -p no:cacheprovider -rfE --junitxml=… tests/<file>` from the checkout root.
- T022's placement is immaterial here: it places only the session runbook, which none of these files reads.

**Base:** `main` `e2340bb6` (T095 landed). **Head:** `6f157db7`, this PR: one change commit, the Copilot fix `2fdbd96`, and merges of `main`. Measured 23:34Z to 23:43Z.

| file | base | head |
|---|---|---|
| `test_doxbench_threads.py` | 78 passed, **21 failed** | 99 passed, 0 failed |
| `test_doxbench_thread_wiring.py` | 47 passed, **3 failed** | 50 passed, 0 failed |
| `test_doxbench_knowledge_service.py` | 47 passed, **4 failed**, 1 skipped | 51 passed, 0 failed, 1 skipped |
| `test_doxbench_transport.py` | 27 passed, **2 failed**, 1 skipped | 29 passed, 0 failed, 1 skipped |
| `test_doxbench_request_handling.py` | 45 passed, **1 failed** | 46 passed, 0 failed |
| `test_doxchat_model_intake.py` | 36 passed, **7 failed** | 43 passed, 0 failed |
| `test_doxbench_blank_reason.py` | 119 passed, **1 failed** | 120 passed, 0 failed |

- **39 nodes go red → green:** the row's 37, plus W5 1 and W6 1. **0 nodes turn red.**
- Earlier, measured at `main` alone (`8b64fae0`, before T095), the main-measurable part read 49 → 17 over these files. The 17 were T095's HR 7, T021's 3, CS 5 and the approval node.
- **The approval node does not need T095's binding-trust seam.** Over a scratch integration of this change with #40 and #46, with `HOST_SEAM_SUITES` emptied by a throwaway plugin, it passes, and `test_doxchat_model_intake.py` reads 43 passed. Its own `stand_in_host_trust` carries the trust.

**The unchanged set, base vs head, same form, node for node (0 newly red):**
- **12.5's 16 governed suites** (`git grep -l -e open-pr -e open_pr -e FakePullRequests -- 'tests/test_*.py'`): identical per node. The reds that remain are other slices', and none is changed.
- **F5.2's files** (`test_generator.py`, `test_snapshot*.py`, `test_session_snapshot.py`; 7 files): all green at both.
- **`test_column_contributions_governed.py`:** 9 passed at both.
- **Over the 31 files:** 49 red at the base, 10 at the head (all in 12.5's governed suites, unchanged), 39 fixed (all in T101's seven), 0 newly red.

**Lone, `tests/test_declared_exclusion.py`** (CI form: `pip install -e ".[test]"`, openDox `dede32b4` from the pin), at `6f157db7`: **207 cases, 0 red**, inside the whole-suite run below. The six other files still fail lone at collection on `doc_health`, unchanged.

## Mutants (14 of 14 killed)

Measured over a scratch integration of this change with #40 and #46. Each fault was planted in the subject, its node run composed over the integration, and everything restored and digest-checked.
- **Push sweep:** a push token in openxFactory's `nightly_lane.py` → `('nightly_lane.py', 'git push')`. `nightly_lane.py` planted at this leg's `src/openxdox/` too → "expected in exactly one of …". The resolver without openxFactory's home (test mutant) → the same.
- **Liveness:** the serve asking `doxbench_scope` directly → the seam pin fails. A shape-valid copy of the question registered as the scope → the identity pin fails.
- **Save seam:** a static `swb-session.js` import back in `app.js`; `firstEditTransport` dropped from the binding's `exports`; the route constant back in openDox's model; the `refusalTransport()` fallback gone. Each fails the test.
- **W6:** a second manifest claiming `gate-action-record` → `('gate-action-record', ['openXdox', 'openDox'])`. A drifted byte in the spec leg's schema → `gate-action-record`. A drifted recorded digest → `xfactory-workbench-chat-turn`.
- **Approval:** the catalog read off the pre-trust factory (test mutant) → `assert False is True`. The trust never recorded → the same.

## CI

- **`validate` at `6f157db7`: success** (run 37548131461). SonarCloud: success.
- CI form locally at `6f157db7`: selected=1165 passed=1161 skipped=4 failures=0 errors=0, above `validate.yml`'s floors 1099/1095/4. The floors are untouched.
- **Copilot at `6f157db7`:** COMMENTED, "Needs a closer look", Findings: None. One thread in all, r4199422320 (Low, the release test's summary), fixed in `2fdbd96` and resolved: 0 unresolved.

## Files (8)

`tests/test_doxbench_threads.py`, `tests/test_doxbench_thread_wiring.py`, `tests/test_doxbench_knowledge_service.py`, `tests/test_doxbench_transport.py`, `tests/test_doxbench_request_handling.py`, `tests/test_doxchat_model_intake.py`, `tests/test_doxbench_blank_reason.py`. The eighth, `tests/declared_exclusion.yaml`, is one entry only (holder `6022291206`). Its chain is now T101 → T073 → T074; T073 (#45) edits only the header and `reasons` block, a different hunk.

None of these is in 12.5's computed 16 or in F5.2's set.

## Not done

- Nothing owed by this slice.
- **Left as is, stale but harmless:** `tests/test_declared_exclusion.py`'s docstring (`:28-30`) and comment (`:261`) still name `test_doxbench_blank_reason.py` as a two-reason entry, and `_CONTRACT_FILES` keeps the checkout's `contracts/manifest.yaml` with its evidence case. That file is not T101's.

🤖 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 to opensoft/openxFactory that referenced this pull request Oct 7, 2026
…e identity response shape and the single-writer chains (plan 038) (#1257)

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

**T002, second bookkeeping batch, plan 038: T094 and the slice entries T095 to T104, the identity response shape, the single-writer chains, and the ticks.** Bookkeeping, so this PR carries no `Arc:` line. It touches no `openspec/changes/` path, so no Rule 6 window applies. It began stacked on #1251's head; #1251 has landed (`1837ea75`) and `main` is merged in (`543f6ce1`), so the diff against `main` is this batch alone.

## From plan 038's `tasks.md`, T002

- **Task**: T002, "Release-2 base, and the re-measure at the ruling" (landed in #1251; this is its second batch of bookkeeping, the brief of the holder's `T002b`).
- **Realizes**: none (bookkeeping; FR-025). **Falsifier**: T002 names none. Its gates are the brief's, quoted under "Review and gates".
- **Ruled / Decisions**: none of its own. Every item is a ruling already posted on `#656`, or a landing already merged, encoded as worded and cited by comment id or merge commit.

## The items

| # | Where | What changed | Record |
|---|---|---|---|
| 1 | tasks.md: T094, T095 to T104, T073 | T094 ticked, with its result quoted: 436 composed reds over 51 files, outside 12.5's set (Base 2; 533 at Base 1). The `T095+` placeholder becomes ten entries, T095 to T104, each with its slice name, Files, After and Falsifier from R2-INV-R9's outline and the holder's rulings, `Lane: 3 (openXfactory-3)`. T073's After and Files name them. The map's means, the rulings (R9-R1 (a), R9-R2 (a), W1 to W7) and the OQ defaults are recorded once, above the entries | map `R2-INV-R9.md`; `6021830607`; claim `6021862323`; rulings `6021830531` |
| 2 | T095, T101, T104, T097 | T095: the two confirmed departures from OQ-R9-1 (`plane_host_for` untouched, an autouse fixture registers the declared host; `DECLARED_HOST_SUITES` guarded structurally). T101: `tests/declared_exclusion.yaml` for ONE entry (reasons `[doc_health]`, note dropped, the `only` list loses the file, count stays 66). T104: `tests/test_binding_stylesheets.py`, the pin 572 to 574. T097: its served-display respelling is R9-R1 (a) applied | `6022291206`; the T097 line is `6023517122` (the holder confirmed the citation, `6026622117`) |
| 3 | plan.md: the single-writer table; tasks.md Files and After lines | W7's chains: `tests/conftest.py` T020, then T021 and T026 in landing order, then T095, T073, T074; `tests/test_host_plane.py` T020, T026, T095; `declared_exclusion.yaml` T101, T073, T074; the four T074 test files T103 then T074; the census T025, T104, T015, T016, T057 (the row splits, so `test_web_boundary.py` keeps its own chain). T015's and T074's After gain T104 and T103 | `6021830531`, `6022291206` |
| 4 | tasks.md: T072, T074, T075 falsifiers | The four folds of #1253, in the arc change's own wording: T072 asserts that ONLY CR, LF and CRLF split rows; T074's seam tests assert that an empty-seam read names the seam AND its registrar; T075's host-wiring test shows a leg declaring only SOME of the four seams refused, and a leg declaring the six and none of the four accepted; T074's protected-suite oracle computes 12.5's set as 12.5 does (`git grep -l -e 'open-pr' -e 'open_pr' -e 'FakePullRequests'`), from the pre-arc and the final tree, the floor of 16 on each, and takes the union | openxFactory#1253 to `fc4fa0ff`; Copilot `5427373153` |
| 5 | spec.md FR-011; tasks.md T041 | FR-011's JSON shape gains `identity`, emitted and bounded as openDox-spec's finding schema bounds it (no `excerpt`, `text`, `content` or `quote` key, no number, strings of at most 200 characters, `{category, entry}` for a pathless finding, `{collided_id}` for a collision). T041's falsifier gains a response-shape test asserting exactly that bound. The coverage row for FR-011 names T041. The serialized-size cap was already T041's at the base (T041's text says so, and T042 "stores what T041 bounds"), so nothing moved | holder `6018624750` |
| 6 | tasks.md ticks | T011, T014, T021, T023, T025, T026 and T060 ticked, T070's tick gains #1253, and T002's own landing is recorded. Each merge commit was verified with `gh pr view <n> -R <repo> --json mergeCommit` (listed below) | the landings |
| 7 | tasks.md: T072 | A Files line (`src/opendox/lines.py`, `tests/test_lines.py`); the Ruled line gains `6023517122`: `head_sha` (a), its own public `head_sha` parity-tested against `serve._head_of`, `serve.py` untouched; the line split (a), `doxbench_defaults.py` untouched. Folding either private helper is recorded as unscheduled and optional, and is not scheduled | holder `6023517122` |
| 8 | contracts/health-finding.md:91 | The sentence reads: a stored `identity` makes a cross-run collision between STORED findings detectable; an exception-only finding is never stored; whether the engine reports it remains T046's concern. Nothing else on the line changed | holder `6018624750`; Copilot `5433292692` |
| 9 | plan.md; tasks.md | Copilot's three items at #1251's head `1eb8e9eb`, accepted by the holder at its landing: (a) the pin and landing-order row for T073 names T005 and T095 to T104 as prerequisites, and `T095+` reads T095 to T104 at the critical path, the lane table and the revision note; (b) the combined `conftest.py` and `test_host_plane.py` row splits, and the cross-lane summary gains T026 (and the other chains of item 3); (c) the same split in T020's, T021's and T026's Files lines | holder replies `4201056969`, `4201057311`, `4201057577`; READY note `6026651962` |

### The ticks, verified

| task | pull request | merge commit | merged |
|---|---|---|---|
| T011 | opensoft/openDox-code#92 | `09b8d44a` | 2026-10-06T19:15:55Z |
| T014 | opensoft/openDox-code#93 | `a7a1c4d3` | 2026-10-06T22:23:06Z |
| T021 | opensoft/openXdox-code#40 | `f70bc9c3` | 2026-10-06T19:07:10Z |
| T023 | opensoft/openXdox-code#42 | `d45a0939` | 2026-10-06T17:45:33Z |
| T025 | opensoft/openDox-code#91 | `84f8ed83` | 2026-10-06T17:33:31Z |
| T026 | opensoft/openXdox-code#43 | `36bbef79` | 2026-10-06T22:35:40Z |
| T060 | opensoft/openDox#20, the tag `dox-v1.2` (tag object `58538ff3`, on `6a9f4902`), opensoft/openDox#21 | `6a9f4902`, `c7f75845` | 17:46:59Z, tag 17:58:45Z, 19:02:39Z |
| T070 | openxFactory#1253 (T070b, added to its tick) | `fc4fa0ff` | 2026-10-06T18:56:42Z |
| T002 | openxFactory#1251 (its own landing) | `1837ea75` | 2026-10-06T22:31:42Z |

- **T021** landed with Copilot r4199337845 an ACCEPTED LIMIT (holder READY note `6023492215`); T021's entry records it.
- **T025's** entry says its 34 nodes are "quoted at T029"; that stays T029's, and the entry says so.
- **T011's** server node is T014's, as its own Falsifier says.

**Skipped, because already present at the base:** T020's and T024's ticks (#1251 item 13) and T004's tick (#1245, `260234b5`), all confirmed with `git show <commit>:<tasks.md> | grep`.

**T071 is NOT ticked.** Brett's word, "Ratify it (Recommended)" (`#656` `6023375303`), is recorded on T071's entry, but the change's ratification-record PR has not landed (no such PR is open or merged on `opensoft/openxFactory` at this writing). The box stays open.

## Readings the holder accepted (`6026622117`), disclosed as the brief requires

- **W7 omits T026 from the openXdox-code chains.** The single-writer table keeps T026 where its own ruling (`6016648451`) puts it, and lane 3's claim (`6021862323`) says T026's PR lands before T095. So the conftest chain reads T020, then T021 and T026 in landing order, then T095, T073, T074; and `test_host_plane.py` reads T020, T026, T095, as W7 gives it with T026 added.
- **R9-R2's last sentence** ("If the re-scoped test runs alone, it leaves the declaration, and the count moves with it, as the declaration's own rules require") is recorded in T100 as worded. `declared_exclusion.yaml` is NOT added to T100's Files: the holder's file list at `6022291206` names it for T101 only.
- **Edits outside tasks.md**, ruled in scope as each item names its place: plan.md (items 3, 9), spec.md FR-011 (item 5) and `contracts/health-finding.md:91` (item 8).

## Other notes on where an item needed a reading

- **Counting.** The task count reads 90 rows: the placeholder `T095+` is replaced by T095 to T104 (81 - 1 + 10).
- **Plan 034's task numbers.** The entries cite plan 034's T103 (#80, in T099's `Host` header) and plan 034's T086 (W2, in the rulings) as "plan 034's", because plan 038 now has its own T103.
- **OQ-R9-3.** The map did not measure the respelled gate-console and wheel-model skips; T097 and T103 say so, and the map's 127 passed and 5 skipped is quoted as the measurement.
- **Counts of slices** are copied in the map's own form (for example "16 nodes, and +5 on R9-R1 (a)"), not summed.
- **T095's chain after T026.** T095's After names T021 and T026, both landed.
- **Not mirrored from the arc's 4.1:** T075's falsifier gains the some-of-four refusal and the six-and-none acceptance, which #1253 folded. The arc's two older refusals (part-way through the seams; at the column call) are not in T075, since #1253 did not add them.
- **T046 and T057** carry no response-shape falsifier for `identity`: item 5 names T041's.

## Review and gates

- **`python3 scripts/validate-openspec-cli-pin.py --all`**, in the clone named `openxFactory`, at `691648c6`: exit 0, `Totals: 113 passed, 1 failed (114 items)`, `0 UNDISPOSITIONED failures`. The one failure is the dispositioned `add-chain-attestation` finding, the same on `main`.
- **openxFactory pytest.** The full local run was stopped at about 44% at a coordinator drain, so it is NOT a result and is not claimed. The repository's own gate, `pytest-suite` in CI at `691648c6`, completed `SUCCESS` (2026-10-06T23:32:38Z to 23:58:56Z).
- **Doc-health**, `scripts/doc-health.py --single-repo . --as-of 2026-10-07`, branch `691648c6` against `origin/main` `0992369a` in the same clone: the two reports are byte-identical (sha256 `ce17aadd6d96e484170e2ec4784e44972e33c7d1a6c65830ea14a1035f2ae976`; `Findings: 31 critical, 26 error, 69 warning, 20 info. New regressions vs previous report: 0.`). So no new error.
- **CI at `691648c6`**: all 14 check runs are `SUCCESS` (rollup `SUCCESS`): `pytest-suite`, `merge-master-approval` (the red inherited from `main` since #1252 was cleared by #1138), `openspec-cli-pin`, `doc-health-py314`, `openxdox-consumer-gate`, `wallet-validation`, `clearing-dispatch-gate`, `signed-execution-chain-gate`, `former-id-arrival-gate`, `release-tag-gate`, `openreposhape-pin`, the two `lane-line` runs and SonarCloud.
- **`main`** has moved to `0992369a` since the last merge (`543f6ce1`). GitHub reports this PR `MERGEABLE`, and the ruleset is not strict, so `main` is not merged in again (a merge would restart the 27-minute gate).

| Copilot at | verdict | findings | answered |
|---|---|---|---|
| `691648c6` (final; review `5435686665`) | Changes recommended | 1 (thread `4201443690`): T041's falsifier cannot verify what T046's `health list --json` and T057's HTTP route emit; the review summary adds that the new T095 to T104 collide with plan 034's task numbers cited unqualified at `spec.md:662`, `spec.md:1222` and `clarify-questions.md:437` | accepted limit (#656 `5988818366`): plan-record accuracy, not a bypass, and the T041 placement is the holder's own instruction (item 5). Answered (`4201487553`) and resolved |

1 thread, 0 unresolved. The holder ruled both points YES at `6027706377` and has them encoded in a THIRD batch, not here: the `identity` falsifier split across T041, T046 and T057, and the plan 034 references qualified. This PR therefore carries T041's wording as the brief gave it.

## Files changed

- `specs/038-opendox-document-tool-self-maintenance/tasks.md`, `plan.md`, `spec.md`
- `specs/038-opendox-document-tool-self-maintenance/contracts/health-finding.md`

## What is NOT done

- T071 stays open (its ratification record has not landed). No box of #1144 is ticked, and no task other than those listed is ticked.
- No scenario or spec delta beyond FR-011's JSON-shape bullet is touched, and nothing is under `openspec/changes/`. `README.md` is untouched.
- The T095 to T104 entries record the slices; none of their work is done here.

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


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
…and the composed tree (plan 038, T094's R2-INV-R9) (#49)

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

Arc: neutral-product-standalone-operability

## T103: the T074 files (plan 038; T094's map R2-INV-R9, slice outline row T103)

There is no `tasks.md` entry yet: lane 4 records T094's slices after T002 (openxFactory#1251). Until then, the contract is the slice outline row plus the holder's rulings, openxFactory#656 `6021830531`.

- **Realizes:** the composed half of requirement 9 for these four declared files (T073's F9.1: each declared file runs green in the composed workflow). The means are PP, plus MN, HR and CS for `test_gate_console.py`.
- **Falsifier (the outline row):** run the four files composed, each alone. **28 reds go to 0**: completeness 2, packet 1, gate_console 1, round_trip 24. `test_gate_console.py` then reads 127 passed and 5 skipped. Its 5 skips are OQ-R9-3, which this slice respells so they run composed: it reads **132 passed, 0 skipped**.
- **Ruled:** openxFactory#656 `6021830531`:
  - OQ-R9-1 to OQ-R9-4 at their defaults;
  - W7: each of these four files' chain is **T103 → T074**, so this lands before T074.
  - The ruling it sits under is `6016982816` (ARC-Q2 (a)).
- **After:** T095 (#46, landed as `e2340bb6`), which this PR codes against: `openxfactory_root()`, `CONTRACTS_DIR` and `DECLARED_HOST_SUITES`. T021 (#40) landed as `f70bc9c3`. Both are met.

## What changed (4 test files, nothing else)

| file | nodes | means | change |
|---|---|---|---|
| `tests/test_completeness.py` | 2 | MN, PP | The purity check reads `completeness.py` at its carved home (`carved_module_path`), and `doc_health.lines` where this process imports it from. The gating bound scans this leg's import root `src/` and expects `openxdox/generator.py` and `openxdox/kickoff.py`. Its `doc_health` half scans the `doc_health` package this process imports. |
| `tests/test_round_trip.py` | 24 | PP | `_MOVER` reads openxFactory's `scripts/proposal-support.py` through `openxfactory_root()` (23 nodes). The shared-predicate pin reads `round_trip.py` at its carved home and `families.py` from `families.__file__` (1 node). |
| `tests/test_doxbench_packet.py` | 1, the rail node | PP | The status-read drift check reads openxFactory's corpus, `openxfactory_root() / "docs"`, instead of this checkout's `docs/`. |
| `tests/test_gate_console.py` | 1 collection error, and the 3 cases behind it | PP, MN, HR, CS | The module-level forward-gate load goes through `openxfactory_root()`. The two in-test `from ideation_dashboard import cli` imports become `from opendox import cli`. HR (the two cases that parse `gate`) is T095's `DECLARED_HOST_SUITES`, and CS (the receipt case) is T095's `CONTRACTS_DIR`; neither needs an edit in this file. |
| `tests/test_gate_console.py`, OQ-R9-3 | 5 skips | CS | `_contract_carries_the_new_verbs` reads each schema where the pinned validator reads it. `gate-action-record` comes from the installed distribution's packaged copy (`openxdox.contracts.verified_path`). `gate-intent` comes from the directory `CONTRACTS_DIR` names. A lone checkout has no `CONTRACTS_DIR`, so the group still skips there. Once `CONTRACTS_DIR` is supplied, a schema that cannot be read fails collection instead of reading as "not released" (Copilot r4199436303). |

Edits are kept local to the failing reads, so T074's later respelling of these files' `doc_health` imports stays clean. The two `doc_health` reads in completeness and round_trip resolve through the import system, so they follow whatever T074 makes those imports.

**Flagged, a widening beyond the failing assertion:** `test_the_scoring_modules_only_importers_are_the_generator_and_the_guard`'s second loop scanned `REPO_ROOT / "scripts" / "doc_health"`. This leg has no such directory, so since the carve the loop has scanned nothing and passed vacuously. It now scans the composed `doc_health` package, 38 modules, and passes. Mutant M2b below shows the check is live.

## The falsifier, measured

Measured in R1Q23 (a)'s composed form:
- openxFactory `92010d3e` (#44's `tests/composed_host_pin.yaml`), with `openDox`, `openXdox` and `openXwallet` initialized recursively, and its `scripts/` on `PYTHONPATH`;
- openDox installed from openDox-code `main` `84f8ed83`;
- colour variables unset, `TERM=dumb`;
- each file ALONE: `python -m pytest -q -p no:cacheprovider -rfE --junitxml=… <file>`.

The integration branches are LOCAL and never pushed:
- **base** = `main` `e2340bb6`, which carries T021 (#40) and T095 (#46), both landed;
- **head** = this PR at `683b9562`, which is `main` merged in and differs from it by exactly these four files.

Earlier runs over local integrations read the same, file for file: `main` `d45a0939` + #40 + #46 at `6aeb6797`, and `main` `f70bc9c3` + #46 `c2192e3e` at `57a970fc`.

The rail node needs T073's status-exemption rail. Without it, the node fails on `pk.STATUS_SCAN_LINES`, at the rail, before it reaches its corpus read. So the four files are also measured with #45 `26fe63e7` merged in, with T022's placement made first and removed after.

**Base + #45, 28 red:**
```
tests/test_completeness.py: exit 1; 29 passed, 2 failed, 0 errors, 0 skipped
tests/test_doxbench_packet.py: exit 1; 85 passed, 1 failed, 0 errors, 0 skipped
tests/test_gate_console.py: exit 2; 0 passed, 0 failed, 1 errors, 0 skipped
tests/test_round_trip.py: exit 1; 50 passed, 24 failed, 0 errors, 0 skipped
TOTAL over 4 files: 164 passed, 27 failed, 1 errors, 0 skipped; 28 red
```

**Head + #45, 0 red:**
```
tests/test_completeness.py: exit 0; 31 passed, 0 failed, 0 errors, 0 skipped
tests/test_doxbench_packet.py: exit 0; 86 passed, 0 failed, 0 errors, 0 skipped
tests/test_gate_console.py: exit 0; 132 passed, 0 failed, 0 errors, 0 skipped
tests/test_round_trip.py: exit 0; 74 passed, 0 failed, 0 errors, 0 skipped
TOTAL over 4 files: 323 passed, 0 failed, 0 errors, 0 skipped; 0 red
```

The 28 red at base, each with its first failure:
- `test_completeness.py`: `test_the_scoring_module_touches_nothing_outside_its_arguments` (FileNotFoundError, `scripts/ideation_dashboard/completeness.py`); `test_the_scoring_modules_only_importers_are_the_generator_and_the_guard` (`AssertionError: []`).
- `test_doxbench_packet.py::test_the_status_read_agrees_with_the_repositorys_own_corpus_reader`: "the drift check needs real documents to be a check".
- `test_gate_console.py`: a collection error (FileNotFoundError, `scripts/proposal-support.py`).
- `test_round_trip.py`:
  - `test_both_gates_agree_where_a_documents_status_header_is` ×12, `test_the_mover_agrees_with_the_other_fence_implementations` ×10 and `test_the_movers_line_split_is_the_three_real_endings_and_nothing_else`: FileNotFoundError, `scripts/proposal-support.py`;
  - `test_the_shared_predicate_is_spelled_the_same_in_all_three`: FileNotFoundError, `scripts/ideation_dashboard/round_trip.py`.

**Without #45**, base vs head reads completeness 2→0, round_trip 24→0 and gate_console 1→0 (132 passed). Packet reads 57 red at both: its 56 rail nodes and the rail node all fail at the rail, which is T073's.

The two "rejected" conformance cases are rejected by the schema (validator exit 1), not by a harness failure (exit 2). Run by hand with the farm:
- a valid `promote-to-staging` record: exit 0;
- one with no workflow job: exit 1, "2 error(s)";
- a `derive-possibles` record with no `cluster_id`: exit 1, "1 error(s)".

## Mutants of the respellings

This slice adds no guard and no fixture. Each mutant reverts or misdirects one respelled read; every one was run with that file alone, composed, over head + #45.

| id | mutation | result |
|---|---|---|
| M1 | purity check reads `generator.py`, not `completeness.py` | killed: 1 failed (`AssertionError` on the import set) |
| M2b | the `doc_health` loop scans `src/` instead | killed: 1 failed (the loop's assertion names `completeness.py`) |
| M3 | importer scan back to `REPO_ROOT / "scripts"` | killed: 1 failed (`AssertionError: []`) |
| M4 | shared-predicate pin reads `gate_console.py` | killed: 1 failed |
| M5 | rail node back to `REPO_ROOT / "docs"` | killed: 1 failed ("the drift check needs real documents to be a check") |
| M6 | conformance group: `CONTRACTS_DIR` test inverted | 127 passed, **5 skipped**: the group reverts to never running |
| M7 | conformance group reads a schema without the verbs | 127 passed, **5 skipped**: the skip condition reads the schema's content |
| M8 | **a broken farm**: a pytest plugin removes `gate-intent.schema.yaml` from the farm after the conftest builds it | at `6aeb6797`, before the fix: 127 passed, 5 skipped, so it **survived** (the masking Copilot r4199436303 named). At `57a970fc`: a collection error (`FileNotFoundError`), so it is **killed** |

## Unchanged

Base (`main` `e2340bb6`) vs head (`683b9562`), each file alone and composed: **974 nodes, 0 differ**, and no node goes from green to red anywhere.
- **12.5's 16 governed suites:** identical at both, node for node. Their remaining reds are T073's and the other slices', not this PR's.
- **F5.2's 5 protected files:** generator 45, session_snapshot 23, snapshot_determinism 6, snapshot_registry 40 and snapshot_validation_launch 9, all passed at both.
- **`test_column_contributions_governed.py`:** 9 passed at both.
- **`tests/test_declared_exclusion.py`, lone** (no `PYTHONPATH`): 207 passed at base and at head.
- None of the four files is in 12.5's computed set (16) or F5.2's (7), so no allow-list entry is needed.

## CI

T095 has landed, so this head carries `openxfactory_root` and `CONTRACTS_DIR` from `main`. Locally, in validate's lone form at `683b9562` (pinned openDox `dede32b4`, `--deselect` of LEFT_OUT): `selected=1165 passed=1161 skipped=4 failures=0 errors=0`, against the floors 1099 / 1095 / 4. `tests/test_declared_exclusion.py` run alone reads 207 passed at base and at head.

**CI at `683b9562`:**
- `validate` passed (run 37548600589): `selected=1165 passed=1161 skipped=4 failures=0 errors=0`, against the floors 1099 / 1095 / 4 (margin 66);
- SonarCloud Code Analysis passed;
- Copilot's review 5435805891 at `683b9562`: **Findings: None**. It rates the PR "Needs a closer look" only because the PR was a draft and the composed packet result depends on T073's rail, which lands separately.

**Threads:** 1 in all (r4199436303, fixed at `57a970fc`, answered and resolved), 0 unresolved.

## Not done / what remains

- Nothing is left on this PR's own After set: T021 (#40) and T095 (#46) have landed, and this branch merges `main` `e2340bb6`. If `main` moves again, the holder's loop updates the branch.
- The rail node's composed green also needs T073 (#45), which lands after this slice. Until then it is measured over a local integration with #45, as above.
- T074 later respells these files' `doc_health` imports; this PR leaves them alone.

🤖 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
…n is composed (plan 038, T094's R2-INV-R9, CS) (#53)

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

Arc: neutral-product-standalone-operability

## T096: the contract family's tests (plan 038; T094's map R2-INV-R9, means CS)

**Realizes** (no `tasks.md` entry yet; the contract is the slice outline row of R2-INV-R9 plus the holder's rulings `6021830531`): requirement 9's declared composed tests, toward T073's composed step 5a (F9.1). This is the CS constants half for the four contracts-only files.

**Falsifier** (the outline row): the 74 composed reds of these four files read 0 (11 / 13 / 32 / 18). Each file runs alone and composed, red first, then green. The unchanged set reads unchanged: 12.5's 16 governed suites, F5.2's files, and `test_column_contributions_governed.py` at 9 passed. `tests/test_declared_exclusion.py` reads green lone.

**Ruled / Decisions:** T094 RULED `6021830531` (OQ-R9-2 at its default: the farm is built by the conftest, composed-only, and named `CONTRACTS_DIR`), under `6016982816` (ARC-Q2 (a)). There are no decisions of this slice's own.

**After:** T095 (#46), which defines `CONTRACTS_DIR`. It landed as e2340bb, and this branch has merged it.

## The change (the four files only)

Each file took `Path(__file__).parents[2]` as openxFactory's root. Composed, that is the directory above the checkout. Each file now reads T095's `CONTRACTS_DIR` (`from conftest import CONTRACTS_DIR`):
- **Composed:** `SCRIPT` is this leg's own `scripts/validate-ideation-dashboard-contracts.py`, which reads the farm through the `CONTRACTS_DIR` environment variable. `SCHEMAS` and `REGISTER_SCHEMA` read `CONTRACTS_DIR/schemas`, and `EXAMPLES` reads `CONTRACTS_DIR.parent/examples/ideation-dashboard`.
- **Lone:** `CONTRACTS_DIR` is None, and every path is byte-identical to before. So each file still fails alone for its declared reason, `openxfactory-contracts`.

The consumer-validator test's docstring now states which validator `find_validator` answers in each case (Copilot r4201041192). No test, guard, fixture or assertion is added or removed.

Files: `tests/test_project_action_contracts.py`, `tests/test_project_schema_election.py`, `tests/test_validate_ideation_dashboard_contracts.py`, `tests/test_wheel_action_contracts.py`.

## Measured: base = `main` e2340bb, head = fd16d70 (this branch, merged with it)

**The composed form, R1Q23 (a):**
- openxFactory `92010d3e` with openDox, openXdox and openXwallet initialized recursively, and its `scripts/` on `PYTHONPATH`;
- openDox installed from openDox-code `main` `84f8ed83`;
- T022's placement made before the run and removed after it, with `git status` clean afterwards;
- colour variables unset, `TERM=dumb`;
- Python 3.12.3, pytest 8.4.2, Node v24.21.0;
- each file run alone.

```
base  test_project_action_contracts.py              1 failed, 10 errors
base  test_project_schema_election.py               13 errors
base  test_validate_ideation_dashboard_contracts.py 1 failed, 1 passed, 31 errors
base  test_wheel_action_contracts.py                1 failed, 17 errors
      every red: FileNotFoundError / can't open file on <workspace>/scripts/validate-ideation-dashboard-contracts.py,
      <workspace>/contracts/schemas/project-register.schema.yaml or .../gate-intent.schema.yaml, where <workspace> is the
      directory above the checkout
head  test_project_action_contracts.py              11 passed
head  test_project_schema_election.py               13 passed
head  test_validate_ideation_dashboard_contracts.py 33 passed
head  test_wheel_action_contracts.py                18 passed
```

**Unchanged set, composed, each file alone, at the same base and head:**
- 12.5's 16 governed suites (computed by `git grep -l -e open-pr -e open_pr -e FakePullRequests -- 'tests/test_*.py'`);
- the 7 files F5.2's set names (`test_generator.py`, `test_snapshot*.py`, `test_session_snapshot.py`);
- `test_column_contributions_governed.py`, at 9 passed.

Together they are 1004 nodes, with 0 red at base and 0 red at head, and **0 nodes differ**.

**Lone** (a venv from `pip install -e ".[test]"`, openDox at the pyproject pin `dede32b4`, no `PYTHONPATH`):
- `tests/test_declared_exclusion.py`: 207 passed at base and at head;
- the `validate.yml` form at head: `1161 passed, 4 skipped, 1 deselected` (selected 1165 / passed 1161 / skipped 4; floors 1099 / 1095 / 4).

**Mutants**, each applied to all four files at head. All were killed:
- M1, the lone half reads the leg (`parents[1]` always): `test_declared_exclusion.py` lone, the 4 entries fail;
- M2, the composed half keeps `parents[2]`: composed, 68 red (every case that loads the validator);
- M3, the schemas not from the farm: composed, all four files red;
- M4, the examples not beside the farm: composed, three files red;
- M5, the composed flag inverted: lone, the 4 entries fail; composed, 74 red.

In M5's lone run, `test_hosted_actor.py`'s entry also failed once. That file is not one of these four and is not mutated, and it reads green in the unmutated head run (207 passed). It was one-off noise on a loaded host.

Earlier rounds measured the same falsifier over local integrations of `main` + #40 + #46 7ad77ae, and of `main` f70bc9c + #46 c2192e3, with the same results for these four files.

## CI and Copilot

- CI at fd16d70: `validate` success (run 37548764129: `1161 passed, 4 skipped, 1 deselected`, which is selected 1165 / passed 1161 / skipped 4, above the floors), and SonarCloud Code Analysis success.
- Copilot review 5435242460 at 70871d1 raised 1 low finding, r4201041192. It was fixed in 98739e8, answered and resolved.
- Copilot review 5435648181 at 631683c: Findings None. Its "Changes recommended" was only the then-open #46.
- Copilot review 5435813394 at fd16d70: Approval recommended, Findings None.
- Review threads: 1 (r4201041192), resolved; 0 unresolved.

## Not done

- The commit carries `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`, the harness's attribution form, not WRITER-RULES' "(1M context)" form.

🤖 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
…1 and T098 landed)

Conflicts in src/openxdox/gate_console.py, tests/conftest.py,
tests/test_gate_console_schema_source.py and tests/test_host_plane.py,
all T020's and T021's files that this branch carried from their pull
requests before they landed: main's side taken for all four (main's
gate_console.py and the schema-source test equal #40's final head; main's
conftest.py and test_host_plane.py carry T026's later edits). No T029 file
conflicted.

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 7, 2026
…escapes (jsonschema>=4.18) (#55)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Authored by lane openXfactory-3, delegated by lane openxfactory-4 (lane4-to-lane3.log 2026-10-06T19:07:30Z; READY note #656 6023492215)

## T021 follow-on: the cyclic-schema test asserts only that `GateRefused` escapes (`jsonschema>=4.18`)

### The defect

`tests/test_gate_console_schema_source.py::test_a_host_schema_the_console_cannot_apply_refuses_as_gate_refused` pinned the exact failure mode `jsonschema` gives a cyclic MAPPING schema: it asserted the refusal text `...could not be checked: RecursionError`. `pyproject.toml` allows `jsonschema>=4.18` with no upper bound, so a supported version that refuses the cycle by another route (a `SchemaError`, a `ValueError`, a changed message) would turn the test red while the console still did its job. Copilot raised it on #40 (r4199337845); the holder classed it an accepted limit under #656 `5988818366` and assigned this follow-on (READY note #656 `6023492215`).

### The fix

The cyclic mapping now asserts what the cyclic list beside it already asserted: the outcome starts with `GateRefused: `. An exception that escaped reads `ESCAPED ...` and fails that prefix. The test's intent is unchanged (a cyclic schema a host supplies refuses as `GateRefused`, never as another exception); only the version-specific step and exception text are dropped. The product code is untouched: `_schema_errors` already catches any exception at each of its three steps.

Files changed: `tests/test_gate_console_schema_source.py` only (+10 -7).

### The falsifier, stated plainly

**The old assertion does not fail under any released `jsonschema` in the supported range today.** I installed 10 versions in throwaway venvs (the repository's CI install, `pip install -e ".[test]"`, with `jsonschema` pinned to each) and ran the same child-interpreter body the test runs:

```
jsonschema 4.18.0 cyclic_mapping old_assert=PASS new_assert=PASS :: GateRefused: ...schema could not be checked: RecursionError: maximum recursion dep...
jsonschema 4.18.0 cyclic_list    old_assert=PASS new_assert=PASS :: GateRefused: ...schema is not a valid JSON Schema: [[...]] is not of type 'object'
(identical lines for 4.18.6, 4.19.2, 4.20.0, 4.21.1, 4.22.0, 4.23.0, 4.24.1, 4.25.1, 4.26.0)
```

So the defect is latent, not live: no version in 4.18.0 to 4.26.0 breaks the old assertion. What I measured instead is that the old assertion pins a library implementation detail (the exception class and step of a pathological traversal), and what happens when the library changes it.

**A simulated library change.** A throwaway `sitecustomize` (in a copy of the tree, never committed) makes `Draft202012Validator.check_schema` refuse a cyclic schema by another route (the cycle becomes a `SchemaError`, or a `ValueError`), run under 4.18.0 and under 4.26.0, the installed version, old test file against the new:

```
installed jsonschema 4.18.0 and 4.26.0, identical:
 library raises SchemaError  old: 1 failed  AssertionError: GateRefused: ...schema is not a valid JSON Schema: a cyclic schema is not a schema
                             new: 1 passed
 library raises ValueError   old: 1 failed  AssertionError: GateRefused: ...schema could not be checked: ValueError: cycle detected
                             new: 1 passed
```

In both, the console still refuses as `GateRefused`; only the old assertion's pinned text was wrong.

**Mutants of the new guard, killed.** Throwaway copies of `gate_console.py`, run against the NEW test:

```
M1 the check step's catch-all removed:  1 failed  AssertionError: ESCAPED RecursionError: maximum recursion depth exceeded
M2 the check step raises RuntimeError:  1 failed  AssertionError: ESCAPED RuntimeError: ...could not be checked: RecursionError: maximum recursion depth exceeded
control (restored):                     1 passed
```

So the loosened assertion still catches what matters: a cyclic schema whose refusal is not a `GateRefused`.

### Run counts

- The edited file alone, under each of the 10 `jsonschema` versions above (4.18.0 to 4.26.0): `6 passed` each.
- The whole suite in its lone CI form (`python -m pytest -q --deselect <LEFT_OUT>`, the `pytest` step of `validate.yml`; `FORCE_COLOR`, `CLICOLOR`, `PY_COLORS`, `COLORTERM` unset, `TERM=dumb`; installed `jsonschema` 4.26.0), at this head on `6f25915`: `1161 passed, 4 skipped, 1 deselected`. Test count unchanged, so the pinned triple's floors do not move.

### Ruled / decisions

Ruled: Copilot r4199337845 accepted limit under #656 `5988818366`; this follow-on assigned by the holder (lane4-to-lane3.log 2026-10-06T19:07:30Z). Decision: take Copilot's one-line remedy as given (assert only `GateRefused` escapes), not a version cap in `pyproject.toml`.

### Not done

- No `pyproject.toml` change (no upper bound added): the loosened assertion is the remedy.
- No change to `gate_console.py`.
- No other test in the file touched.

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


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