From c52900e9eb80324fcecceaf6705a06c057a4d243 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:39:02 +0000 Subject: [PATCH 01/13] T020, U-1: the governed host for the governed suites that drive the gate 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 --- tests/conftest.py | 213 +++++++++++++++++++++++++++++++++++++-- tests/test_host_plane.py | 206 ++++++++++++++++++++++++++++++++++++- 2 files changed, 409 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 27de232..44e1b4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,9 @@ from __future__ import annotations +import contextlib +import functools +import importlib import sys from pathlib import Path @@ -557,22 +560,218 @@ class StandInHost: SUBCOMMAND_EXTENSIONS: tuple = () -@_pytest.fixture(autouse=True) -def _a_hosts_plane_for_the_token_reading_suites(request): - if request.node.path.name not in HOST_PLANE_SUITES: - yield - return +@contextlib.contextmanager +def a_hosts_plane(host_factory): + """Register the host `host_factory()` builds with openDox's registry for + the length of the block, then put back what was registered before. + + The registration is dropped first, so the host's is made on a clean slate, + and the host is built only then. Whatever the block does, the registry is + emptied again afterwards and the earlier registration restored.""" from opendox import default_profile as _default_profile from opendox import domain_profile as _registry previous = _registry.current() if _registry.is_registered() else None _registry.unregister() - _registry.register(StandInHost()) try: - yield + yield _registry.register(host_factory()) finally: _registry.unregister() # The entry point's default is not put back as a registration: the # next entry point registers it again, as it does in any process. if previous is not None and previous is not _default_profile: _registry.register(previous) + + +@_pytest.fixture(autouse=True) +def _a_hosts_plane_for_the_token_reading_suites(request): + if plane_host_for(request.node.path.name) != "stand-in": + yield + return + with a_hosts_plane(StandInHost): + yield + + +# --------------------------------------------------------------------------- +# THE GOVERNED HOST FOR THE GOVERNED SUITES THAT DRIVE THE GATE VERBS (plan 038 +# T020, U-1; R2Q8 (a), openxFactory#656 comment 6003486656; P4F-5, ruled with +# plan 038 on comment 6013547504). +# +# #1144's 12.5 runs openxFactory's governed suites, each alone, "with the +# host's implementation registered". Eight of them drive openXdox's `gate` +# verbs, and with them its gate and projection routes. Those columns reach +# openDox only through a registered host: openDox's default profile +# contributes none, and `StandInHost` above contributes none either. So their +# cases read `invalid choice: 'gate'` or `unknown_action` (R2-INV-P4F's +# host-reg bucket and its one-off groups H and T). +# +# For the length of each test of the modules GOVERNED_HOST_SUITES names, this +# registers THE GOVERNED HOST, and puts the registry back afterwards, as the +# stand-in's fixture above does. Which host that is depends on the run: +# * Where the run is composed, with openxFactory's `scripts/` on the path +# (R1Q23 (a)), it is openxFactory's own composite, `opendox_host.profile()`: +# the object openxFactory's process-start call registers. It carries +# openXdox's gate and projection columns and openxFactory's own lane +# column, whose `POST /actions/refresh` two cases request. It is found as +# the home corpus's registration above finds its adapter: only a missing +# `opendox_host` itself means "absent". +# * Elsewhere it is `GovernedStandInHost`: openXdox's three columns and +# nothing else, as `tests/test_hermeticity_gate_verbs.py`'s `_GateHost` +# carries the gate verbs. Today these suites reach `doc_health`, so they +# run composed or not at all; this is the host they get wherever else they +# come to run. +# +# THE COMPOSITE ALONE, NOT openxFactory'S WHOLE START. `register_openxfactory()` +# also fills openxFactory's process-wide seams, and openxFactory's host has no +# call that takes them back, so they would outlive the test into every later +# module of the run. And it refuses while openXdox's own registry holds the +# profile the root `conftest.py` registers. This harness makes a host's other registrations +# itself (the profile, the home corpus), so only the composite is registered, +# with openDox's registry, as the stand-in is. +# +# AND openxFactory'S REACH STAYS OUT OF THIS PROCESS. Importing openxFactory's +# lane column runs `carved_reach.install()`, which puts openxFactory's pinned +# `openXdox/code/src` at the head of `sys.path`. `openxdox` is a namespace +# package, so every later `openxdox.*` import would come from that pinned copy +# and not from this checkout, and the run would measure another commit. So +# openXdox's columns are imported from this checkout first, the composite's +# facets are read once with `sys.path` saved, `sys.path` is put back, and an +# `openxdox` module imported from anywhere else refuses the case, by name. +# +# THE LIST IS THE SCAN'S, as HOST_PLANE_SUITES is. `tests/test_host_plane.py` +# holds it equal to the governed suites, computed as 12.5 computes them +# (`governed_suites()` below), that name a `gate` verb of openXdox's command +# line. A module on both lists runs under the governed host, which is a host's +# plane too (`plane_host_for`). +# --------------------------------------------------------------------------- + +GOVERNED_HOST_SUITES = frozenset({ + "test_branch_session.py", + "test_gateway_provenance.py", + "test_session_confinement.py", + "test_session_gates.py", + "test_session_lifecycle.py", + "test_session_notebook.py", + "test_session_transaction.py", + "test_session_verbs.py", +}) + +#: 12.5's governed set is every `tests/test_*.py` that names one of these +#: (its falsifier's `git grep -l -e … -- 'tests/test_*.py'`). They are spelled +#: here, in a file that pathspec does not match, so the check that holds the +#: list to its scan does not join the set it scans. +GOVERNED_SUITE_MARKERS: tuple[str, ...] = ("open-pr", "open_pr", "FakePullRequests") + + +def governed_suites(tests: Path = HERE) -> frozenset[str]: + """12.5's governed set: the `test_*.py` modules directly in `tests` that + name one of GOVERNED_SUITE_MARKERS.""" + return frozenset( + path.name for path in tests.glob("test_*.py") + if any(marker in path.read_text(encoding="utf-8") + for marker in GOVERNED_SUITE_MARKERS)) + + +def plane_host_for(suite: str) -> str | None: + """The host a test module's cases run under: "governed" for + GOVERNED_HOST_SUITES, "stand-in" for the rest of HOST_PLANE_SUITES, and + None for every other module. The governed host wins where a module is on + both lists, because it is a host's plane as well.""" + if suite in GOVERNED_HOST_SUITES: + return "governed" + if suite in HOST_PLANE_SUITES: + return "stand-in" + return None + + +#: openXdox's governed columns, as openxFactory's profile contributes them: +#: (the facet, the module, the class). Named rather than imported, because +#: each of the three modules reaches `doc_health`. +GOVERNED_COLUMNS: tuple[tuple[str, str, str], ...] = ( + ("SUBCOMMAND_EXTENSIONS", "openxdox.cli_gate", "GateSubcommands"), + ("ROUTE_EXTENSIONS", "openxdox.serve_gate", "GateRoutesExtension"), + ("ROUTE_EXTENSIONS", "openxdox.serve_projection", "ProjectionRoutesExtension"), +) + + +def _governed_columns(facet: str) -> tuple: + return tuple(getattr(importlib.import_module(module), name)() + for each, module, name in GOVERNED_COLUMNS if each == facet) + + +class GovernedStandInHost: + """A host's profile that carries openXdox's governed columns and nothing + else. Each facet is built on first read, so the host itself can be made + where `doc_health` cannot be imported.""" + + @functools.cached_property + def SUBCOMMAND_EXTENSIONS(self) -> tuple: # noqa: N802 (the facet's name) + return _governed_columns("SUBCOMMAND_EXTENSIONS") + + @functools.cached_property + def ROUTE_EXTENSIONS(self) -> tuple: # noqa: N802 (the facet's name) + return _governed_columns("ROUTE_EXTENSIONS") + + +def openxfactory_host(): + """openxFactory's host module (`scripts/opendox_host.py`) where the run is + composed, and None where it is not. Only a missing `opendox_host` itself + means "absent": a present one that cannot be imported fails here, loudly.""" + try: + return importlib.import_module("opendox_host") + except ModuleNotFoundError as absent: + if absent.name != "opendox_host": + raise + return None + + +#: The facets openDox's parser and server builders read off a host's profile. +#: Reading them is what imports a composite's columns. +BUILT_FACETS: tuple[str, ...] = ( + "SUBCOMMAND_EXTENSIONS", "ROUTE_EXTENSIONS", "HANDLER_CONTRIBUTIONS") + + +def _refuse_an_openxdox_from_elsewhere() -> None: + """Every `openxdox` module this process holds is this checkout's, or the + case is refused, naming each module that is not.""" + home = Path(_domain_profile.__file__).resolve().parent + elsewhere = sorted( + name for name, module in list(sys.modules.items()) + if name.startswith("openxdox.") + and getattr(module, "__file__", None) + and not Path(module.__file__).resolve().is_relative_to(home)) + if elsewhere: + raise RuntimeError( + f"{elsewhere} were imported from outside this checkout's openxdox " + f"({home}), so the run would measure another copy of them. " + "openxFactory's reach puts its pinned openXdox leg ahead of this " + "checkout when its lane column is imported; import the module " + "from this checkout before the composite's facets are read.") + + +def governed_host(): + """THE GOVERNED HOST for one case: openxFactory's composite where the run + is composed, and `GovernedStandInHost` where it is not.""" + host_module = openxfactory_host() + if host_module is None: + return GovernedStandInHost() + for _, module, _ in GOVERNED_COLUMNS: + importlib.import_module(module) + saved = list(sys.path) + try: + composite = host_module.profile() + for facet in BUILT_FACETS: + getattr(composite, facet) + finally: + sys.path[:] = saved + _refuse_an_openxdox_from_elsewhere() + return composite + + +@_pytest.fixture(autouse=True) +def _the_governed_host_for_the_governed_suites(request): + if plane_host_for(request.node.path.name) != "governed": + yield + return + with a_hosts_plane(governed_host): + yield diff --git a/tests/test_host_plane.py b/tests/test_host_plane.py index 5045cb5..9520b83 100644 --- a/tests/test_host_plane.py +++ b/tests/test_host_plane.py @@ -1,24 +1,36 @@ """The suites that read the console token run on a host's plane (plan 034 T086, -step (b); openDox-code's T104, RULED openxFactory#656 comment 5963851934). +step (b); openDox-code's T104, RULED openxFactory#656 comment 5963851934), and +the governed suites that drive openXdox's gate verbs run under the governed +host (plan 038 T020, U-1; R2Q8 (a)). `tests/conftest.py`'s `_a_hosts_plane_for_the_token_reading_suites` registers a stand-in host for the length of each test of the modules `HOST_PLANE_SUITES` names, because from T104 openDox publishes the console token on -`/capabilities` only on a host's plane. These cases hold that list and that -host to their reasons, and they run in a lone checkout, so in F9.1. +`/capabilities` only on a host's plane. Its +`_the_governed_host_for_the_governed_suites` registers the governed host, the +same way, for the modules `GOVERNED_HOST_SUITES` names. These cases hold both +lists and both hosts to their reasons, and they run in a lone checkout, so in +F9.1. None of them needs openxFactory: its host is stood in where a case needs +one. A CREATED file: no manifest row (RULED OQ-C). """ from __future__ import annotations +import ast import importlib.util import inspect import re +import sys +import types from pathlib import Path +import pytest + import conftest as tests_conftest from opendox import default_profile +from opendox import domain_profile as registry TESTS = Path(__file__).resolve().parent @@ -81,3 +93,191 @@ def test_on_the_stand_in_hosts_plane_the_token_stays_on_capabilities() -> None: source = inspect.getsource(serve_mod.build_server) assert "capabilities[CONSOLE_TOKEN_FIELD] = console_token" in source assert "delivery_for" not in source + + +# --------------------------------------------------------------------------- +# THE GOVERNED HOST (plan 038 T020, U-1). +# --------------------------------------------------------------------------- + +SOURCE = TESTS.parent / "src" + +#: A `gate` verb, as a test module spells it: the string `gate` and, after the +#: comma, the verb, the way an argument vector names a subcommand. +_GATE_VERB = re.compile(r"""["']gate["']\s*,\s*["']([a-z][a-z-]*)["']""") + + +def _gate_verbs() -> frozenset[str]: + """The verbs openXdox's `gate` command declares: every name + `src/openxdox/cli_gate.py` hands to `add_parser`, less `gate` itself. Read + from the source, so a lone checkout can read it without `doc_health`.""" + tree = ast.parse((SOURCE / "openxdox" / "cli_gate.py").read_text(encoding="utf-8")) + names = {node.args[0].value for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "add_parser" and node.args + and isinstance(node.args[0], ast.Constant) + and isinstance(node.args[0].value, str)} + return frozenset(names - {"gate"}) + + +def _drives_a_gate_verb(path: Path, verbs: frozenset[str]) -> bool: + return any(verb in verbs + for verb in _GATE_VERB.findall(path.read_text(encoding="utf-8"))) + + +def test_the_governed_list_is_every_governed_suite_that_drives_a_gate_verb() -> None: + verbs = _gate_verbs() + assert {"create-document", "edit-document", "abandon-session"} <= verbs + governed = tests_conftest.governed_suites() + assert len(governed) >= 16, "12.5's floor: fewer means a proof vanished" + scanned = {name for name in governed + if _drives_a_gate_verb(TESTS / name, verbs)} + listed = set(tests_conftest.GOVERNED_HOST_SUITES) + assert scanned == listed, ( + f"drive a gate verb but are not listed: {sorted(scanned - listed)}; " + f"listed but drive none: {sorted(listed - scanned)}. A governed suite " + "(12.5's set) that drives openXdox's gate verbs needs a host carrying " + "openXdox's columns, so it joins tests/conftest.py's " + "GOVERNED_HOST_SUITES") + # The scan is 12.5's own set, and not every module that drives a verb: + # outside the governed set a suite registers its own host, or none. + assert any(_drives_a_gate_verb(path, verbs) + for path in TESTS.glob("test_*.py") if path.name not in governed) + + +def test_a_module_on_both_lists_runs_under_the_governed_host() -> None: + governed = tests_conftest.GOVERNED_HOST_SUITES + plane = tests_conftest.HOST_PLANE_SUITES + assert governed & plane, "no module is on both lists, so nothing tests the order" + for name in governed: + assert tests_conftest.plane_host_for(name) == "governed", name + for name in plane - governed: + assert tests_conftest.plane_host_for(name) == "stand-in", name + assert tests_conftest.plane_host_for(Path(__file__).name) is None + + +def _stand_in_columns(monkeypatch) -> list[str]: + """A module object for each of openXdox's governed columns, standing in for + the real one, which reaches `doc_health`. Each carries its named class, and + the harness's imports of them are recorded, in order, in the list returned. + Nothing else is imported differently, and `sys.modules` is not touched.""" + modules: dict[str, types.ModuleType] = {} + for _, module, name in tests_conftest.GOVERNED_COLUMNS: + stand_in = modules.setdefault(module, types.ModuleType(module)) + setattr(stand_in, name, type(name, (), {})) + imported: list[str] = [] + real_import = importlib.import_module + + def import_module(name: str, package: str | None = None): + if name in modules: + imported.append(name) + return modules[name] + return real_import(name, package) + + monkeypatch.setattr(tests_conftest, "importlib", + types.SimpleNamespace(import_module=import_module)) + return imported + + +def test_the_governed_stand_in_carries_openxdoxs_three_columns(monkeypatch) -> None: + """openXdox's share of openxFactory's profile: its gate verbs, and its gate + and projection routes. Each named class is defined where it is named.""" + assert tests_conftest.GOVERNED_COLUMNS == ( + ("SUBCOMMAND_EXTENSIONS", "openxdox.cli_gate", "GateSubcommands"), + ("ROUTE_EXTENSIONS", "openxdox.serve_gate", "GateRoutesExtension"), + ("ROUTE_EXTENSIONS", "openxdox.serve_projection", "ProjectionRoutesExtension"), + ) + for _, module, name in tests_conftest.GOVERNED_COLUMNS: + source = (SOURCE / Path(*module.split("."))).with_suffix(".py") + tree = ast.parse(source.read_text(encoding="utf-8")) + assert name in {node.name for node in tree.body + if isinstance(node, ast.ClassDef)}, (module, name) + + _stand_in_columns(monkeypatch) + monkeypatch.setitem(sys.modules, "opendox_host", None) # not composed + host = tests_conftest.governed_host() + assert isinstance(host, tests_conftest.GovernedStandInHost) + assert [type(c).__name__ for c in host.SUBCOMMAND_EXTENSIONS] == ["GateSubcommands"] + assert [type(c).__name__ for c in host.ROUTE_EXTENSIONS] == [ + "GateRoutesExtension", "ProjectionRoutesExtension"] + assert host.ROUTE_EXTENSIONS is host.ROUTE_EXTENSIONS + assert host is not default_profile + if importlib.util.find_spec("opendox.console_access") is not None: + from opendox import console_access + + assert (console_access.delivery_for(host) + == console_access.DELIVERY_CAPABILITIES) + + +class _Composite: + """openxFactory's composite, stood in. Reading a built facet records + whether openXdox's columns were already imported, and then does what + importing openxFactory's lane column does: puts a pinned leg at the head + of `sys.path`.""" + + def __init__(self, leg: Path, imported: list[str], on_read=None) -> None: + self.leg, self.imported, self.on_read, self.reads = leg, imported, on_read, [] + + def __getattr__(self, name: str): + if name not in tests_conftest.BUILT_FACETS: + raise AttributeError(name) + columns = {module for _, module, _ in tests_conftest.GOVERNED_COLUMNS} + self.reads.append((name, columns <= set(self.imported))) + if str(self.leg) not in sys.path: + sys.path.insert(0, str(self.leg)) + if self.on_read is not None: + self.on_read() + return () + + +def _composed(monkeypatch, composite: _Composite) -> None: + host_module = types.ModuleType("opendox_host") + host_module.profile = lambda: composite + monkeypatch.setitem(sys.modules, "opendox_host", host_module) + + +def test_composed_the_governed_host_is_the_composite_and_the_reach_is_put_back( + monkeypatch, tmp_path) -> None: + imported = _stand_in_columns(monkeypatch) + composite = _Composite(tmp_path / "openXdox" / "code" / "src", imported) + _composed(monkeypatch, composite) + before = list(sys.path) + earlier = tests_conftest.StandInHost() + with tests_conftest.a_hosts_plane(lambda: earlier): + with tests_conftest.a_hosts_plane(tests_conftest.governed_host) as host: + assert host is composite + assert registry.current() is composite + assert sys.path == before + assert registry.current() is earlier + assert [name for name, _ in composite.reads] == list(tests_conftest.BUILT_FACETS) + assert all(imported for _, imported in composite.reads), ( + "a facet was read before openXdox's columns were imported from this " + "checkout") + + +def test_an_openxdox_module_from_elsewhere_refuses_the_case(monkeypatch, tmp_path) -> None: + imported = _stand_in_columns(monkeypatch) + elsewhere = types.ModuleType("openxdox.from_a_pinned_leg") + elsewhere.__file__ = str(tmp_path / "openxdox" / "from_a_pinned_leg.py") + + def import_from_the_leg(): + monkeypatch.setitem(sys.modules, elsewhere.__name__, elsewhere) + + composite = _Composite(tmp_path / "leg", imported, on_read=import_from_the_leg) + _composed(monkeypatch, composite) + before = list(sys.path) + with pytest.raises(RuntimeError, match=r"openxdox\.from_a_pinned_leg"): + tests_conftest.governed_host() + assert sys.path == before + + +def test_openxfactorys_host_is_absent_only_when_its_own_module_is(monkeypatch, tmp_path) -> None: + monkeypatch.setitem(sys.modules, "opendox_host", None) + assert tests_conftest.openxfactory_host() is None + monkeypatch.delitem(sys.modules, "opendox_host") + (tmp_path / "opendox_host.py").write_text( + "import a_dependency_the_host_lacks\n", encoding="utf-8") + monkeypatch.syspath_prepend(str(tmp_path)) + with pytest.raises(ModuleNotFoundError) as refused: + tests_conftest.openxfactory_host() + assert refused.value.name == "a_dependency_the_host_lacks" From 073615822921fe7b562f0256607a39d5deb44116 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:01:10 +0000 Subject: [PATCH 02/13] T021, U-2: the gate console's schemas, each read where its owner keeps 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 --- src/openxdox/gate_console.py | 138 ++++++++++++-- tests/conftest.py | 16 ++ tests/test_gate_console_schema_source.py | 217 +++++++++++++++++++++++ 3 files changed, 359 insertions(+), 12 deletions(-) create mode 100644 tests/test_gate_console_schema_source.py diff --git a/src/openxdox/gate_console.py b/src/openxdox/gate_console.py index 640de0c..b6dbe45 100644 --- a/src/openxdox/gate_console.py +++ b/src/openxdox/gate_console.py @@ -631,22 +631,22 @@ def build_gate_action_record( def _validate_contract_document( - document: Mapping[str, Any], *, schema_filename: str, label: str, + document: Mapping[str, Any], *, schema_text: str, label: str, ) -> None: - """Validate one document against a repository-pinned released schema.""" + """Validate one document against a released schema's text, which its + caller reads where the schema's owner keeps it: `_packaged_schema_text` + or `_host_schema_text`, under "THE CONTRACT SCHEMAS THE CONSOLE VALIDATES + AGAINST" near the end of this module (plan 038 T021).""" try: from jsonschema import Draft202012Validator, FormatChecker except ImportError as exc: # pragma: no cover - release dependency guard raise GateRefused( f"jsonschema is required to validate {label}") from exc - schema_path = (Path(__file__).resolve().parents[2] / "contracts" / "schemas" - / schema_filename) try: - schema = yaml.safe_load(schema_path.read_text(encoding="utf-8")) - except (OSError, yaml.YAMLError) as exc: + schema = yaml.safe_load(schema_text) + except yaml.YAMLError as exc: raise GateRefused( - f"the {label} schema could not be loaded from {schema_path}: " - f"{exc}") from exc + f"the {label} schema could not be parsed: {exc}") from exc errors = sorted( Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors( dict(document)), @@ -661,8 +661,8 @@ def _validate_contract_document( def validate_gate_action_record(record: Mapping[str, Any]) -> None: """Validate one record against the repository-pinned released schema.""" - _validate_contract_document( - record, schema_filename="gate-action-record.schema.yaml", + _validate_contract_document(record, schema_text=_packaged_schema_text( + GATE_ACTION_RECORD_SCHEMA, label="gate-action record"), label="gate-action record") if record.get("action") != ACTION_CLEANUP_ABANDONED_BRANCH: return @@ -707,8 +707,8 @@ def validate_gate_action_record(record: Mapping[str, Any]) -> None: def validate_demotion_execution_receipt(receipt: Mapping[str, Any]) -> None: - _validate_contract_document( - receipt, schema_filename="demotion-execution-receipt.schema.yaml", + _validate_contract_document(receipt, schema_text=_host_schema_text( + DEMOTION_RECEIPT_SCHEMA, label="demotion execution receipt"), label="demotion execution receipt") destination = receipt.get("destination") if not isinstance(destination, Mapping) or str(destination.get("path") or "") \ @@ -2348,6 +2348,120 @@ def edit_project(self, project_id: str, *, add=None, remove=None, at=at, records_dir=self.records_dir, provenance=provenance, **extra) +# -------------------------------------------------------------------------- +# THE CONTRACT SCHEMAS THE CONSOLE VALIDATES AGAINST, AND WHERE EACH IS READ +# (plan 038 T021, U-2; R2Q8 (a); P4F-4, ruled with plan 038 on +# openxFactory#656 comment 6013547504; R1Q27 (a), comment 5851950767). +# +# Two documents are validated here, against two schemas with two owners: +# * a gate-action record, against openXdox-spec's +# `gate-action-record.schema.yaml`. This distribution packages it in +# `openxdox.contracts`, as one of the consumer validator's own three kinds, +# held to its digest by `copies.yaml`. So it is read from there, through +# `contracts.verified_bytes()`, wherever this package runs; +# * a demotion execution receipt, against openxFactory's +# `demotion-execution-receipt.schema.yaml`. That schema is the HOST's +# contract: openXdox-spec carries only its examples, and the copy record +# admits the validator's three kinds and no fourth +# (`tests/test_packaged_validator.py`), so nothing of it is vendored here. +# A host that writes receipts registers, once, at process start, the +# directory its schema is in, with `register_contract_schema_source()`. +# With none registered, validating a receipt refuses, naming that call: +# R1Q27 (a)'s "only where the tree it runs from supplies their schemas". +# +# Both used to be read from `parents[2] / "contracts" / "schemas"` beside +# this module. That was openxFactory's own tree before the carve. In this +# leg it is the checkout's root, which holds no `contracts/`. +# -------------------------------------------------------------------------- + +#: The gate-action record schema's copy id in `openxdox.contracts`. +GATE_ACTION_RECORD_SCHEMA = "gate-action-record" +#: The demotion execution receipt schema's file name in the host's source. +DEMOTION_RECEIPT_SCHEMA = "demotion-execution-receipt.schema.yaml" + +_contract_schema_source: Path | None = None + + +class ContractSchemaSourceRefused(RuntimeError): + """A contract schema source the gate console cannot take, refused when it + is registered rather than when a receipt is first validated.""" + + +def register_contract_schema_source(source: str | Path) -> Path: + """Register the host's contract schema directory. Returns it, resolved. + + The directory must hold `demotion-execution-receipt.schema.yaml`, the one + schema the console reads from a host. Registering the same directory again + is a no-op, so an idempotent host start is not punished. A DIFFERENT one is + refused: one process whose receipts are validated against two schemas, + depending on which registration a caller reached, is the failure one + registration exists to prevent. Call `unregister_contract_schema_source()` + first if the swap is deliberate.""" + global _contract_schema_source + directory = Path(source).resolve() + if not (directory / DEMOTION_RECEIPT_SCHEMA).is_file(): + raise ContractSchemaSourceRefused( + f"{directory} holds no {DEMOTION_RECEIPT_SCHEMA}, so the gate " + "console cannot read a demotion execution receipt's schema from " + "it. Register the directory the host keeps its contract schemas " + "in, its contracts/schemas/.") + if (_contract_schema_source is not None + and _contract_schema_source != directory): + raise ContractSchemaSourceRefused( + f"a contract schema source is already registered " + f"({_contract_schema_source}), and {directory} would replace it. " + "Registration happens once, at process start. Call " + "openxdox.gate_console.unregister_contract_schema_source() first " + "if the swap is deliberate.") + _contract_schema_source = directory + return directory + + +def unregister_contract_schema_source() -> None: + """Drop the registration. For test isolation and for a host tearing down.""" + global _contract_schema_source + _contract_schema_source = None + + +def contract_schema_source_registered() -> bool: + """Is a contract schema source registered, without reading anything?""" + return _contract_schema_source is not None + + +def _packaged_schema_text(copy_id: str, *, label: str) -> str: + """A schema this distribution packages, once its digest has been checked.""" + from . import contracts + + try: + return contracts.verified_bytes(copy_id).decode("utf-8") + except (contracts.CopyRefused, UnicodeDecodeError) as exc: + raise GateRefused( + f"the {label} schema could not be read from openXdox's packaged " + f"copy: {exc}") from exc + + +def _host_schema_text(filename: str, *, label: str) -> str: + """A schema the host supplies, from the source it registered.""" + source = _contract_schema_source + if source is None: + raise GateRefused( + f"the {label} cannot be validated: its schema, {filename}, 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()" + " (R1Q27 (a): a kind is validated only where the tree it runs " + "from supplies its schema).") + path = source / filename + try: + return path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as exc: + raise GateRefused( + f"the {label} schema could not be loaded from {path}: " + f"{exc}") from exc + + + # --------------------------------------------------------------------------- # BACKWARD-COMPATIBLE MODULE ATTRIBUTES, RESOLVED ON FIRST ACCESS (PEP 562). # diff --git a/tests/conftest.py b/tests/conftest.py index 44e1b4f..5c0ad6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -775,3 +775,19 @@ def _the_governed_host_for_the_governed_suites(request): return with a_hosts_plane(governed_host): yield + + +# THE GATE CONSOLE'S CONTRACT SCHEMA SOURCE, REGISTERED AS A HOST REGISTERS IT +# (plan 038 T021, U-2; P4F-4). A demotion execution receipt's schema is +# openxFactory's contract, and the gate console reads it only from a source a +# host registers (`gate_console.register_contract_schema_source`). Where the +# run is composed, this registers openxFactory's `contracts/schemas/` from the +# composed tree, at process start, as openxFactory's own host wiring does from +# T030. A lone checkout registers none, and there a receipt refuses by name +# (R1Q27 (a)). +_OPENXFACTORY_HOST = openxfactory_host() +if _OPENXFACTORY_HOST is not None: + from openxdox import gate_console as _gate_console # noqa: E402 + + _gate_console.register_contract_schema_source( + Path(_OPENXFACTORY_HOST.__file__).resolve().parents[1] / "contracts" / "schemas") diff --git a/tests/test_gate_console_schema_source.py b/tests/test_gate_console_schema_source.py new file mode 100644 index 0000000..79152db --- /dev/null +++ b/tests/test_gate_console_schema_source.py @@ -0,0 +1,217 @@ +"""The gate console's contract schemas, each read where its owner keeps it +(plan 038 T021, U-2; R2Q8 (a); P4F-4, ruled with plan 038 on openxFactory#656 +comment 6013547504; R1Q27 (a), comment 5851950767). + +`openxdox.gate_console` validates two documents against two schemas with two +owners: + * a gate-action record, against openXdox-spec's schema, which this + distribution packages (`openxdox.contracts`, held to its digest by + `copies.yaml`). The console reads it from there; + * a demotion execution receipt, against openxFactory's schema, which nothing + here vendors. The console reads it from the source a host registers with + `register_contract_schema_source()`, and with none registered it refuses, + naming that call. + +THESE CASES RUN IN A LONE CHECKOUT, so in F9.1 and in the required job. The +console imports openxFactory's `doc_health` when it loads (R1Q6 (d)'s reason), +though nothing these 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 console's import reads and nothing else. This process never imports the +console through the stand-in. The child's import path is set whole, so in a +composed run openxFactory's `doc_health` is not on it either, and no source is +registered there unless the case registers one. + +A CREATED file: no manifest row (RULED OQ-C). +""" + +from __future__ import annotations + +import ast +import json +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +SOURCE = Path(__file__).resolve().parents[1] / "src" +GATE_CONSOLE = SOURCE / "openxdox" / "gate_console.py" + +#: The names the console's import reads from `doc_health`: `corpus` for +#: `STATUS_SCAN_LINES` (gate_console.py), and `lines` for `split_keepends` and +#: `join_rows` (round_trip.py). Nothing these cases run calls either function. +_STAND_IN = { + "__init__.py": "", + "corpus.py": "STATUS_SCAN_LINES = 0\n", + "lines.py": ("def split_keepends(text):\n" + " raise AssertionError('the doc_health stand-in')\n\n\n" + "def join_rows(rows):\n" + " raise AssertionError('the doc_health stand-in')\n"), +} + +#: Run first in every child: the console, a valid gate-action record and a +#: receipt, and `refusal()`, which answers None where the call passes and the +#: refusal's type and words where it refuses. +_PRELUDE = ''' +import json +import shutil +import sys +from pathlib import Path + +from openxdox import contracts +from openxdox import gate_console as gc + +out = {} +RECORD = gc.build_gate_action_record( + actor="brett", action="demote", at="2026-10-06T00:00:00Z", reason="why", + change_id="add-x", artifacts=[{"kind": "transition-manifest", + "reference": "ideation/staging/x/m.yaml"}]) +RECEIPT = {"kind": "from-a", "destination": {"id": "x", "path": "ideation/staging/x"}, + "transition_manifest": "ideation/staging/x/m.yaml"} + + +def refusal(call, *args): + try: + call(*args) + except (gc.GateRefused, gc.ContractSchemaSourceRefused) as exc: + return f"{type(exc).__name__}: {exc}" + return None +''' + + +def _in_a_child(tmp_path: Path, body: str) -> dict: + """Run `body` after the prelude in a fresh interpreter whose import path is + the `doc_health` stand-in and this checkout's `src/`, and answer the + `out` mapping it prints.""" + stand_in = tmp_path / "stand-in" / "doc_health" + stand_in.mkdir(parents=True) + for name, text in _STAND_IN.items(): + (stand_in / name).write_text(text, encoding="utf-8") + script = tmp_path / "case.py" + script.write_text(_PRELUDE + textwrap.dedent(body) + + "\nprint(json.dumps(out))\n", encoding="utf-8") + env = {key: value for key, value in os.environ.items() + if key != "PYTHONPATH"} + env["PYTHONPATH"] = os.pathsep.join((str(stand_in.parent), str(SOURCE))) + env["PYTHONDONTWRITEBYTECODE"] = "1" + run = subprocess.run([sys.executable, str(script)], cwd=tmp_path, env=env, + capture_output=True, text=True, check=False) + assert run.returncode == 0, run.stderr + return json.loads(run.stdout.splitlines()[-1]) + + +def _receipt_source(directory: Path, *, kind: str, extra: dict | None = None) -> Path: + """A host's contract schema directory, stood in: its receipt schema admits + one `kind`. `extra` adds further schema files beside it.""" + directory.mkdir(parents=True) + schema = {"type": "object", "required": ["kind", "destination"], + "properties": {"kind": {"const": kind}}} + (directory / "demotion-execution-receipt.schema.yaml").write_text( + json.dumps(schema), encoding="utf-8") + for name, text in (extra or {}).items(): + (directory / name).write_text(text, encoding="utf-8") + return directory + + +def test_a_receipt_refuses_by_name_where_no_host_registered_its_schema(tmp_path) -> None: + out = _in_a_child(tmp_path, ''' + out["registered"] = gc.contract_schema_source_registered() + out["refusal"] = refusal(gc.validate_demotion_execution_receipt, RECEIPT) + ''') + assert out["registered"] is False + assert out["refusal"].startswith("GateRefused: the demotion execution receipt cannot be validated") + assert "demotion-execution-receipt.schema.yaml" in out["refusal"] + assert ("openxdox.gate_console.register_contract_schema_source()" + in out["refusal"]) + + +def test_a_gate_action_record_is_read_from_the_packaged_copy(tmp_path) -> None: + """With no source registered the record validates; a record the schema + does not admit refuses; a registered source whose own record schema admits + nothing is never read for a record; and a packaged copy that drifted from + its digest refuses.""" + hostile = _receipt_source(tmp_path / "hostile", kind="from-a", extra={ + "gate-action-record.schema.yaml": json.dumps({"not": {}})}) + out = _in_a_child(tmp_path, f''' + out["valid"] = refusal(gc.validate_gate_action_record, RECORD) + out["not_admitted"] = refusal(gc.validate_gate_action_record, + dict(RECORD, kind="another-kind")) + gc.register_contract_schema_source({str(hostile)!r}) + out["beside_a_hostile_source"] = refusal(gc.validate_gate_action_record, RECORD) + staged = Path({str(tmp_path / "staged")!r}) + shutil.copytree(contracts.package_dir(), staged, + ignore=shutil.ignore_patterns("__pycache__", "*.py")) + drifted = staged / "schemas" / "gate-action-record.schema.yaml" + drifted.write_bytes(drifted.read_bytes() + b"\\n") + contracts.package_dir = lambda: staged + out["drifted"] = refusal(gc.validate_gate_action_record, RECORD) + ''') + assert out["valid"] is None + assert out["not_admitted"].startswith( + "GateRefused: the gate-action record failed schema validation: kind") + assert out["beside_a_hostile_source"] is None + assert out["drifted"].startswith( + "GateRefused: the gate-action record schema could not be read from " + "openXdox's packaged copy") + assert "is not the spec leg's file" in out["drifted"] + + +def test_a_receipt_is_validated_against_the_registered_sources_schema(tmp_path) -> None: + source = _receipt_source(tmp_path / "host" / "contracts" / "schemas", kind="from-a") + out = _in_a_child(tmp_path, f''' + out["returned"] = str(gc.register_contract_schema_source({str(source)!r})) + out["registered"] = gc.contract_schema_source_registered() + out["valid"] = refusal(gc.validate_demotion_execution_receipt, RECEIPT) + out["not_admitted"] = refusal(gc.validate_demotion_execution_receipt, + dict(RECEIPT, kind="from-b")) + ''') + assert out["returned"] == str(source.resolve()) + assert out["registered"] is True + assert out["valid"] is None + assert out["not_admitted"].startswith( + "GateRefused: the demotion execution receipt failed schema validation: kind") + + +def test_the_seam_holds_one_source_and_refuses_a_second(tmp_path) -> None: + first = _receipt_source(tmp_path / "a", kind="from-a") + second = _receipt_source(tmp_path / "b", kind="from-b") + empty = tmp_path / "c" + empty.mkdir() + out = _in_a_child(tmp_path, f''' + out["first"] = str(gc.register_contract_schema_source(Path({str(first)!r}))) + out["again"] = str(gc.register_contract_schema_source({str(first)!r} + "/.")) + out["second"] = refusal(gc.register_contract_schema_source, {str(second)!r}) + out["still_first"] = refusal(gc.validate_demotion_execution_receipt, RECEIPT) + gc.unregister_contract_schema_source() + out["after_unregister"] = gc.contract_schema_source_registered() + out["swapped"] = str(gc.register_contract_schema_source({str(second)!r})) + out["now_second"] = refusal(gc.validate_demotion_execution_receipt, + dict(RECEIPT, kind="from-b")) + gc.unregister_contract_schema_source() + out["no_schema"] = refusal(gc.register_contract_schema_source, {str(empty)!r}) + out["after_no_schema"] = gc.contract_schema_source_registered() + ''') + assert out["first"] == out["again"] == str(first.resolve()) + assert out["second"].startswith( + "ContractSchemaSourceRefused: a contract schema source is already registered") + assert str(first.resolve()) in out["second"] and str(second.resolve()) in out["second"] + assert out["still_first"] is None + assert out["after_unregister"] is False + assert out["swapped"] == str(second.resolve()) + assert out["now_second"] is None + assert out["no_schema"].startswith( + f"ContractSchemaSourceRefused: {empty.resolve()} holds no " + "demotion-execution-receipt.schema.yaml") + assert out["after_no_schema"] is False + + +def test_the_console_reads_no_schema_from_beside_the_checkout() -> None: + """Before T021 both schemas were read from `parents[2]` of the module, + which in this leg is the checkout root. No `parents` index is read in the + console's code any more (its comments may still say where it was).""" + tree = ast.parse(GATE_CONSOLE.read_text(encoding="utf-8")) + reaches = [node.lineno for node in ast.walk(tree) + if isinstance(node, ast.Subscript) + and isinstance(node.value, ast.Attribute) + and node.value.attr == "parents"] + assert reaches == [], f"gate_console.py reads a parents index at lines {reaches}" From c40af91b9b78b37629c72715c10498cdbf051029 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:11:01 +0000 Subject: [PATCH 03/13] T022, U-3: the runbook placed from the composed tree, git-ignored, nothing vendored (plan 038) tests/test_session_runbook.py (its RUNBOOK) and tests/test_session_notebook.py read docs/ideation-dashboard-session-runbook.md at this checkout, where the runbook sat before the carve sent it to openDox-spec. The new scripts/composed_placements.py links it, for a composed run, from the composed tree (openDox/spec/docs/ under the openxFactory checkout) into docs/. .gitignore names the placed path, so the checkout stays clean and nothing placed can be committed. Before it places anything, the script checks four things and refuses (exit 2), naming the remedy, if any fails: * the spec checkout's HEAD is the commit the openDox root's spec gitlink pins; * the runbook is tracked there, unmodified; * the destination is ignored by .gitignore; * anything already at the destination is the script's own link to that same source. It places all or nothing. --remove takes back the script's own links and nothing else. The composed workflow (T029) and the documented local composed run both call it. Composed, the 10 runbook nodes turn green on T021's tree, 12.5's suites read 82 red of the 174, and no green node turns red. Refs opensoft/openxFactory#656 (CLAIMED 6013608959). Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 --- .gitignore | 4 + scripts/composed_placements.py | 207 +++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100755 scripts/composed_placements.py diff --git a/.gitignore b/.gitignore index 2fb4cee..2060e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ venv/ *.swp .idea/ .vscode/ + +# What scripts/composed_placements.py places for a composed run (plan 038 +# T022): linked from the composed tree, never committed. +/docs/ideation-dashboard-session-runbook.md diff --git a/scripts/composed_placements.py b/scripts/composed_placements.py new file mode 100755 index 0000000..ae22adf --- /dev/null +++ b/scripts/composed_placements.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Place, for a COMPOSED run, the files this checkout's suites read from the +composed tree (plan 038 T022, U-3; R2Q8 (a); P4F-3, ruled with plan 038 on +opensoft/openxFactory#656 comment 6013547504). + +WHY IT EXISTS. 12.5's falsifier runs openxFactory's governed suites in this +checkout, composed: openxFactory checked out with its submodules initialized +recursively, and its `scripts/` on `PYTHONPATH` (R1Q23 (a)). Two of those +suites read the session runbook at this checkout's +`docs/ideation-dashboard-session-runbook.md`, which is where it sat before the +carve: `tests/test_session_runbook.py` (its module constant `RUNBOOK`) and +`tests/test_session_notebook.py` (inline). The carve sent the runbook to +openDox-spec (`docs/opendox-carve-manifest.yaml`, in openxFactory), and this +checkout carries none. Neither suite may be edited outside an allow-list +entry, and a module constant lies inside no test. + +WHY A PLACEMENT AND NOT A COPY. openXdox-code's copy record +(`src/openxdox/contracts/copies.yaml`) is single-source: openXdox-spec, at one +commit, and the consumer validator's three kinds. An openDox-spec document +cannot be a row in it, and a second copy of it here would be one more copy to +keep in step. So the runbook is LINKED, for the run, from the composed tree, +where it is openDox-spec's own file at the commit the openDox root pins. The +placed path is in `.gitignore`, so the checkout stays clean and nothing placed +can be committed. + +WHAT IT CHECKS BEFORE IT PLACES ANYTHING, refusing (exit 2) and naming the +remedy otherwise: + * each source is a file openDox-spec TRACKS, UNMODIFIED, in a spec checkout + whose HEAD is the commit the openDox root's `spec` gitlink names. So the + run reads the pinned document, and not whatever a working tree holds; + * each destination is ignored by this checkout's `.gitignore`; + * a destination already present is this script's own link to that same + source. Anything else there is refused, never overwritten. +It places all or nothing: every check passes before the first link is made. + +USAGE, from anywhere: + + python3 scripts/composed_placements.py --openxfactory "$OPENXFACTORY" + python3 scripts/composed_placements.py --openxfactory "$OPENXFACTORY" --remove + +`--remove` takes back this script's own links and nothing else. The composed +workflow (T029) and the documented local composed run both call it before +they run the suites. The standard library and `git` are all it needs. + +A CREATED file: no row in openxFactory's `docs/opendox-carve-manifest.yaml` +(RULED OQ-C). +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path, PurePosixPath + +CHECKOUT = Path(__file__).resolve().parents[1] + +#: The openDox root inside the openxFactory checkout, and its `spec` leg. +OPENDOX_ROOT = PurePosixPath("openDox") +SPEC_LEG = PurePosixPath("spec") + + +@dataclass(frozen=True) +class Placement: + """One file the composed run places: `source`, a path inside openDox-spec, + linked to `destination`, a path inside this checkout.""" + + source: PurePosixPath + destination: PurePosixPath + + +#: Every placement, and the only ones. A new one comes with its reader's +#: reason here and its path in `.gitignore`. +PLACEMENTS: tuple[Placement, ...] = ( + # The session runbook: read by tests/test_session_runbook.py (`RUNBOOK`) + # and tests/test_session_notebook.py, at this checkout's `docs/`. + Placement(PurePosixPath("docs/ideation-dashboard-session-runbook.md"), + PurePosixPath("docs/ideation-dashboard-session-runbook.md")), +) + + +class PlacementRefused(Exception): + """A placement this script will not make, with the reason and the remedy.""" + + +def _git(*args: str, cwd: Path) -> subprocess.CompletedProcess: + try: + return subprocess.run(["git", *args], cwd=cwd, capture_output=True, + text=True, check=False) + except FileNotFoundError as exc: + raise PlacementRefused( + "git is not on PATH, and the placements are checked with it") from exc + + +def _pinned_spec(openxfactory: Path) -> Path: + """The spec checkout, once its HEAD is the commit the openDox root pins.""" + root = openxfactory / OPENDOX_ROOT + spec = root / SPEC_LEG + pinned = _git("rev-parse", "--verify", f"HEAD:{SPEC_LEG}", cwd=root) \ + if root.is_dir() else None + if pinned is None or pinned.returncode != 0: + raise PlacementRefused( + f"{root} is not an openDox root checkout with a `spec` gitlink. " + "Check openxFactory out with its submodules initialized " + "recursively (`git submodule update --init --recursive`).") + head = _git("rev-parse", "--verify", "HEAD", cwd=spec) if spec.is_dir() else None + if head is None or head.returncode != 0: + raise PlacementRefused( + f"{spec} is not checked out. Initialize openxFactory's submodules " + "recursively (`git submodule update --init --recursive`).") + if head.stdout.strip() != pinned.stdout.strip(): + raise PlacementRefused( + f"{spec} is at {head.stdout.strip()}, and the openDox root pins " + f"{pinned.stdout.strip()}. The run must read the pinned document: " + "run `git submodule update --init --recursive` in the openxFactory " + "checkout.") + return spec + + +def _check(placement: Placement, spec: Path) -> tuple[Path, Path]: + """The placement's source and destination, once every check has passed.""" + source = spec / placement.source + tracked = _git("ls-files", "--error-unmatch", "--", str(placement.source), cwd=spec) + if tracked.returncode != 0 or not source.is_file(): + raise PlacementRefused( + f"openDox-spec does not track {placement.source} at its pinned " + "commit, so there is nothing pinned to place") + clean = _git("diff", "--quiet", "HEAD", "--", str(placement.source), cwd=spec) + if clean.returncode != 0: + raise PlacementRefused( + f"{source} differs from openDox-spec's pinned commit. The run must " + "read the pinned document; restore it with `git checkout -- " + f"{placement.source}` in {spec}.") + destination = CHECKOUT / placement.destination + ignored = _git("check-ignore", "-q", "--", str(placement.destination), + cwd=CHECKOUT) + if ignored.returncode != 0: + raise PlacementRefused( + f"{placement.destination} is not ignored by this checkout's " + ".gitignore, so a placed file could be committed. Name it there.") + if os.path.lexists(destination): + if not (destination.is_symlink() + and Path(os.readlink(destination)) == source): + raise PlacementRefused( + f"{placement.destination} already exists and is not this " + f"script's link to {source}. It is not overwritten; move it " + "away first.") + return source, destination + + +def place(openxfactory: Path) -> list[str]: + """Make every placement, all or nothing. Answers what was done.""" + spec = _pinned_spec(openxfactory.resolve()) + checked = [(placement, *_check(placement, spec)) for placement in PLACEMENTS] + done = [] + for placement, source, destination in checked: + if destination.is_symlink(): + done.append(f"already placed {placement.destination} -> {source}") + continue + destination.parent.mkdir(parents=True, exist_ok=True) + destination.symlink_to(source) + done.append(f"placed {placement.destination} -> {source}") + return done + + +def remove(openxfactory: Path) -> list[str]: + """Take back this script's own links, and nothing else.""" + spec = openxfactory.resolve() / OPENDOX_ROOT / SPEC_LEG + done = [] + for placement in PLACEMENTS: + destination = CHECKOUT / placement.destination + if not os.path.lexists(destination): + continue + if not (destination.is_symlink() + and Path(os.readlink(destination)) == spec / placement.source): + raise PlacementRefused( + f"{placement.destination} is not this script's link, so it " + "is not removed") + destination.unlink() + done.append(f"removed {placement.destination}") + return done + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Place, for a composed run, the files this checkout's " + "suites read from the composed tree (plan 038 T022).") + parser.add_argument("--openxfactory", required=True, type=Path, + help="the openxFactory checkout of the composed run, " + "its submodules initialized recursively") + parser.add_argument("--remove", action="store_true", + help="take back this script's own links") + args = parser.parse_args(argv) + try: + done = (remove if args.remove else place)(args.openxfactory) + except PlacementRefused as exc: + print(f"composed_placements: refused: {exc}", file=sys.stderr) + return 2 + for line in done: + print(f"composed_placements: {line}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ecf0ba9bd4f2b54af66b7022ac1fc8c68005a89c Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:20:19 +0000 Subject: [PATCH 04/13] T021: a host schema the console cannot apply refuses as GateRefused (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 --- src/openxdox/gate_console.py | 49 ++++++++++++++++++++---- tests/test_gate_console_schema_source.py | 35 +++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/openxdox/gate_console.py b/src/openxdox/gate_console.py index b6dbe45..eba3773 100644 --- a/src/openxdox/gate_console.py +++ b/src/openxdox/gate_console.py @@ -636,20 +636,20 @@ def _validate_contract_document( """Validate one document against a released schema's text, which its caller reads where the schema's owner keeps it: `_packaged_schema_text` or `_host_schema_text`, under "THE CONTRACT SCHEMAS THE CONSOLE VALIDATES - AGAINST" near the end of this module (plan 038 T021).""" + AGAINST" near the end of this module (plan 038 T021). + + A schema that is not YAML, is not a valid JSON Schema, or cannot be + applied is refused there too, by `_schema_errors`, as `GateRefused`. So + a host's broken schema reaches a caller as a failing document does, and + never as another exception.""" try: from jsonschema import Draft202012Validator, FormatChecker except ImportError as exc: # pragma: no cover - release dependency guard raise GateRefused( f"jsonschema is required to validate {label}") from exc - try: - schema = yaml.safe_load(schema_text) - except yaml.YAMLError as exc: - raise GateRefused( - f"the {label} schema could not be parsed: {exc}") from exc errors = sorted( - Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors( - dict(document)), + _schema_errors(document, schema_text, label=label, + validator=Draft202012Validator, formats=FormatChecker), key=lambda error: tuple(str(part) for part in error.absolute_path)) if errors: detail = "; ".join( @@ -2440,6 +2440,39 @@ def _packaged_schema_text(copy_id: str, *, label: str) -> str: f"copy: {exc}") from exc +def _schema_errors(document: Mapping[str, Any], schema_text: str, *, + label: str, validator: Any, formats: Any) -> list: + """`document`'s errors against the schema `schema_text` holds. + + The schema itself is checked first, and refused as `GateRefused` where it + is not YAML, where it is not a valid JSON Schema under the validator's own + meta-schema (an empty document, `required: 1`), and where applying it + raises (an unresolvable `$ref`). A host supplies the receipt schema, and + the demotion verb writes its receipt after it has moved files: it reports + a `GateRefused` there as an executed demotion whose receipt was not + written, where any other exception would end it with a traceback.""" + from jsonschema.exceptions import SchemaError + + try: + schema = yaml.safe_load(schema_text) + except yaml.YAMLError as exc: + raise GateRefused( + f"the {label} schema could not be parsed: {exc}") from exc + try: + validator.check_schema(schema) + except SchemaError as exc: + raise GateRefused( + f"the {label} schema is not a valid JSON Schema: {exc.message}" + ) from exc + try: + return list(validator(schema, format_checker=formats()).iter_errors( + dict(document))) + except Exception as exc: # noqa: BLE001 - any failure to apply it refuses + raise GateRefused( + f"the {label} schema could not be applied: " + f"{type(exc).__name__}: {exc}") from exc + + def _host_schema_text(filename: str, *, label: str) -> str: """A schema the host supplies, from the source it registered.""" source = _contract_schema_source diff --git a/tests/test_gate_console_schema_source.py b/tests/test_gate_console_schema_source.py index 79152db..2427cf0 100644 --- a/tests/test_gate_console_schema_source.py +++ b/tests/test_gate_console_schema_source.py @@ -205,6 +205,41 @@ def test_the_seam_holds_one_source_and_refuses_a_second(tmp_path) -> None: assert out["after_no_schema"] is False +def test_a_host_schema_the_console_cannot_apply_refuses_as_gate_refused(tmp_path) -> None: + """A registered source whose receipt schema is empty, is not a valid JSON + Schema, or cannot be applied refuses as `GateRefused`, never as another + exception. The demotion verb writes its receipt after it has moved files, + and reports only a `GateRefused` there as a receipt not written (Copilot + on #40, r4194497045).""" + sources = {} + for name, text in {"empty": "", "not_a_schema": "required: 1\n", + "unresolvable": json.dumps({"$ref": "#/$defs/absent"})}.items(): + directory = tmp_path / name + directory.mkdir() + (directory / "demotion-execution-receipt.schema.yaml").write_text( + text, encoding="utf-8") + sources[name] = str(directory) + out = _in_a_child(tmp_path, f''' + for name, source in {sources!r}.items(): + gc.unregister_contract_schema_source() + gc.register_contract_schema_source(source) + try: + gc.validate_demotion_execution_receipt(RECEIPT) + out[name] = None + except gc.GateRefused as exc: + out[name] = f"GateRefused: {{exc}}" + except Exception as exc: + out[name] = f"ESCAPED {{type(exc).__name__}}: {{exc}}" + ''') + for name in ("empty", "not_a_schema"): + assert out[name].startswith( + "GateRefused: the demotion execution receipt schema is not a valid " + "JSON Schema"), out[name] + assert out["unresolvable"].startswith( + "GateRefused: the demotion execution receipt schema could not be " + "applied"), out["unresolvable"] + + def test_the_console_reads_no_schema_from_beside_the_checkout() -> None: """Before T021 both schemas were read from `parents[2]` of the module, which in this leg is the checkout root. No `parents` index is read in the From 7a149b70c815514beca4aa6bd14a7653acb1060a Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:40:44 +0000 Subject: [PATCH 05/13] T022: the placement script's guards held in CI, tests/test_composed_placements.py (Copilot r4194596317; holder ruling (a)) The holder's ruling on opensoft/openxFactory#656 (comment 6016356225, option (a)) adds tests/test_composed_placements.py to T022's files. The script changes the file system, so its guards now live in the suite CI runs, not in a scratch harness. Each case runs the script in a child interpreter, as the composed run calls it. The script under test is a copy of this checkout's script, inside a scratch checkout that carries this checkout's own .gitignore, and the tree it reads is a scratch openxFactory/openDox/spec tree pinned to commit A. No case writes into this checkout. 14 cases cover: * placing, with the checkout left clean; * the no-op of placing again; * --remove; * a spec off its pin; * a modified runbook; * an untracked runbook; * an uninitialized spec leg; * an uninitialized openDox root; * a destination .gitignore does not name; * no git; * a file, or a link elsewhere, at the destination, kept under both place and --remove. No case skips, and the file carries none of 12.5's governed-suite markers. Writing the uninitialized-leg case found a misreport in the script, now fixed. An uninitialized spec leg is an empty directory, so git answered for the openDox root above it, and the refusal read "spec is at ". The script now asks git about the leg only when the leg has its own .git, so the refusal reads "is not checked out" with the remedy. It refused before as well; only the message was wrong. Refs opensoft/openxFactory#656 (CLAIMED 6013608959). Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 --- scripts/composed_placements.py | 5 +- tests/test_composed_placements.py | 276 ++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 tests/test_composed_placements.py diff --git a/scripts/composed_placements.py b/scripts/composed_placements.py index ae22adf..3e232af 100755 --- a/scripts/composed_placements.py +++ b/scripts/composed_placements.py @@ -105,7 +105,10 @@ def _pinned_spec(openxfactory: Path) -> Path: f"{root} is not an openDox root checkout with a `spec` gitlink. " "Check openxFactory out with its submodules initialized " "recursively (`git submodule update --init --recursive`).") - head = _git("rev-parse", "--verify", "HEAD", cwd=spec) if spec.is_dir() else None + # Its own `.git`: an uninitialized leg is an empty directory, and git + # would answer for the openDox root above it. + head = _git("rev-parse", "--verify", "HEAD", cwd=spec) \ + if (spec / ".git").exists() else None if head is None or head.returncode != 0: raise PlacementRefused( f"{spec} is not checked out. Initialize openxFactory's submodules " diff --git a/tests/test_composed_placements.py b/tests/test_composed_placements.py new file mode 100644 index 0000000..ee808f2 --- /dev/null +++ b/tests/test_composed_placements.py @@ -0,0 +1,276 @@ +"""`scripts/composed_placements.py`, held in CI (plan 038 T022, U-3; option (a) +of the holder's ruling on opensoft/openxFactory#656, comment 6016356225). + +WHY HERE. The script makes links in this checkout's working tree for a +composed run, and refuses (exit 2) whatever would let that run read anything +but the pinned document, or overwrite or take away anything but its own link. +A script that changes the file system keeps its guards in the suite CI runs, +so each guard below is a case. + +HOW. Each case runs the script as the composed run calls it: in a child +interpreter, against a scratch openxFactory tree whose openDox root pins +commit A of a scratch spec repository, where commit B changes the runbook. +The script it runs is a COPY of this checkout's script, inside a scratch +checkout that also carries a copy of this checkout's own `.gitignore`. The +script places relative to its own location, so no case writes into this +checkout. And the `.gitignore` the cases read is the one this checkout +commits, so a placement it stops ignoring turns the first case red. + +The standard library, `git` and pytest are all it needs; no case skips. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +import pytest + +CHECKOUT = Path(__file__).resolve().parents[1] +SCRIPT = Path("scripts") / "composed_placements.py" + +#: The one placement: openDox-spec's session runbook, at the same path here. +RUNBOOK = "docs/ideation-dashboard-session-runbook.md" + + +def _environment(**overrides: str) -> dict[str, str]: + """The child's environment: no inherited `GIT_*` (a hook's `GIT_DIR` + would aim every call at another repository), no user or system git + configuration, and a fixed identity for the scratch commits.""" + environment = {key: value for key, value in os.environ.items() + if not key.startswith("GIT_")} + environment.update( + GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1", + GIT_AUTHOR_NAME="composed placements", GIT_AUTHOR_EMAIL="cases@invalid", + GIT_COMMITTER_NAME="composed placements", + GIT_COMMITTER_EMAIL="cases@invalid") + environment.update(overrides) + return environment + + +def _git(*args: str, cwd: Path) -> str: + return subprocess.run(["git", *args], cwd=cwd, env=_environment(), + check=True, capture_output=True, text=True).stdout.strip() + + +@dataclass +class Composed: + """The scratch checkout, the scratch composed tree, and the script.""" + + checkout: Path + openxfactory: Path + root: Path + spec: Path + + @property + def destination(self) -> Path: + return self.checkout / RUNBOOK + + @property + def source(self) -> Path: + return self.spec / RUNBOOK + + @property + def pinned(self) -> str: + return _git("rev-parse", "HEAD:spec", cwd=self.root) + + def run(self, *extra: str, **environment: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(self.checkout / SCRIPT), + "--openxfactory", str(self.openxfactory), *extra], + capture_output=True, text=True, env=_environment(**environment)) + + def refused(self, *extra: str, **environment: str) -> str: + """Run the script, hold it to a refusal, and answer what it said.""" + result = self.run(*extra, **environment) + assert result.returncode == 2, (result.returncode, result.stdout, + result.stderr) + assert result.stdout == "", result.stdout + assert result.stderr.startswith("composed_placements: refused: "), \ + result.stderr + return result.stderr + + +def _spec_repository(spec: Path) -> str: + """A spec repository whose commit A holds runbook A and whose commit B + changes it; checked out at A. Answers A.""" + spec.mkdir(parents=True) + _git("init", "-q", cwd=spec) + (spec / RUNBOOK).parent.mkdir(parents=True) + (spec / RUNBOOK).write_text("runbook A\n", encoding="utf-8") + _git("add", RUNBOOK, cwd=spec) + _git("commit", "-q", "-m", "A", cwd=spec) + commit_a = _git("rev-parse", "HEAD", cwd=spec) + (spec / RUNBOOK).write_text("runbook B\n", encoding="utf-8") + _git("commit", "-q", "-a", "-m", "B", cwd=spec) + _git("checkout", "-q", commit_a, cwd=spec) + return commit_a + + +def _opendox_root(root: Path, pinned: str) -> None: + """An openDox root whose `spec` gitlink pins `pinned`.""" + root.mkdir(parents=True, exist_ok=True) + _git("init", "-q", cwd=root) + _git("update-index", "--add", "--cacheinfo", f"160000,{pinned},spec", + cwd=root) + _git("commit", "-q", "-m", "the openDox root", cwd=root) + + +@pytest.fixture +def composed(tmp_path: Path) -> Composed: + checkout = tmp_path / "openXdox-code" + (checkout / SCRIPT).parent.mkdir(parents=True) + shutil.copyfile(CHECKOUT / SCRIPT, checkout / SCRIPT) + shutil.copyfile(CHECKOUT / ".gitignore", checkout / ".gitignore") + _git("init", "-q", cwd=checkout) + _git("add", ".gitignore", str(SCRIPT), cwd=checkout) + _git("commit", "-q", "-m", "the scratch checkout", cwd=checkout) + openxfactory = tmp_path / "openxFactory" + root = openxfactory / "openDox" + spec = root / "spec" + _opendox_root(root, _spec_repository(spec)) + return Composed(checkout, openxfactory, root, spec) + + +# --- what it places ------------------------------------------------------- + + +def test_a_pinned_unmodified_runbook_is_linked_and_leaves_the_checkout_clean( + composed): + result = composed.run() + assert result.returncode == 0, result.stderr + assert result.stdout == f"composed_placements: placed {RUNBOOK} -> " \ + f"{composed.source.resolve()}\n" + assert composed.destination.is_symlink() + assert Path(os.readlink(composed.destination)) == composed.source.resolve() + assert composed.destination.read_text(encoding="utf-8") == "runbook A\n" + # Ignored by the checkout's own `.gitignore`: nothing placed can be committed. + assert _git("status", "--porcelain", "--untracked-files=all", + cwd=composed.checkout) == "" + + +def test_placing_again_is_a_no_op(composed): + assert composed.run().returncode == 0 + result = composed.run() + assert result.returncode == 0, result.stderr + assert result.stdout.startswith(f"composed_placements: already placed {RUNBOOK}") + assert composed.destination.read_text(encoding="utf-8") == "runbook A\n" + + +def test_remove_takes_back_its_own_link_and_nothing_else(composed): + assert composed.run().returncode == 0 + result = composed.run("--remove") + assert result.returncode == 0, result.stderr + assert result.stdout == f"composed_placements: removed {RUNBOOK}\n" + assert not os.path.lexists(composed.destination) + assert composed.source.read_text(encoding="utf-8") == "runbook A\n" + # And with nothing placed, there is nothing to take back. + again = composed.run("--remove") + assert (again.returncode, again.stdout) == (0, ""), again.stderr + + +# --- what it refuses, placing nothing -------------------------------------- + + +def test_a_spec_checkout_off_the_opendox_roots_pin_is_refused(composed): + _git("checkout", "-q", "-", cwd=composed.spec) # commit B + said = composed.refused() + assert f"and the openDox root pins {composed.pinned}" in said + assert "git submodule update --init --recursive" in said + assert not os.path.lexists(composed.destination) + + +def test_a_modified_runbook_is_refused(composed): + composed.source.write_text("edited in the working tree\n", encoding="utf-8") + said = composed.refused() + assert "differs from openDox-spec's pinned commit" in said + assert not os.path.lexists(composed.destination) + + +def test_a_runbook_the_spec_does_not_track_is_refused(composed): + _git("rm", "-q", "--cached", RUNBOOK, cwd=composed.spec) + said = composed.refused() + assert f"openDox-spec does not track {RUNBOOK}" in said + assert not os.path.lexists(composed.destination) + + +def test_an_uninitialized_spec_leg_is_refused_as_not_checked_out(composed): + # What a composed checkout without `--recursive` holds: an empty directory, + # where git would answer for the openDox root above it. + shutil.rmtree(composed.spec) + composed.spec.mkdir() + said = composed.refused() + assert "is not checked out" in said + assert "git submodule update --init --recursive" in said + assert not os.path.lexists(composed.destination) + + +def test_an_uninitialized_opendox_root_is_refused(composed): + shutil.rmtree(composed.root) + composed.root.mkdir() + # An openxFactory checkout above it, whose own tree has no `spec`. + _git("init", "-q", cwd=composed.openxfactory) + (composed.openxfactory / "README.md").write_text("openxFactory\n", encoding="utf-8") + _git("add", "README.md", cwd=composed.openxfactory) + _git("commit", "-q", "-m", "openxFactory", cwd=composed.openxfactory) + said = composed.refused() + assert "is not an openDox root checkout with a `spec` gitlink" in said + assert not os.path.lexists(composed.destination) + + +def test_a_destination_the_gitignore_does_not_name_is_refused(composed): + gitignore = composed.checkout / ".gitignore" + text = gitignore.read_text(encoding="utf-8") + assert f"/{RUNBOOK}\n" in text, "this checkout's .gitignore names the runbook" + gitignore.write_text(text.replace(f"/{RUNBOOK}\n", ""), encoding="utf-8") + said = composed.refused() + assert "is not ignored by this checkout's .gitignore" in said + assert not os.path.lexists(composed.destination) + + +def test_without_git_on_path_it_refuses(composed, tmp_path): + nothing = tmp_path / "an-empty-PATH" + nothing.mkdir() + said = composed.refused(PATH=str(nothing)) + assert "git is not on PATH" in said + assert not os.path.lexists(composed.destination) + + +# --- what it never overwrites or takes away --------------------------------- + + +def _a_file_of_someone_elses(composed: Composed) -> None: + composed.destination.parent.mkdir(parents=True) + composed.destination.write_text("someone's own file\n", encoding="utf-8") + + +def _a_link_elsewhere(composed: Composed, tmp_path: Path) -> Path: + elsewhere = tmp_path / "another-runbook.md" + elsewhere.write_text("another runbook\n", encoding="utf-8") + composed.destination.parent.mkdir(parents=True) + composed.destination.symlink_to(elsewhere) + return elsewhere + + +@pytest.mark.parametrize("verb", [(), ("--remove",)], ids=["place", "remove"]) +def test_a_file_at_the_destination_is_refused_and_kept(composed, verb): + _a_file_of_someone_elses(composed) + said = composed.refused(*verb) + assert ("is not this script's link, so it is not removed" if verb + else "already exists and is not this script's link") in said + assert not composed.destination.is_symlink() + assert composed.destination.read_text(encoding="utf-8") == "someone's own file\n" + + +@pytest.mark.parametrize("verb", [(), ("--remove",)], ids=["place", "remove"]) +def test_a_link_elsewhere_at_the_destination_is_refused_and_kept(composed, + tmp_path, verb): + elsewhere = _a_link_elsewhere(composed, tmp_path) + said = composed.refused(*verb) + assert ("is not this script's link, so it is not removed" if verb + else "already exists and is not this script's link") in said + assert Path(os.readlink(composed.destination)) == elsewhere From d6f4a814adcb733807b1dbbfc497cfdd4010aae1 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:42:08 +0000 Subject: [PATCH 06/13] T029, U-9: the composed run, permanent: composed.yml and its openxFactory pin (plan 038) A new workflow, `.github/workflows/composed.yml` (job `composed`), runs F12.1, the falsifier of #1144's 12.5, composed with openxFactory, as F5.2 runs under R1Q23 (a). ARC-Q2 (a) (openxFactory#656 6003918488) makes the composition permanent (CF-5); N-7a gives it its own file and its own pin, `tests/composed_host_pin.yaml` (`schema_version`, `kind`, `commit`), first value openxFactory main 92010d3e. Both ruled with plan 038 (6013547504). The job: the package and its test extra in a fresh venv, with F9.2's pin assertion; openxFactory at the pinned commit, with only openDox, openXdox and openXwallet initialized recursively (installs/omnigent-install is private and never imported); T022's placement script; 12.5's governed set, computed with its floor of 16, each suite alone with PYTHONPATH="$OPENXFACTORY/scripts"; the oracle in 12.5's call; and the 32-entry help tree against its golden (R2Q3 (a)), which `validate.yml` leaves out. Its child scrubs PYTHONPATH, so the composed `scripts/` reaches it through a `.pth` in the run's own venv, written after the suites ran. Making `composed` a required check is the holder's act after the first green run on main (N-7b (a)); this commit changes no setting. Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/composed.yml | 311 +++++++++++++++++++++++++++++++++ tests/composed_host_pin.yaml | 33 ++++ 2 files changed, 344 insertions(+) create mode 100644 .github/workflows/composed.yml create mode 100644 tests/composed_host_pin.yaml diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml new file mode 100644 index 0000000..7d903e8 --- /dev/null +++ b/.github/workflows/composed.yml @@ -0,0 +1,311 @@ +name: composed + +# THE COMPOSED RUN, PERMANENT (plan 038 task T029, U-9, in openxFactory +# `specs/038-opendox-document-tool-self-maintenance/`). It runs F12.1, the +# falsifier of #1144's 12.5 ("the governed flow is unchanged"), where it can +# run: composed with openxFactory, as F5.2 runs under R1Q23 (a). In a lone +# checkout 15 of 12.5's 16 governed suites cannot even be collected, because +# they reach openxFactory's `doc_health`. ARC-Q2 (a) (openxFactory#656 comment +# 6003918488) makes this composition their permanent home (CF-5). Decision +# N-7a gives the run its own file and its own pin, apart from `validate.yml`. +# Both were ruled with plan 038 (comment 6013547504). +# +# WHAT IT RUNS, IN ORDER. +# 1. This checkout, with its whole history, because the oracle reads the +# arc's landings since ARC_BASE. +# 2. The package and its test extra, in a fresh venv, with `validate.yml`'s +# install command. So openDox arrives at the pin `pyproject.toml` names, +# and F9.2's pin assertion holds it there. +# 3. openxFactory, at the ONE commit `tests/composed_host_pin.yaml` names, +# with `openDox`, `openXdox` and `openXwallet` initialized recursively. +# Without them, `corpus_adapter_openxfactory` refuses at import. The run +# initializes ONLY those three. openxFactory's fourth submodule, +# `installs/omnigent-install`, is a private repository that this run never +# imports and that this repository's token cannot read, so a blanket +# recursive init would fail before any test ran. +# 4. T022's `scripts/composed_placements.py`, which links the composed tree's +# session runbook into `docs/` for the run. +# 5. 12.5's governed suites, computed the way its falsifier computes them +# (16 is the floor). Each suite runs alone, with +# `PYTHONPATH="$OPENXFACTORY/scripts"` (R1Q23 (a)'s form). +# 6. The oracle, `scripts/protected_suites.py`, in 12.5's call: no landing +# of the arc touched a governed suite outside an allow-list entry that +# holds. +# 7. The assembled `--help` tree, against the golden it records (R2Q3 (a): +# the hosts' help goldens are unchanged). `validate.yml` leaves that test +# out (`LEFT_OUT`), because building the tree needs `doc_health`. Composed, +# it can run. +# Steps 6 and 7 run even when step 5 is red, so one run reports every red. +# +# WHAT IS NOT HERE. Making `composed` a REQUIRED check is the holder's act, +# after this workflow's first green run on `main` (N-7b (a), ruled +# 6013547504), and it is recorded in T029's evidence. This file changes no +# repository setting. + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + composed: + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + shell: bash + working-directory: openXdox-code + env: + # The openxFactory checkout of the composed run, beside this one. + OPENXFACTORY: ${{ github.workspace }}/openxFactory + # openXdox-code's ARC_BASE, as plan 034 T003 recorded it (openxFactory + # `specs/034-opendox-standalone-operation/evidence/arc-base.md`). The + # oracle's landings are the commits from it to HEAD that carry the + # arc's trailer. + ARC_BASE: e28930bf052febff0c7bdd0463a72ce3c4f5e8fe + steps: + - uses: actions/checkout@v4 + with: + path: openXdox-code + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + # A fresh venv, as 12.5's falsifier makes one. Its `bin/` goes on PATH, + # so every later step's `python` is this one. + - name: install the package and its test extra, openDox at the pin + id: install + run: | + echo "COMPOSED=$RUNNER_TEMP/composed" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/composed" + python -m venv --clear "$RUNNER_TEMP/venv" + echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/venv/bin/python" -m pip install -e ".[test]" + # F9.2's pin assertion: the composition names a full-commit pin, and + # the openDox installed is that one. + "$RUNNER_TEMP/venv/bin/python" - <<'PY' + import importlib.metadata as md, json, pathlib, re, tomllib + deps = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["dependencies"] + pins = [d for d in deps if re.match(r"opendox\s*@", d)] + assert len(pins) == 1 and re.search(r"@[0-9a-f]{40}$", pins[0]), f"the composition names no full-commit pin: {pins}" + got = json.loads(md.distribution("opendox").read_text("direct_url.json") or "{}").get("vcs_info", {}).get("commit_id") + assert got == pins[0].rsplit("@", 1)[1], f"the installed openDox is {got!r}, not the pinned commit" + print(f"openDox installed at the pin: openDox-code@{got}") + PY + + # The pin file's rules are in its own header. A file that breaks one is + # refused, naming the rule, and nothing is checked out. + - name: read the host pin + id: pin + run: | + python - <<'PY' + import os, re, sys + import yaml + + PIN = "tests/composed_host_pin.yaml" + + + def refuse(why): + sys.exit(f"::error file={PIN}::{PIN} is refused: {why}") + + + class Strict(yaml.SafeLoader): + """Refuses a key given twice, which YAML would quietly collapse.""" + + + def mapping(loader, node, deep=False): + keys = [loader.construct_object(key, deep=deep) for key, _ in node.value] + twice = sorted({str(key) for key in keys if keys.count(key) > 1}) + if twice: + raise yaml.constructor.ConstructorError( + None, None, f"a key is given twice: {twice}", node.start_mark) + return loader.construct_mapping(node, deep=deep) + + + Strict.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, mapping) + try: + with open(PIN, encoding="utf-8") as handle: + pin = yaml.load(handle, Loader=Strict) + except (OSError, yaml.YAMLError) as exc: + refuse(f"it cannot be read as YAML ({exc})") + if not isinstance(pin, dict): + refuse("it is not a mapping") + keys = {"schema_version", "kind", "commit"} + if set(pin) != keys: + refuse(f"its keys are {sorted(map(str, pin))}, and they must be exactly {sorted(keys)}") + if type(pin["schema_version"]) is not int or pin["schema_version"] != 1: + refuse(f"schema_version is {pin['schema_version']!r}, not the integer 1") + if pin["kind"] != "composed-host-pin": + refuse(f"kind is {pin['kind']!r}, not 'composed-host-pin'") + commit = pin["commit"] + if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-f]{40}", commit): + refuse(f"commit is {commit!r}, not a full 40-character lowercase commit id") + for name, value in (("GITHUB_OUTPUT", f"commit={commit}"), + ("GITHUB_ENV", f"COMPOSED_HOST_COMMIT={commit}")): + with open(os.environ[name], "a", encoding="utf-8") as handle: + handle.write(value + "\n") + print(f"the composed run's host: opensoft/openxFactory@{commit}") + PY + + - uses: actions/checkout@v4 + with: + repository: opensoft/openxFactory + ref: ${{ steps.pin.outputs.commit }} + path: openxFactory + fetch-depth: 0 + persist-credentials: false + + # openxFactory's `.gitmodules` spells these three as ssh URLs, and this + # run holds no ssh key. All three, and the legs nested in them, are + # public, so the URLs are read over https. They are rewritten in this + # checkout's own `.git/config`, which `git submodule init` writes, and + # `.gitmodules` is not touched. + - name: initialize openDox, openXdox and openXwallet, recursively + id: submodules + working-directory: openxFactory + run: | + paths=(openDox openXdox openXwallet) + git submodule init -- "${paths[@]}" + for path in "${paths[@]}"; do + url=$(git config --get "submodule.$path.url") + case "$url" in + git@github.com:*) git config "submodule.$path.url" "https://github.com/${url#git@github.com:}" ;; + esac + done + git submodule update --init --recursive -- "${paths[@]}" + head=$(git rev-parse HEAD) + if [ "$head" != "$COMPOSED_HOST_COMMIT" ]; then + echo "::error::the openxFactory checkout is at $head, and tests/composed_host_pin.yaml pins $COMPOSED_HOST_COMMIT" + exit 1 + fi + echo "OPENXFACTORY (an openxFactory checkout) at $head" + git submodule status --recursive -- "${paths[@]}" + + - name: place the composed tree's files (T022) + id: placements + run: python scripts/composed_placements.py --openxfactory "$OPENXFACTORY" + + # 12.5's falsifier computes the set and holds it to a floor: every suite + # that drives `open-pr` or injects the fake. Fewer than 16 means a proof + # vanished. + - name: the governed set, computed + id: governed + run: | + status=0 + git grep -l -e 'open-pr' -e 'open_pr' -e 'FakePullRequests' -- 'tests/test_*.py' > "$COMPOSED/governed.txt" || status=$? + # git grep exits 1 when nothing matches, which the floor below names. + if [ "$status" -gt 1 ]; then + echo "::error::git grep could not read this checkout (exit $status)" + exit "$status" + fi + count=$(wc -l < "$COMPOSED/governed.txt") + echo "governed suites: $count (the floor is 16)" + cat "$COMPOSED/governed.txt" + if [ "$count" -lt 16 ]; then + echo "::error::12.5's governed set has $count suites, below its floor of 16: a proof vanished" + exit 1 + fi + + # Each suite runs alone, as 12.5's falsifier runs it. pytest's stdin is + # /dev/null, so no test can read the list this loop reads. Every suite + # runs whatever the one before it did, and the step fails at the end if + # any of them did. + - name: the governed suites, composed, each alone + id: suites + run: | + export PYTHONPATH="$OPENXFACTORY/scripts" + echo "OPENXFACTORY (an openxFactory checkout) at $(git -C "$OPENXFACTORY" rev-parse HEAD)" + echo "python $(python -c 'import platform; print(platform.python_version())'), $(python -m pytest --version 2>&1), node $(node --version 2>&1)" + mkdir -p "$COMPOSED/reports" + red=0 + while read -r suite <&3; do + echo "::group::$suite" + rc=0 + python -m pytest -q -p no:cacheprovider -rfE \ + --junitxml="$COMPOSED/reports/$(basename "$suite" .py).xml" "$suite" < /dev/null || rc=$? + echo "::endgroup::" + echo "$suite: pytest exit $rc" + if [ "$rc" -ne 0 ]; then red=1; fi + done 3< "$COMPOSED/governed.txt" + python - "$COMPOSED" <<'PY' + import os, pathlib, sys + import xml.etree.ElementTree as ET + + out = pathlib.Path(sys.argv[1]) + suites = [l.strip() for l in (out / "governed.txt").read_text().splitlines() if l.strip()] + rows, red, total = [], set(), dict.fromkeys(("passed", "failed", "errors", "skipped"), 0) + for suite in suites: + report = out / "reports" / (pathlib.PurePosixPath(suite).stem + ".xml") + if not report.exists(): + rows.append(f"{suite}: NO REPORT (pytest did not finish)") + red.add(suite) + continue + root = ET.parse(report).getroot() + n = dict.fromkeys(("tests", "failures", "errors", "skipped"), 0) + module = suite[:-3].replace("/", ".") + for ts in (root.iter("testsuite") if root.tag == "testsuites" else [root]): + for key in n: + n[key] += int(ts.get(key, 0)) + for case in ts.iter("testcase"): + if case.find("failure") is None and case.find("error") is None: + continue + cls, name = case.get("classname", ""), case.get("name", "") + if not cls: # a collection error names the file, not a test + red.add(suite) + continue + rest = cls[len(module):] if cls == module or cls.startswith(module + ".") else cls + red.add("::".join([suite, *[p for p in rest.split(".") if p], name])) + passed = n["tests"] - n["failures"] - n["errors"] - n["skipped"] + for key, value in (("passed", passed), ("failed", n["failures"]), + ("errors", n["errors"]), ("skipped", n["skipped"])): + total[key] += value + rows.append(f"{suite}: {passed} passed, {n['failures']} failed, " + f"{n['errors']} errors, {n['skipped']} skipped") + lines = [*rows, + f"TOTAL over {len(suites)} suites: {total['passed']} passed, " + f"{total['failed']} failed, {total['errors']} errors, {total['skipped']} skipped; " + f"{total['failed'] + total['errors']} red, in {len(red)} red nodes", + *(f"RED {node}" for node in sorted(red))] + print("\n".join(lines)) + (out / "red-nodes.txt").write_text("".join(f"{node}\n" for node in sorted(red))) + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle: + handle.write("## 12.5's governed suites, composed\n\n```\n" + "\n".join(lines) + "\n```\n") + PY + exit "$red" + + # 12.5's call: one entry per suite per landing, so no `--chains`. + - name: the oracle, protected_suites.py + id: oracle + if: ${{ !cancelled() && steps.governed.outcome == 'success' }} + run: | + git log --first-parent --format=%H --grep='^Arc: neutral-product-standalone-operability$' "$ARC_BASE..HEAD" > "$COMPOSED/x-arc.txt" + if [ ! -s "$COMPOSED/x-arc.txt" ]; then + echo "::error::no landing since ARC_BASE $ARC_BASE carries the arc's trailer, so the trailer was dropped" + exit 1 + fi + echo "the arc's landings since ARC_BASE: $(wc -l < "$COMPOSED/x-arc.txt")" + python scripts/protected_suites.py --landings="$(cat "$COMPOSED/x-arc.txt")" --suites="$(cat "$COMPOSED/governed.txt")" + + # The test builds the tree in a child process. It hands the child this + # run's environment LESS `PYTHONPATH` (its `SCRUBBED_ENVIRONMENT`), so + # the composition of step 5 never reaches it, and the child needs + # `doc_health` (`cli_gate` imports `gate_console`, which imports it). So + # the composed tree's `scripts/` reaches the child through a `.pth` file + # in this run's own venv. The file is written here, after the governed + # suites have run in R1Q23 (a)'s form, and nothing runs after it. + - name: the assembled --help tree, against its recorded golden + id: help-tree + if: ${{ !cancelled() && steps.submodules.outcome == 'success' }} + run: | + purelib=$(python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') + printf '%s\n' "$OPENXFACTORY/scripts" > "$purelib/composed-openxfactory-scripts.pth" + python -m pytest -q -p no:cacheprovider -rfE \ + "tests/integration/test_assembled_surface.py::test_the_assembled_help_tree_is_the_32_entry_tree_the_manifest_records" < /dev/null diff --git a/tests/composed_host_pin.yaml b/tests/composed_host_pin.yaml new file mode 100644 index 0000000..6c5ba1b --- /dev/null +++ b/tests/composed_host_pin.yaml @@ -0,0 +1,33 @@ +# THE COMPOSED RUN'S HOST: the one openxFactory commit the composed workflow +# checks out (plan 038 task T029, U-9, in openxFactory +# `specs/038-opendox-document-tool-self-maintenance/`; decision N-7a, ruled +# with the plan on openxFactory#656 comment 6013547504). +# +# WHAT READS IT. `.github/workflows/composed.yml`, and nothing else. It checks +# openxFactory out at `commit`, initializes that checkout's `openDox`, +# `openXdox` and `openXwallet` submodules recursively, and runs 12.5's +# governed suites with that checkout's `scripts/` on `PYTHONPATH` (R1Q23 (a)'s +# form; F12.1 runs composed, CF-5). The workflow refuses the file, naming the +# rule, unless all of these hold: +# * `schema_version` is the integer 1, and `kind` is `composed-host-pin`; +# * `commit` is a full 40-character lowercase commit id; +# * no other key is present, and no key is given twice. +# The repository is openxFactory, always: the workflow names it, and this file +# names only the commit. +# +# WHEN IT MOVES. Never on its own: each advance is an openXdox-code pull +# request of its own, so the composed run always tests the host that +# openxFactory's `main` carries (plan 038 plan.md, "The composed workflow's +# openxFactory pin"). It advances after T030, T064 and T075 land in +# openxFactory, and T073 re-runs after each advance. +# +# ITS FIRST VALUE. openxFactory `main`'s head when T029 authored this file, +# read with `git ls-remote` at 2026-10-06T11:56:20Z: `92010d3e`, "Land +# add-factory-mcp-conformance on main as Speckit feature 037, with repairs +# and validator fixes (#1243)". +# +# A CREATED file: no row in openxFactory's `docs/opendox-carve-manifest.yaml` +# (RULED OQ-C). +schema_version: 1 +kind: composed-host-pin +commit: 92010d3e67f2c556687e1c3583ed3ca1867f4b8b From 541852d6096888253260d178b8e9ab8a0bba3089 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:52:41 +0000 Subject: [PATCH 07/13] T029: judge a pull request's own edits as the landing they would be (Copilot r4195440016) On `pull_request` GitHub checks out a merge commit whose first parent is the base, so the oracle's first-parent walk found only what had already landed, and a pull request's edit to a governed suite was judged only once it was on main. The oracle judges a landing by its diff against its first parent and by the blobs on either side, so the merge commit is the landing as it would be: it now joins the landings where the pull request's body carries the arc's trailer line (exact line), which the lander copies into the landing's message. The step first asserts HEAD's second parent is the pull request's head, so HEAD really is that merge commit. Also: an empty governed set is refused by name (git grep's no-match exit is read as zero suites; any other git grep failure stops the step). Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/composed.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml index 7d903e8..639471d 100644 --- a/.github/workflows/composed.yml +++ b/.github/workflows/composed.yml @@ -282,9 +282,23 @@ jobs: exit "$red" # 12.5's call: one entry per suite per landing, so no `--chains`. + # + # ON A PULL REQUEST, THE LANDING IT WOULD BE. GitHub checks out a merge + # commit whose first parent is the base, so the walk below finds only + # what has landed already, and an edit the pull request makes to a + # governed suite would be judged only once it is on `main`. The oracle + # judges a landing by its diff against its first parent and by the + # blobs on either side, so the merge commit IS the landing as it would + # be. It joins the landings where the pull request's body carries the + # arc's trailer line, which the lander copies into the landing's + # message. A body with no such line makes no arc landing, so 12.5 does + # not judge it. - name: the oracle, protected_suites.py id: oracle if: ${{ !cancelled() && steps.governed.outcome == 'success' }} + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} run: | git log --first-parent --format=%H --grep='^Arc: neutral-product-standalone-operability$' "$ARC_BASE..HEAD" > "$COMPOSED/x-arc.txt" if [ ! -s "$COMPOSED/x-arc.txt" ]; then @@ -292,6 +306,18 @@ jobs: exit 1 fi echo "the arc's landings since ARC_BASE: $(wc -l < "$COMPOSED/x-arc.txt")" + if [ "$GITHUB_EVENT_NAME" = pull_request ]; then + if [ "$(git rev-parse --verify --quiet 'HEAD^2')" != "$PR_HEAD" ]; then + echo "::error::HEAD is not this pull request's merge commit (its second parent is not the head $PR_HEAD), so the change cannot be judged as the landing it would be" + exit 1 + fi + if python -c 'import os, sys; sys.exit("Arc: neutral-product-standalone-operability" not in os.environ.get("PR_BODY", "").replace("\r", "").split("\n"))'; then + git rev-parse HEAD >> "$COMPOSED/x-arc.txt" + echo "the pull request's body carries the arc's trailer, so its merge commit $(git rev-parse HEAD) is judged as the landing it would be" + else + echo "the pull request's body carries no arc trailer line, so its change is not an arc landing" + fi + fi python scripts/protected_suites.py --landings="$(cat "$COMPOSED/x-arc.txt")" --suites="$(cat "$COMPOSED/governed.txt")" # The test builds the tree in a child process. It hands the child this From 82d95924e96676ef47f689852df171617157a9be Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:03:47 +0000 Subject: [PATCH 08/13] T029: an edited pull request body is judged again (Copilot r4195546545) The oracle reads the pull request's body for the arc's trailer line, and a bare `pull_request` trigger runs on opened, synchronize and reopened only, so a trailer added after a green run left the head's check green without the change ever being judged as an arc landing. The workflow now also runs on `edited`, beside the default three. Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/composed.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml index 639471d..5026043 100644 --- a/.github/workflows/composed.yml +++ b/.github/workflows/composed.yml @@ -44,6 +44,9 @@ name: composed on: pull_request: + # `edited` beside the default three: the oracle reads the pull request's + # body for the arc's trailer line, so an edited body is judged again. + types: [opened, synchronize, reopened, edited] push: branches: [main] workflow_dispatch: From c27c91d654bb128bc5e2d3d570b981e115c3ee71 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:10:01 +0000 Subject: [PATCH 09/13] T073: the composed declarations, ARC-Q2 (a): each declared entry runs composed, and the rail files get the host's rail (plan 038) Batch Q's amendment of #1144's F9.1 (openxFactory#1248; ARC-Q2 (a), openxFactory#656 comment 6003918488) makes every file of the declared exclusion a DECLARED INTEGRATION TEST that the composed workflow runs. - tests/declared_exclusion.yaml: the header declares every entry a declared integration test, run by .github/workflows/composed.yml at the ONE openxFactory commit tests/composed_host_pin.yaml names. The count, every entry, and each reason's id, reason and ruled are unchanged. Each open_until gains one clause naming that composed home, so every lone run prints it. No commit id is written outside the pin file, so a pin advance needs no edit here. - tests/conftest.py: one block after T021's. Where the run is composed, each test of a file the declaration names under status-exemption-rail (R1Q24 (a), comment 5850003126) runs with the rail openxFactory's host registers. That rail is read off the register_status_exemption row of opendox_host.seams(), registered for the test and taken back after it, as U-1 registers the governed host. A lone checkout registers nothing, so the declaration's lone evidence is unchanged. No T020, T021 or T026 line is touched. - .github/workflows/composed.yml: step 5a runs every entry outside 12.5's governed set, composed in step 5's form and each alone. It refuses an entry that has a report from neither step. Refs opensoft/openxFactory#656 (CLAIMED 6016678145; QUESTION 6016925160). Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/composed.yml | 99 +++++++++++++++++++++++++++++++++- tests/conftest.py | 99 ++++++++++++++++++++++++++++++++++ tests/declared_exclusion.yaml | 29 ++++++++-- 3 files changed, 223 insertions(+), 4 deletions(-) diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml index 639471d..4b04bb9 100644 --- a/.github/workflows/composed.yml +++ b/.github/workflows/composed.yml @@ -28,6 +28,14 @@ name: composed # 5. 12.5's governed suites, computed the way its falsifier computes them # (16 is the floor). Each suite runs alone, with # `PYTHONPATH="$OPENXFACTORY/scripts"` (R1Q23 (a)'s form). +# 5a. The declaration's other entries (plan 038 T073; ARC-Q2 (a), +# openxFactory#656 comment 6003918488). Every file of +# `tests/declared_exclusion.yaml` is a declared integration test that +# this workflow runs. The governed suites among them ran in step 5, and +# this step runs every other entry in the same form, each alone. It +# refuses an entry with no report from either step, so none is dropped +# from both suites. The rail the R1Q24 rail files need is registered by +# `tests/conftest.py` where the run is composed. # 6. The oracle, `scripts/protected_suites.py`, in 12.5's call: no landing # of the arc touched a governed suite outside an allow-list entry that # holds. @@ -35,7 +43,7 @@ name: composed # the hosts' help goldens are unchanged). `validate.yml` leaves that test # out (`LEFT_OUT`), because building the tree needs `doc_health`. Composed, # it can run. -# Steps 6 and 7 run even when step 5 is red, so one run reports every red. +# Steps 5a, 6 and 7 run even when step 5 is red, so one run reports every red. # # WHAT IS NOT HERE. Making `composed` a REQUIRED check is the holder's act, # after this workflow's first green run on `main` (N-7b (a), ruled @@ -281,6 +289,95 @@ jobs: PY exit "$red" + # Step 5a (T073): the declaration's other entries, each alone, in step + # 5's form. The list is the declaration's own `entries`, read here, so an + # entry runs from the landing that lists it; the governed suites among + # them ran in step 5. Every entry must then have a report from one of the + # two steps, or it is refused as dropped from both suites. + - name: the declared integration tests, composed, each alone + id: declared + if: ${{ !cancelled() && steps.governed.outcome == 'success' }} + run: | + export PYTHONPATH="$OPENXFACTORY/scripts" + echo "the declaration's composed home: opensoft/openxFactory@$COMPOSED_HOST_COMMIT, the commit tests/composed_host_pin.yaml names" + python - "$COMPOSED" <<'PY' + import pathlib, sys + import yaml + + out = pathlib.Path(sys.argv[1]) + declaration = yaml.safe_load(pathlib.Path("tests/declared_exclusion.yaml").read_text(encoding="utf-8")) + entries = [entry["path"] for entry in declaration["entries"]] + governed = {line.strip() for line in (out / "governed.txt").read_text().splitlines() if line.strip()} + here = [path for path in entries if path not in governed] + (out / "declared.txt").write_text("".join(f"{path}\n" for path in here)) + print(f"declared integration tests: {len(entries)} (count {declaration['count']}); " + f"{len(entries) - len(here)} ran in step 5, {len(here)} run here") + PY + mkdir -p "$COMPOSED/declared-reports" + red=0 + while read -r test_file <&3; do + echo "::group::$test_file" + rc=0 + python -m pytest -q -p no:cacheprovider -rfE \ + --junitxml="$COMPOSED/declared-reports/$(basename "$test_file" .py).xml" "$test_file" < /dev/null || rc=$? + echo "::endgroup::" + echo "$test_file: pytest exit $rc" + if [ "$rc" -ne 0 ]; then red=1; fi + done 3< "$COMPOSED/declared.txt" + python - "$COMPOSED" <<'PY' || red=1 + import os, pathlib, sys + import xml.etree.ElementTree as ET + import yaml + + out = pathlib.Path(sys.argv[1]) + entries = [entry["path"] for entry in yaml.safe_load( + pathlib.Path("tests/declared_exclusion.yaml").read_text(encoding="utf-8"))["entries"]] + here = [line.strip() for line in (out / "declared.txt").read_text().splitlines() if line.strip()] + rows, red, total = [], set(), dict.fromkeys(("passed", "failed", "errors", "skipped"), 0) + for path in entries: + stem = pathlib.PurePosixPath(path).stem + if not any((out / where / f"{stem}.xml").exists() for where in ("reports", "declared-reports")): + rows.append(f"{path}: NOT RUN by step 5 or step 5a, so dropped from both suites") + red.add(path) + for path in here: + report = out / "declared-reports" / f"{pathlib.PurePosixPath(path).stem}.xml" + if not report.exists(): + continue + root = ET.parse(report).getroot() + n = dict.fromkeys(("tests", "failures", "errors", "skipped"), 0) + module = path[:-3].replace("/", ".") + for ts in (root.iter("testsuite") if root.tag == "testsuites" else [root]): + for key in n: + n[key] += int(ts.get(key, 0)) + for case in ts.iter("testcase"): + if case.find("failure") is None and case.find("error") is None: + continue + cls, name = case.get("classname", ""), case.get("name", "") + if not cls: # a collection error names the file, not a test + red.add(path) + continue + rest = cls[len(module):] if cls == module or cls.startswith(module + ".") else cls + red.add("::".join([path, *[p for p in rest.split(".") if p], name])) + passed = n["tests"] - n["failures"] - n["errors"] - n["skipped"] + for key, value in (("passed", passed), ("failed", n["failures"]), + ("errors", n["errors"]), ("skipped", n["skipped"])): + total[key] += value + rows.append(f"{path}: {passed} passed, {n['failures']} failed, " + f"{n['errors']} errors, {n['skipped']} skipped") + lines = [*rows, + f"TOTAL over {len(here)} declared integration tests: {total['passed']} passed, " + f"{total['failed']} failed, {total['errors']} errors, {total['skipped']} skipped; " + f"{total['failed'] + total['errors']} red, in {len(red)} red nodes", + *(f"RED {node}" for node in sorted(red))] + print("\n".join(lines)) + (out / "declared-red-nodes.txt").write_text("".join(f"{node}\n" for node in sorted(red))) + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle: + handle.write("## The declared integration tests, composed\n\n```\n" + "\n".join(lines) + "\n```\n") + sys.exit(1 if red else 0) + PY + exit "$red" + # 12.5's call: one entry per suite per landing, so no `--chains`. # # ON A PULL REQUEST, THE LANDING IT WOULD BE. GitHub checks out a merge diff --git a/tests/conftest.py b/tests/conftest.py index 5c0ad6a..2e3346d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -791,3 +791,102 @@ def _the_governed_host_for_the_governed_suites(request): _gate_console.register_contract_schema_source( Path(_OPENXFACTORY_HOST.__file__).resolve().parents[1] / "contracts" / "schemas") + + +# --------------------------------------------------------------------------- +# THE HOST'S STATUS-EXEMPTION RAIL FOR THE DECLARED RAIL SUITES (plan 038 +# T073; ARC-Q2 (a), openxFactory#656 comment 6003918488; R1Q24 (a), comment +# 5850003126). +# +# R1Q24 (a) declared the files that need openxFactory's status-exemption rail, +# which only openxFactory's host registers at openDox's status-exemption seam +# (`opendox.doxbench_packet.register_status_exemption`). In a lone checkout +# nothing registers it, so their cases refuse with +# `StatusExemptionNotRegistered`, which is the declaration's evidence for their +# reason. ARC-Q2 (a) makes them declared integration tests, which the composed +# workflow runs. So, as U-1 registers the governed host above, for the length +# of each test of a file `tests/declared_exclusion.yaml` names under the reason +# `status-exemption-rail`, this registers THE HOST'S RAIL where the run is +# composed, and puts the seam back afterwards. Where the run is not composed it +# registers nothing, and the lone checkout's evidence is unchanged. +# +# THE RAIL IS THE ONE openxFactory'S HOST REGISTERS: the +# `register_status_exemption` row of `opendox_host.seams()`, the table the +# host's process start registers from. It is read off that table rather than +# named here, so a later host pin is followed. +# +# THE LIST IS THE DECLARATION'S, as the root `conftest.py` has already held it +# to its rules: a file gets the rail, or stops getting it, only with its entry. +# +# THE SEAM IS PUT BACK. A rail held before the test is dropped for it. After +# it, a host's rail is registered again, and openDox's own default, an entry +# point's, is not, since the next entry point registers it again, as +# `a_hosts_plane` treats the default profile. +# --------------------------------------------------------------------------- + +STATUS_EXEMPTION_RAIL_REASON = "status-exemption-rail" + + +@functools.cache +def status_exemption_rail_suites() -> frozenset[str]: + """The test modules the declaration names under the rail's reason.""" + import yaml + + declaration = yaml.safe_load( + (HERE / "declared_exclusion.yaml").read_text(encoding="utf-8")) + return frozenset(Path(entry["path"]).name + for entry in declaration["entries"] + if STATUS_EXEMPTION_RAIL_REASON in entry["reasons"]) + + +@functools.cache +def openxfactory_status_exemption_rail(): + """openxFactory's status-exemption rail where the run is composed, and + None where it is not: what its host registers at openDox's seam, read off + the host's own seam table.""" + host_module = openxfactory_host() + if host_module is None: + return None + saved = list(sys.path) + try: + rails = [what for _, call, what in host_module.seams() + if call == "register_status_exemption"] + finally: + sys.path[:] = saved + _refuse_an_openxdox_from_elsewhere() + if len(rails) != 1: + raise RuntimeError( + f"openxFactory's host seam table names {len(rails)} " + "status-exemption rails, so the rail it registers is not one " + "this harness can register for it") + return rails[0] + + +@contextlib.contextmanager +def a_hosts_rail(rail): + """Register `rail` at openDox's status-exemption seam for the length of + the block, then put the seam back as it was, less openDox's default.""" + from opendox import doxbench_packet as _packet + + previous = getattr(_packet, "_status_exemption_rail", None) + a_hosts = previous is not None and not getattr( + _packet, "_status_exemption_is_default", False) + _packet.unregister_status_exemption() + try: + yield _packet.register_status_exemption(rail) + finally: + _packet.unregister_status_exemption() + if a_hosts: + _packet.register_status_exemption(previous) + + +@_pytest.fixture(autouse=True) +def _the_hosts_rail_for_the_declared_rail_suites(request): + rail = (openxfactory_status_exemption_rail() + if request.node.path.name in status_exemption_rail_suites() + else None) + if rail is None: + yield + return + with a_hosts_rail(rail): + yield diff --git a/tests/declared_exclusion.yaml b/tests/declared_exclusion.yaml index 6576e56..63c07a0 100644 --- a/tests/declared_exclusion.yaml +++ b/tests/declared_exclusion.yaml @@ -12,6 +12,23 @@ # emptied list is `count: 0` with no reasons and no entries. The root # conftest loads it, excludes nothing, and prints that nothing is left out. # +# EVERY ENTRY IS ALSO A DECLARED INTEGRATION TEST (plan 038 task T073, in +# openxFactory `specs/038-opendox-document-tool-self-maintenance/`; ARC-Q2 +# (a), openxFactory#656 comment 6003918488; #1144's F9.1 as batch Q amends +# it). What a lone checkout cannot run runs in this repository's own CI, +# composed with openxFactory: `.github/workflows/composed.yml` runs every +# file below at the ONE openxFactory commit `tests/composed_host_pin.yaml` +# names, in R1Q23 (a)'s form. 12.5's governed suites among them run in its +# governed step, and every other entry runs in its declared step, each file +# alone. So no file here is dropped from both suites. The count is `count`, +# and each entry keeps its reason. This file names that commit by naming the +# pin file, and writes no commit id of its own, so the pin advances (after +# T030, T064 and T075) with no edit here. Read with the paragraph above: in a +# lone checkout each entry stays an open extraction until its reason is +# cleared, and composed it is a declared integration test, by which +# declaration requirement 9 closes for openXdox-code (batch Q). Each reason's +# `open_until` names that composed home, so every lone run prints it. +# # WHO READS IT. # * The repository-root `conftest.py`. It derives `collect_ignore` from # `entries`, so a run that loads it collects the whole suite less these @@ -27,6 +44,12 @@ # list cannot hide a failure of any other kind. A file that stops failing # for its reason turns that test red, and it leaves the list in the pull # request that clears the reason. +# * `.github/workflows/composed.yml`. Its declared step runs every entry +# that is not one of 12.5's governed suites, composed and each alone, and +# refuses an entry that neither of its two steps runs. +# * `tests/conftest.py`. Where the run is composed, it registers the rail +# openxFactory's host registers for each entry naming +# `status-exemption-rail`, and only for those. # # THE RULES. # * `schema_version` is the integer 1, and `count` is an integer equal to the @@ -65,15 +88,15 @@ reasons: - id: doc_health reason: "reaches openxFactory's doc_health, which openxFactory packages nowhere and no lone checkout supplies" ruled: "R1Q6 (d), openxFactory#656 comment 5817152735" - open_until: "the doc_health direction arc (plan 034 T008)" + open_until: "the doc_health direction arc (plan 034 T008); run composed, as a declared integration test, by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" - id: status-exemption-rail reason: "needs openxFactory's status-exemption rail, which only openxFactory registers at openDox's status-exemption seam" ruled: "R1Q24 (a), openxFactory#656 comment 5850003126" - open_until: "the doc_health direction arc (plan 034 T008)" + open_until: "the doc_health direction arc (plan 034 T008); run composed, as a declared integration test, by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" - id: openxfactory-contracts reason: "reads openxFactory's contracts (the contract family's validator, schemas and examples, or its contracts/manifest.yaml) where openxFactory's tree keeps them, which is outside this checkout" ruled: "R1Q24 (a), openxFactory#656 comment 5850003126" - open_until: "the doc_health direction arc (plan 034 T008)" + open_until: "the doc_health direction arc (plan 034 T008); run composed, as a declared integration test, by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" entries: - {path: tests/test_authoring_agent.py, reasons: [doc_health]} From b4993d10a69dc4f409861c52ca854c7d3ba2d472 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:53:47 +0000 Subject: [PATCH 10/13] T029: 12.5's oracle call takes --chains, as Brett ruled (#656 6016648451) Brett Heap ruled "Amend F12.1 with --chains (Recommended)" (openxFactory#656 comment 6016648451). Batch Q (T005, openxFactory#1248) records it at 12.5's falsifier, and its note says T026 and T029 carry it out. The oracle step now passes `--chains`, so several entries chained by blob may together admit one landing's edits to one governed suite, each still one edit inside its own named test or span, under batch K's rule unchanged. T026 lands as one pull request on exactly that. Proved on simulated pull-request merge commits: two edits to one governed suite in one landing, entered as two chained entries, are admitted (`ok: 4 protected edit(s), each entered and holding`); with `--chains` dropped the same landing is refused, so that mutant is killed. Today's nine landings still read `ok: 3`. Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/composed.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml index 5026043..5baae30 100644 --- a/.github/workflows/composed.yml +++ b/.github/workflows/composed.yml @@ -28,9 +28,9 @@ name: composed # 5. 12.5's governed suites, computed the way its falsifier computes them # (16 is the floor). Each suite runs alone, with # `PYTHONPATH="$OPENXFACTORY/scripts"` (R1Q23 (a)'s form). -# 6. The oracle, `scripts/protected_suites.py`, in 12.5's call: no landing -# of the arc touched a governed suite outside an allow-list entry that -# holds. +# 6. The oracle, `scripts/protected_suites.py`, in 12.5's call as batch Q +# amends it (`--chains`): no landing of the arc touched a governed suite +# outside allow-list entries that hold. # 7. The assembled `--help` tree, against the golden it records (R2Q3 (a): # the hosts' help goldens are unchanged). `validate.yml` leaves that test # out (`LEFT_OUT`), because building the tree needs `doc_health`. Composed, @@ -284,7 +284,12 @@ jobs: PY exit "$red" - # 12.5's call: one entry per suite per landing, so no `--chains`. + # 12.5's call, with `--chains`: Brett Heap ruled "Amend F12.1 with + # --chains (Recommended)" (openxFactory#656 comment 6016648451), which + # batch Q (T005) records at 12.5's falsifier. So several entries, chained + # by blob, may together admit one landing's edits to one governed suite, + # each still one edit inside its own named test or span, under batch K's + # rule unchanged. # # ON A PULL REQUEST, THE LANDING IT WOULD BE. GitHub checks out a merge # commit whose first parent is the base, so the walk below finds only @@ -321,7 +326,7 @@ jobs: echo "the pull request's body carries no arc trailer line, so its change is not an arc landing" fi fi - python scripts/protected_suites.py --landings="$(cat "$COMPOSED/x-arc.txt")" --suites="$(cat "$COMPOSED/governed.txt")" + python scripts/protected_suites.py --chains --landings="$(cat "$COMPOSED/x-arc.txt")" --suites="$(cat "$COMPOSED/governed.txt")" # The test builds the tree in a child process. It hands the child this # run's environment LESS `PYTHONPATH` (its `SCRUBBED_ENVIRONMENT`), so From 2565dac4eb104dbc81371b262d733dca9bd6422a Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:54:07 +0000 Subject: [PATCH 11/13] T073: the rail's and the contracts' open_until, as the holder ruled (openxFactory#656 6016982816) Under ARC-Q2 (a) the doc_health direction arc does not clear the status-exemption-rail and openxfactory-contracts reasons, so their open_until drops "the doc_health direction arc (plan 034 T008)" and reads, as ruled: "never in a lone checkout: a declared integration test, run composed by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)". The doc_health reason keeps its words and the clause. id, reason and ruled stay byte-identical, and no commit id is written outside the pin file. Refs opensoft/openxFactory#656 (QUESTION 6016925160, RULED 6016982816). Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/declared_exclusion.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/declared_exclusion.yaml b/tests/declared_exclusion.yaml index 63c07a0..49bca31 100644 --- a/tests/declared_exclusion.yaml +++ b/tests/declared_exclusion.yaml @@ -92,11 +92,11 @@ reasons: - id: status-exemption-rail reason: "needs openxFactory's status-exemption rail, which only openxFactory registers at openDox's status-exemption seam" ruled: "R1Q24 (a), openxFactory#656 comment 5850003126" - open_until: "the doc_health direction arc (plan 034 T008); run composed, as a declared integration test, by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" + open_until: "never in a lone checkout: a declared integration test, run composed by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" - id: openxfactory-contracts reason: "reads openxFactory's contracts (the contract family's validator, schemas and examples, or its contracts/manifest.yaml) where openxFactory's tree keeps them, which is outside this checkout" ruled: "R1Q24 (a), openxFactory#656 comment 5850003126" - open_until: "the doc_health direction arc (plan 034 T008); run composed, as a declared integration test, by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" + open_until: "never in a lone checkout: a declared integration test, run composed by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" entries: - {path: tests/test_authoring_agent.py, reasons: [doc_health]} From 26fe63e7624bc61cf055be40ea86db77fed9241b Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:26:43 +0000 Subject: [PATCH 12/13] T073: commit the rail block's restore cases, tests/test_declared_rail_registration.py (Copilot r4197793290; holder ruling (a)) Copilot found that nothing committed exercised conftest.a_hosts_rail's put-back: the mutants that drop the take-back, skip the restore of a host's earlier rail, or skip the drop before the test (C5 to C7) died only under uncommitted probes. Five cases with stand-in rails, run in the lone required check: - an earlier host rail is dropped for the block and put back after; - no rail is left behind where none was registered; - openDox's own default is not put back; - the seam is put back when the block fails; - the rail suites are the declaration's rail entries. They kill C2, C5, C6 and C7. C1 and C3 die in the composed run and C4 in tests/test_declared_exclusion.py, so all seven are killed by committed tests. Each case leaves the seam as it found it. The holder added this file to T073's Files (openxFactory#656, question 6020683941, option (a)). Refs opensoft/openxFactory#656 (QUESTION 6020683941). Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_declared_rail_registration.py | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/test_declared_rail_registration.py diff --git a/tests/test_declared_rail_registration.py b/tests/test_declared_rail_registration.py new file mode 100644 index 0000000..b6f7907 --- /dev/null +++ b/tests/test_declared_rail_registration.py @@ -0,0 +1,100 @@ +"""The rail registration `tests/conftest.py` makes for the declared rail +suites, held to its word (plan 038 task T073; ARC-Q2 (a), openxFactory#656 +comment 6003918488; Copilot r4197793290 on openXdox-code#45). + +Where the run is composed, each test of a file `tests/declared_exclusion.yaml` +names under `status-exemption-rail` runs with the rail openxFactory's host +registers, through `conftest.a_hosts_rail`. Those runs prove the rail is +registered. These cases prove what a composed run cannot see: that the seam is +put back afterwards, so no test after a rail suite meets a rail it did not +register, and no host's rail is lost to one. They use stand-in rails, so they +run in a lone checkout too, inside the required check. + +Each case leaves openDox's seam exactly as it found it: `_the_seam_as_found` +records the seam's three globals through `monkeypatch`, which writes them back +at teardown, whatever the case did. +""" + +from __future__ import annotations + +import pytest +import yaml + +from conftest import HERE, a_hosts_rail, status_exemption_rail_suites +from opendox import doxbench_packet + + +class _Rail: + """A stand-in rail: the two names a rail must carry, each its own.""" + + def __init__(self, name: str): + self.name = name + + def lifecycle_status(text): + return None + + def is_compression_exempt(text): + return False + + self.lifecycle_status = lifecycle_status + self.is_compression_exempt = is_compression_exempt + + +def _answering() -> object: + """The rail openDox's seam answers with now, read through the name it + hands over (`doxbench_packet.is_compression_exempt`), or None when no + rail is registered.""" + if not doxbench_packet.status_exemption_registered(): + return None + return doxbench_packet.is_compression_exempt + + +@pytest.fixture(autouse=True) +def _the_seam_as_found(monkeypatch): + for name in ("_status_exemption_rail", "_status_exemption_is_default", + "_status_exemption_default_read"): + monkeypatch.setattr(doxbench_packet, name, getattr(doxbench_packet, name)) + doxbench_packet.unregister_status_exemption() + + +def test_a_hosts_earlier_rail_is_dropped_for_the_block_and_put_back_after(): + earlier, rail = _Rail("earlier"), _Rail("the host's") + doxbench_packet.register_status_exemption(earlier) + with a_hosts_rail(rail): + assert _answering() is rail.is_compression_exempt + assert _answering() is earlier.is_compression_exempt + + +def test_no_rail_is_left_behind_where_none_was_registered(): + rail = _Rail("the host's") + with a_hosts_rail(rail): + assert _answering() is rail.is_compression_exempt + assert _answering() is None + + +def test_opendoxs_own_default_is_not_put_back(): + """As `a_hosts_plane` treats the default profile: the next entry point + registers the default again, as it does in any process.""" + default, rail = _Rail("openDox's default"), _Rail("the host's") + doxbench_packet.register_default_status_exemption(default) + with a_hosts_rail(rail): + assert _answering() is rail.is_compression_exempt + assert _answering() is None + + +def test_the_seam_is_put_back_when_the_block_fails(): + earlier, rail = _Rail("earlier"), _Rail("the host's") + doxbench_packet.register_status_exemption(earlier) + with pytest.raises(RuntimeError, match="the case failed"): + with a_hosts_rail(rail): + raise RuntimeError("the case failed") + assert _answering() is earlier.is_compression_exempt + + +def test_the_rail_suites_are_the_declarations_rail_entries(): + declaration = yaml.safe_load( + (HERE / "declared_exclusion.yaml").read_text(encoding="utf-8")) + named = {entry["path"].rsplit("/", 1)[-1] for entry in declaration["entries"] + if "status-exemption-rail" in entry["reasons"]} + assert named, "the declaration names no rail file" + assert status_exemption_rail_suites() == frozenset(named) From cebf3452a99e290a3a3f6c180ab86f3c56e7b08d Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:37:18 +0000 Subject: [PATCH 13/13] T073: the contracts farm leaves out only openDox-spec's opendox-snapshot-* examples (Copilot r4201411760 on #46; holder r4201421102) T095's farm (build_contracts_farm) skipped every example named opendox-snapshot-* whichever owner kept it, while the farm's own contract leaves out only openDox's. The condition is now scoped to the opendox_spec owner (CONTRACT_EXAMPLE_LEFT_OUT_OWNER), so a same-prefix example kept by openxFactory or openXdox-spec is linked like any other. As the holder measured, none exists today, so the composed farm is unchanged. tests/test_host_plane.py gains one farm case beside T095's: a same-prefix example from openxFactory and one from openXdox-spec are linked, and openDox-spec's two are not. It is red on the owner-wide condition and green on the scoped one. It also kills the mutants that scope the exclusion to the wrong owner or drop it, the latter beside T095's existing farm case. The fix rides T073 as the holder ruled (r4201421102, under openxFactory#656 5988818366). T073's files gain tests/test_host_plane.py for this one case. Lane: openXfactory-3 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/conftest.py | 14 +++++++++++--- tests/test_host_plane.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 231fb87..2fca5db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1027,8 +1027,11 @@ def _the_hosts_seams_for_the_seam_suites(request): # * `/examples/ideation-dashboard/`: a link to each YAML example the # three owners keep under `examples/ideation-dashboard/`, at the same # relative path, less openDox's own `opendox-snapshot-*` examples, whose -# kind the family's validator does not serve. Two owners naming one path -# is refused, never resolved. +# kind the family's validator does not serve. Only openDox-spec's are left +# out: an example of that prefix from openxFactory or openXdox-spec is +# linked like any other (T073; Copilot r4201411760 on openXdox-code#46, +# the holder's r4201421102). Two owners naming one path is refused, never +# resolved. # It is OUTSIDE the checkout, in a temporary directory removed at exit: the # validator refuses a `contracts/` that links out of its tree (`:157-163`). A # temporary root inside the checkout (a TMPDIR, or `under`, beneath it) is @@ -1036,6 +1039,9 @@ def _the_hosts_seams_for_the_seam_suites(request): # The reach is read with `sys.path` saved and put back, and it must be the # composed tree's own. CONTRACT_EXAMPLE_LEFT_OUT = "opendox-snapshot-" +#: The one owner whose examples of that prefix are left out: openDox-spec, +#: as `carved_reach.MOUNTS` names it. +CONTRACT_EXAMPLE_LEFT_OUT_OWNER = "opendox_spec" def build_contracts_farm(reach, farm: Path) -> Path: @@ -1054,11 +1060,13 @@ def build_contracts_farm(reach, farm: Path) -> Path: examples = farm / "examples" / "ideation-dashboard" examples.mkdir(parents=True) linked: dict[Path, Path] = {} + left_out_from = reach.MOUNTS[CONTRACT_EXAMPLE_LEFT_OUT_OWNER] for owner in (reach.REPO_ROOT, reach.MOUNTS["opendox_spec"], reach.MOUNTS["openxdox_spec"]): base = owner / "examples" / "ideation-dashboard" for path in sorted(base.rglob("*.yaml")): - if path.name.startswith(CONTRACT_EXAMPLE_LEFT_OUT): + if (owner == left_out_from + and path.name.startswith(CONTRACT_EXAMPLE_LEFT_OUT)): continue relative = path.relative_to(base) if relative in linked: diff --git a/tests/test_host_plane.py b/tests/test_host_plane.py index 19f4537..cefdf26 100644 --- a/tests/test_host_plane.py +++ b/tests/test_host_plane.py @@ -663,6 +663,28 @@ def test_the_farm_links_the_familys_schemas_and_examples(tmp_path) -> None: } +def test_the_farm_leaves_out_only_opendox_specs_snapshot_examples(tmp_path) -> None: + """The `opendox-snapshot-` prefix is left out for openDox-spec's own + examples only (T073; Copilot r4201411760 on #46, the holder's + r4201421102). An example of the same prefix from openxFactory or from + openXdox-spec is linked like any other.""" + root = tmp_path / "openxFactory" + _a_tree(root, { + "examples/ideation-dashboard/opendox-snapshot-a.example.yaml": "", + "openXdox/spec/examples/ideation-dashboard/negative/opendox-snapshot-b.negative.yaml": "", + "openDox/spec/examples/ideation-dashboard/opendox-snapshot-c.example.yaml": "", + "openDox/spec/examples/ideation-dashboard/negative/opendox-snapshot-d.negative.yaml": "", + }) + farm = tmp_path / "farm" + tests_conftest.build_contracts_farm(_a_reach(root, {}), farm) + assert _links(farm / "examples" / "ideation-dashboard") == { + "opendox-snapshot-a.example.yaml": + root / "examples/ideation-dashboard/opendox-snapshot-a.example.yaml", + "negative/opendox-snapshot-b.negative.yaml": + root / "openXdox/spec/examples/ideation-dashboard/negative/opendox-snapshot-b.negative.yaml", + } + + def test_the_farm_refuses_an_example_two_owners_keep(tmp_path) -> None: root = tmp_path / "openxFactory" _a_tree(root, {