From 343c1ba823e775fe58dc28cb7b62b1b2660e5d00 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:46:14 +0000 Subject: [PATCH 01/25] Pin openDox-code e3ef506a, the head of openDox-code#59 (T055), for plan 034 T059 T059 moves the opendox pin to the phase-2 openDox-code commit (5.4a, 9.5 step 3). That commit is T062's, and it lands after every phase-2 openDox-code landing, so this draft builds against openDox-code#59's head, the last of the seams T059 registers at, and re-points to T062's commit before it lands. The comments that named the old pin as the one this leg declares are brought to the new one, each measured there: ViewBinding at e3ef506a still has styles and exports (dataclasses.fields in a venv at that pin), and the tuple and helpers tests/integration/test_assembled_bundle.py copies are unchanged between 2d116415 and e3ef506a. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- pyproject.toml | 2 +- src/openxdox/view_extensions.py | 15 ++++++++------- tests/integration/test_assembled_bundle.py | 2 +- tests/test_gate_loop_probes.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4343bbf..26cbb32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ requires-python = ">=3.12" # PR) — and not an arbitrary choice: `Draft202012Validator`, the name # `gate_console.py:563` imports, was added in jsonschema 4.18.0. dependencies = [ - "opendox @ git+https://github.com/opensoft/openDox-code@2d116415159b721b55613fe20a159afc46202d1f", + "opendox @ git+https://github.com/opensoft/openDox-code@e3ef506a8036c1360d88ff75cab50f8b8e5d5fca", "PyYAML>=6.0", "jsonschema>=4.18", ] diff --git a/src/openxdox/view_extensions.py b/src/openxdox/view_extensions.py index ef3b9b8..e3b2c9b 100644 --- a/src/openxdox/view_extensions.py +++ b/src/openxdox/view_extensions.py @@ -23,23 +23,24 @@ `a99eba03` and this paragraph said the pinned commit "is older than the view registry itself — `opendox.view_extension` does not exist there, and the `exports` field RULED Q2 adds is newer still". THAT IS NO LONGER TRUE, and the -old wording is quoted here as provenance rather than deleted: the pin now names -openDox-code#55 (`2d116415`, plan 034 T037's landing, since plan 034 T040), -where `view_extension` is importable and `ViewBinding` takes `exports` — +old wording is quoted here as provenance rather than deleted: the pin named +openDox-code#55 (`2d116415`, plan 034 T037's landing) from plan 034 T040, and +names openDox-code `e3ef506a` (the head of openDox-code#59, T055) since plan 034 +T059; at both `view_extension` is importable and `ViewBinding` takes `exports` — measured, and the three materialization assertions in `tests/test_gate_loop_views.py` run and pass against it instead of skipping. It first became true at `0b4e8bbf` (openDox-code#23, § 3.4 slice S8 leg B), which is where that wording was corrected; the pin then crossed `0e65b5f8` (#24) to `5c137a90` (openDox-code#27, § 3.4 RULED Q7), whose `ViewBinding` first carried a `styles` field — absent at `0b4e8bbf`, present at `5c137a90` -and still at `2d116415`, measured by `dataclasses.fields()` in a venv at each -pin. THE `5c137a90` BUMP ITSELF READ NOTHING, and the review of `ea6991b` was +and still at `2d116415` and at `e3ef506a`, measured by `dataclasses.fields()` +in a venv at each pin. THE `5c137a90` BUMP ITSELF READ NOTHING, and the review of `ea6991b` was right to check that: it materialized `VIEW_BINDING_SPECS` unchanged and asked nothing about the installed `ViewBinding`. THE READING IS THIS ACT'S, and this act is the pull request that bump named as waiting on it: `specs_for()` below reads `dataclasses.fields(binding_cls)` and drops `styles` where the installed dataclass has no such field. MEASURED IN A VENV AT THAT PIN, and again at -`2d116415`: it has one, so nothing is dropped, every binding that owns +`2d116415` and at `e3ef506a`: it has one, so nothing is dropped, every binding that owns selectors declares its sheet, and the four contributed stylesheets are LIVE rather than inert — which is the one thing they waited on that bump for. @@ -518,7 +519,7 @@ def specs_for(binding_cls: Any) -> tuple[dict[str, Any], ...]: So the field is DROPPED where the installed class does not take it and the column mounts unstyled. AT THE PIN THIS LEG DECLARES TODAY THE DETECTION IS THE PLAIN PATH, not a fallback: `dataclasses.fields()` finds `styles` on - `2d116415`'s `ViewBinding`, as on `5c137a90`'s where the field first + `e3ef506a`'s `ViewBinding`, as on `2d116415`'s and on `5c137a90`'s where the field first reached the pin, every spec crosses whole, and the four contributed sheets are LIVE — same code, same behaviour, one branch not taken. This paragraph read "the pin bump that follows openDox-code's Q7 leg turns the sheets on with no edit here"; that bump landed, and that is what diff --git a/tests/integration/test_assembled_bundle.py b/tests/integration/test_assembled_bundle.py index 2e8bb19..158b181 100644 --- a/tests/integration/test_assembled_bundle.py +++ b/tests/integration/test_assembled_bundle.py @@ -31,7 +31,7 @@ `_ST_DECLARATION` and `_declared_st_tokens`) and the `GATE_EXCLUSIVE` tuple are openDox-code's text at `55194335`, the last commit that carried all five, byte for byte. The tuple and the three helpers openDox-code kept are unchanged at -`2d116415`. Their comments are kept too, so "Copilot review, round N" in them +`2d116415` and at `e3ef506a`. Their comments are kept too, so "Copilot review, round N" in them is a round on openDox-code#27, where that file was written. Four things changed, each because this is the composition and not a lone leg: 1. A missing assembly FAILS here, where it skipped there. The composition is diff --git a/tests/test_gate_loop_probes.py b/tests/test_gate_loop_probes.py index e1252ed..7355bc0 100644 --- a/tests/test_gate_loop_probes.py +++ b/tests/test_gate_loop_probes.py @@ -194,7 +194,7 @@ def bundle(tmp_path) -> Path: # Its four-row table is there, not restated here. Round 1 of the review on # #21 found this file claiming the install "came from somewhere older than # the declared pin" on a check that only tested for a marker. UNDER THAT - # CHECK, had the DECLARED leg (`0b4e8bbf` then, `2d116415` now) itself ever + # CHECK, had the DECLARED leg (`0b4e8bbf` then, `e3ef506a` now) itself ever # stopped shipping `web/**`, all thirteen # probes below would have skipped and this required check would have stayed # green over the regression. UNDER THE TABLE THEY OBEY NOW THEY FAIL: the From bc03c00753203cb5084f6ce29e0c6769aa1f85b2 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:46:43 +0000 Subject: [PATCH 02/25] Seal the four gaps in the governed registry and writer before they are registered (plan 034 T059) openDox's own defaults sealed each of these in openDox-code#59's review rounds, and #59's body names them as openXdox-code's to seal before T059 registers this leg's mechanisms at the same seams: 1. snapshot_registry.resolve_within applied the hidden-name rule only to the path as the URL spells it, so a symlink inside the root led to what the rule refuses by name (r4125556296). It now applies the rule to the canonical path too. 2. SnapshotRegistry.drop left the active key naming a dropped entry, which also kept every later entry from becoming active. Dropping the active entry now clears the key. 3. snapshot.write_snapshot wrote in place (r4126138808), and canonical_json wrote NaN and Infinity (r4125900060). The write now goes to an exclusive temporary sibling, keeps the target's permission bits, is fsynced, and moves over the target in one os.replace; a value JSON cannot carry is refused as SnapshotNotWritable, a ProjectionSeamError, with nothing written. 4. SnapshotRegistry read without its lock (r4136863481). Every read now holds it. snapshot_registry also reads doc_health's pin_sentinels where it writes the sentinel, in SnapshotEntry.index_entry, rather than at module level, so the registration at openDox's registry seam can be made in a lone checkout. tests/test_governed_registry_and_writer.py holds each gap, red before this commit. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- src/openxdox/snapshot.py | 78 ++++++- src/openxdox/snapshot_registry.py | 92 ++++++-- tests/test_governed_registry_and_writer.py | 240 +++++++++++++++++++++ 3 files changed, 387 insertions(+), 23 deletions(-) create mode 100644 tests/test_governed_registry_and_writer.py diff --git a/src/openxdox/snapshot.py b/src/openxdox/snapshot.py index bad36e1..78f2c18 100644 --- a/src/openxdox/snapshot.py +++ b/src/openxdox/snapshot.py @@ -8,7 +8,9 @@ date by the generator. List ordering is the generator's responsibility. Writes go through the interactivity boundary (never around it): `write_snapshot` -takes an `OutputBoundary` and writes only under a declared output path. +takes an `OutputBoundary` and writes only under a declared output path. The +write is atomic, and a value JSON cannot carry is refused rather than written +(plan 034 T059: this module is openXdox's writer at openDox's writer seam). Validation is DELEGATED to this product's own validator, in this repository (`scripts/validate-ideation-dashboard-contracts.py`) — the schema is never @@ -19,14 +21,20 @@ from __future__ import annotations +import contextlib import json +import os +import stat import subprocess import sys import tomllib +import uuid from dataclasses import dataclass from pathlib import Path from typing import Any +from opendox import projection_seams + # RELATIVE TO THIS PRODUCT'S OWN ROOT — the repository this module ships in — # never to an aggregation checkout above it. openxFactory's § 5.2 shed # (`cc4ae9d3`) deleted `openxFactory/scripts/validate-ideation-dashboard-contracts.py` @@ -42,13 +50,37 @@ class SnapshotInvalid(Exception): """A rendered snapshot failed the pinned validator (or it could not run).""" +class SnapshotNotWritable(projection_seams.ProjectionSeamError, ValueError): + """The snapshot holds a value JSON cannot carry, so nothing is written. + + A `ProjectionSeamError`, because this module is openXdox's writer at + openDox's writer seam, and openDox's generate verbs report that family as a + refusal. A `ValueError` too, which is what `json` itself raises for the + same value.""" + + # --------------------------- canonical serialization --------------------------- def canonical_json(snapshot: dict[str, Any]) -> str: """Deterministic, diffable JSON: sorted keys, 2-space indent, trailing newline (the house canonical-render discipline — see doc_health/runner.py, - execution_lane/bundler.py).""" - return json.dumps(snapshot, indent=2, sort_keys=True, ensure_ascii=True) + "\n" + execution_lane/bundler.py). + + NOTHING JSON CANNOT CARRY (plan 034 T059). NaN and the infinities are + refused (`allow_nan=False`) rather than written as the `NaN` and + `Infinity` that Python's `json` otherwise emits, which no JSON reader + parses: not the server's, not a browser's, not the validator's. So are a + value of no JSON type and a structure that contains itself. openDox's own + writer refuses the same values (openDox-code#59, r4125900060).""" + try: + return json.dumps(snapshot, indent=2, sort_keys=True, ensure_ascii=True, + allow_nan=False) + "\n" + except (TypeError, ValueError) as exc: + raise SnapshotNotWritable( + f"the snapshot holds a value JSON cannot carry ({exc}). NaN, the " + "infinities and values of no JSON type have no JSON spelling, and " + "a file carrying one would be one no JSON reader parses, so " + "nothing was written") from exc def canonical_bytes(snapshot: dict[str, Any]) -> bytes: @@ -61,8 +93,44 @@ def load_snapshot(path: Path | str) -> dict[str, Any]: def write_snapshot(snapshot: dict[str, Any], path: Path | str, boundary) -> Path: """Render canonically and write through the interactivity boundary. Every - snapshot write lands under the boundary's declared output allowlist.""" - return boundary.write_output(path, canonical_json(snapshot)) + snapshot write lands under the boundary's declared output allowlist. + + ATOMICALLY (plan 034 T059). openDox's server answers `/snapshot.json` on + threads of its own while a refresh rewrites the very snapshot it serves, + and a write in place (truncate, then write) let a request read a truncated + file. So the bytes go to a temporary sibling first, and one `os.replace` + moves them over the target: a reader sees the whole old snapshot or the + whole new one, never part of either. openDox's own writer does the same + (openDox-code#59, r4126138808). + + THE BOUNDARY STILL DECIDES THE DESTINATION. `permit_output` is the check + `write_output` makes, root and allowlist, with its refusal and its ledger, + and it runs first, so a refused target leaves nothing behind. The + rendering runs before it, so a snapshot JSON cannot carry is refused with + nothing written either. The sibling is created exclusively beside the + permitted target, with the mode an ordinary write would give it, or with + the target's own permission bits where the target exists, so a refresh + never widens a restricted snapshot. Its name is a dot-file with no + document extension, so `/source` never serves it, and it is removed if the + write or the move fails.""" + data = canonical_json(snapshot).encode("ascii") + target = boundary.permit_output(path) + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp") + descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666) + try: + with os.fdopen(descriptor, "wb") as stream: + with contextlib.suppress(FileNotFoundError): + os.fchmod(stream.fileno(), stat.S_IMODE(os.stat(target).st_mode)) + stream.write(data) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, target) + except BaseException: + with contextlib.suppress(OSError): + os.unlink(temporary) + raise + return target # --------------------------- validator location --------------------------- diff --git a/src/openxdox/snapshot_registry.py b/src/openxdox/snapshot_registry.py index 6fdfbaa..6897939 100644 --- a/src/openxdox/snapshot_registry.py +++ b/src/openxdox/snapshot_registry.py @@ -82,7 +82,16 @@ # `scripts/` and the path insertion above already reaches it, so the spelling # comes from the declaration that a verification guarding on the exact string # reads. -from doc_health import pin_sentinels # noqa: E402 +# +# IT IS IMPORTED WHERE IT IS READ, in `SnapshotEntry.index_entry` (plan 034 +# T059). This module is openXdox's contribution at openDox's snapshot registry +# seam (`openxdox.projection_contributions`), and openDox probes a +# registration's names when it is made. `pin_sentinels` is this module's one +# reach into openxFactory's `doc_health`, which a lone openXdox-code checkout +# does not carry (R1Q6 (d), openxFactory#656 comment 5817152735). Read at +# module level, it made the registration itself fail there, and with it every +# process that registers a profile. Read in `index_entry`, it fails where the +# sentinel is written, on `doc_health`, as before, and nowhere else. DEFAULT_REF = "main" INDEX_KIND = "ideation-dashboard-snapshot-index" @@ -293,6 +302,8 @@ def index_entry(self) -> dict: and whether the snapshot's own generation lacked a revision, could not fetch one, or never recorded one is not knowable from here. Writing a stronger member would assert a condition nobody established.""" + from doc_health import pin_sentinels + out: dict[str, Any] = { "repository": self.repository, "ref": self.ref, @@ -401,17 +412,24 @@ def resolve_within(root: Path, url_tail: str) -> Path | None: A `.git` (or any dot-directory) named as the LAST component is refused by the `is_file()` check below, so the first half needs no special case for - it.""" + it. + + THE RULE IS APPLIED TWICE (plan 034 T059): to the path as the URL spells + it, and to the CANONICAL path, relative to `root`, once symlinks are + resolved. The spelling alone let a symlink inside the root lead to what the + rule refuses by name: `link -> .git` served `/source/link/config`, which is + `.git/config`, and `notes.md -> .env` served `.env`. Both resolve inside + the root, to a regular file, so confinement never saw them. openDox's own + default registry closed the same gap in its review round + (openDox-code#59, r4125556296), and this module is registered at the same + seam (`openxdox.projection_contributions`).""" rel = urllib.parse.unquote(url_tail) rel = rel.split("?", 1)[0].split("#", 1)[0] if not rel or rel.startswith("/") or "\x00" in rel: return None parts = [p for p in rel.replace("\\", "/").split("/") if p not in ("", ".")] - if any(p.startswith(".") and p != ".." for p in parts[:-1]): + if _names_something_hidden(parts): return None - if parts and parts[-1].startswith(".") and parts[-1] != "..": - if PurePosixPath(parts[-1]).suffix not in SERVED_DOTFILE_SUFFIXES: - return None root = Path(root).resolve() try: resolved = (root / rel).resolve() @@ -419,11 +437,25 @@ def resolve_within(root: Path, url_tail: str) -> Path | None: return None if resolved != root and not resolved.is_relative_to(root): return None + if _names_something_hidden(resolved.relative_to(root).parts): + return None if not resolved.is_file(): return None return resolved +def _names_something_hidden(parts: list[str] | tuple[str, ...]) -> bool: + """Whether a relative path's components name what `/source` never serves: + a dot-directory anywhere but the last component, or a last component that + is a dot-file without a projected extension. `..` is not a name, and the + escape check decides it.""" + if any(p.startswith(".") and p != ".." for p in parts[:-1]): + return True + last = parts[-1] if parts else "" + return (last.startswith(".") and last != ".." + and PurePosixPath(last).suffix not in SERVED_DOTFILE_SUFFIXES) + + # --------------------------- the registry --------------------------- @dataclass @@ -502,32 +534,55 @@ def register_aggregate(self, aggregate: Aggregate) -> Aggregate: return aggregate def drop(self, repository: str, ref: str | None = None) -> None: + """Remove an entry. Dropping the ACTIVE entry clears the active key + (plan 034 T059), so no ref-less request meets a key with nothing + behind it, and the next entry registered becomes active, as the first + one did. Left set, the stale key also kept every later registration + from becoming active, since `register` promotes only while nothing is, + so the registry held entries and answered no active one.""" with self._lock: - self._entries.pop(snapshot_key(repository, ref), None) + key = snapshot_key(repository, ref) + self._entries.pop(key, None) + if self._active == key: + self._active = None # ---- lookup ---- + # + # EVERY READ HOLDS THE LOCK (plan 034 T059). A block `atomically()` holds + # is a read-modify-write, and a reader that did not wait for it could + # answer from its middle: the active key moved and not yet put back, for + # one. openDox's own default registry closed the same gap in its review + # round (openDox-code#59, r4136863481). The lock is re-entrant, so a read + # inside a held block, or inside another read, is safe. def get(self, repository: str, ref: str | None = None) -> SnapshotEntry | None: """Ref-less lookups resolve to `main` (D4).""" - return self._entries.get(snapshot_key(repository, ref)) + key = snapshot_key(repository, ref) + with self._lock: + return self._entries.get(key) def entries(self) -> list[SnapshotEntry]: """Registered entries, ordered by (repository, ref) — a stable roster.""" - return [self._entries[k] for k in sorted(self._entries)] + with self._lock: + return [self._entries[k] for k in sorted(self._entries)] def aggregates(self) -> list[Aggregate]: - return [self._aggregates[k] for k in sorted(self._aggregates)] + with self._lock: + return [self._aggregates[k] for k in sorted(self._aggregates)] def keys(self) -> list[tuple[str, str]]: - return sorted(self._entries) + with self._lock: + return sorted(self._entries) def __len__(self) -> int: - return len(self._entries) + with self._lock: + return len(self._entries) @property def active(self) -> SnapshotEntry | None: - if self._active is None: - return None - return self._entries.get(self._active) + with self._lock: + if self._active is None: + return None + return self._entries.get(self._active) def set_active(self, repository: str, ref: str | None = None) -> SnapshotEntry | None: with self._lock: @@ -540,9 +595,10 @@ def resolve(self, repository: str | None, ref: str | None = None) -> SnapshotEnt """The one resolution every route uses: a named pair, or the ACTIVE entry when no repository is named (which is what `/snapshot.json` with no query means — today's behaviour, unchanged).""" - if repository is None or repository == "": - return self.active - return self.get(repository, ref) + with self._lock: + if repository is None or repository == "": + return self.active + return self.get(repository, ref) # ---- per-entry source confinement (task 2.2) ---- def resolve_source(self, repository: str | None, ref: str | None, tail: str) -> Path | None: diff --git a/tests/test_governed_registry_and_writer.py b/tests/test_governed_registry_and_writer.py new file mode 100644 index 0000000..b358276 --- /dev/null +++ b/tests/test_governed_registry_and_writer.py @@ -0,0 +1,240 @@ +"""Four gaps in openXdox's governed registry and writer, closed before they are +registered at openDox's seams (plan 034 T059). + +openDox's own defaults closed each of these in openDox-code#59's review +rounds, and #59's body lists them as openXdox-code's to close before T059 +registers this leg's mechanisms: + +1. `snapshot_registry.resolve_within` met the hidden-name rule only as the URL + spells a path, so a symlink inside the root led to what the rule refuses by + name (r4125556296): `link -> .git` served `.git/config`. +2. `SnapshotRegistry.drop` left the active key naming a dropped entry. +3. `snapshot.write_snapshot` wrote in place, so a reader could meet a + truncated snapshot (r4126138808), and `canonical_json` wrote NaN and + Infinity, which no JSON reader parses (r4125900060). +4. `SnapshotRegistry` read without its lock, so a reader could answer from the + middle of a block `atomically()` holds (r4136863481). + +Each case below is red against the code before T059. + +A CREATED file: no manifest row (RULED OQ-C). +""" + +from __future__ import annotations + +import json +import math +import os +import threading +import time +from pathlib import Path + +import pytest + +from opendox import projection_seams +from opendox.boundary import OutputBoundary +from openxdox import snapshot as snapshot_mod +from openxdox import snapshot_registry as reg + + +# -------------------------------------------------------------------------- +# 1: the hidden-name rule meets the canonical path too +# -------------------------------------------------------------------------- + +@pytest.fixture +def served_root(tmp_path: Path) -> Path: + root = tmp_path / "checkout" + (root / ".git").mkdir(parents=True) + (root / ".git" / "config").write_text("[remote]\n", encoding="utf-8") + (root / ".env").write_text("TOKEN=x\n", encoding="utf-8") + (root / "docs").mkdir() + (root / "docs" / "note.md").write_text("# a note\n", encoding="utf-8") + return root + + +def test_a_symlink_to_a_dot_directory_serves_nothing(served_root: Path) -> None: + (served_root / "link").symlink_to(served_root / ".git", target_is_directory=True) + assert reg.resolve_within(served_root, "link/config") is None + + +def test_a_symlink_to_a_hidden_file_serves_nothing(served_root: Path) -> None: + (served_root / "notes.md").symlink_to(served_root / ".env") + assert reg.resolve_within(served_root, "notes.md") is None + + +def test_a_symlink_to_a_document_is_still_served(served_root: Path) -> None: + (served_root / "alias.md").symlink_to(served_root / "docs" / "note.md") + assert reg.resolve_within(served_root, "alias.md") == ( + served_root / "docs" / "note.md").resolve() + assert reg.resolve_within(served_root, "docs/note.md") == ( + served_root / "docs" / "note.md").resolve() + + +def test_the_spelled_rule_still_refuses_first(served_root: Path) -> None: + assert reg.resolve_within(served_root, ".git/config") is None + assert reg.resolve_within(served_root, "%2egit/config") is None + assert reg.resolve_within(served_root, ".env") is None + + +# -------------------------------------------------------------------------- +# 2: dropping the active entry clears the active key +# -------------------------------------------------------------------------- + +def _entry(repository: str, ref: str = "main") -> reg.SnapshotEntry: + return reg.SnapshotEntry(repository=repository, ref=ref) + + +def test_dropping_the_active_entry_leaves_no_active_key() -> None: + registry = reg.SnapshotRegistry() + registry.register(_entry("alpha")) + registry.drop("alpha") + assert registry.active is None + assert registry._active is None + + +def test_after_the_active_entry_is_dropped_the_next_one_becomes_active() -> None: + """Left set, the stale key kept every later entry from becoming active, + since `register` promotes only while nothing is.""" + registry = reg.SnapshotRegistry() + registry.register(_entry("alpha")) + registry.drop("alpha") + registry.register(_entry("beta")) + assert registry.active is not None and registry.active.repository == "beta" + + +def test_dropping_another_entry_keeps_the_active_one() -> None: + registry = reg.SnapshotRegistry() + registry.register(_entry("alpha")) + registry.register(_entry("beta")) + registry.drop("beta") + assert registry.active.repository == "alpha" + + +# -------------------------------------------------------------------------- +# 3: the writer is atomic, and refuses what JSON cannot carry +# -------------------------------------------------------------------------- + +def _boundary(root: Path, name: str = "snapshot.json") -> OutputBoundary: + return OutputBoundary(root, [name]) + + +@pytest.mark.parametrize("value", [math.nan, math.inf, -math.inf], + ids=["nan", "infinity", "minus-infinity"]) +def test_a_value_json_cannot_carry_is_refused_and_nothing_is_written(tmp_path, value) -> None: + target = tmp_path / "snapshot.json" + with pytest.raises(snapshot_mod.SnapshotNotWritable): + snapshot_mod.write_snapshot({"kind": "k", "score": value}, target, + _boundary(tmp_path)) + assert list(tmp_path.iterdir()) == [] + + +def test_a_value_of_no_json_type_is_refused(tmp_path) -> None: + with pytest.raises(snapshot_mod.SnapshotNotWritable): + snapshot_mod.canonical_json({"kind": "k", "when": object()}) + + +def test_the_refusal_is_one_openDox_reports_as_a_seam_refusal() -> None: + assert issubclass(snapshot_mod.SnapshotNotWritable, projection_seams.ProjectionSeamError) + assert issubclass(snapshot_mod.SnapshotNotWritable, ValueError) + + +def test_the_write_replaces_the_target_in_one_move(tmp_path, monkeypatch) -> None: + """The bytes land in a sibling, and one `os.replace` puts them over the + target. So the target is never opened for writing.""" + target = tmp_path / "snapshot.json" + target.write_text("old\n", encoding="utf-8") + moves = [] + real_replace = os.replace + + def replace(source, destination): + assert Path(destination) == target + assert target.read_text(encoding="utf-8") == "old\n" # untouched until the move + moves.append(Path(source).name) + return real_replace(source, destination) + + monkeypatch.setattr(os, "replace", replace) + written = snapshot_mod.write_snapshot({"kind": "k"}, target, _boundary(tmp_path)) + assert written == target.resolve() + assert json.loads(target.read_text(encoding="utf-8")) == {"kind": "k"} + assert len(moves) == 1 and moves[0].startswith(".snapshot.json.") + assert sorted(p.name for p in tmp_path.iterdir()) == ["snapshot.json"] + + +def test_a_failed_move_leaves_the_old_snapshot_and_no_sibling(tmp_path, monkeypatch) -> None: + target = tmp_path / "snapshot.json" + target.write_text("old\n", encoding="utf-8") + + def refuse(source, destination): + raise OSError("the move failed") + + monkeypatch.setattr(os, "replace", refuse) + with pytest.raises(OSError, match="the move failed"): + snapshot_mod.write_snapshot({"kind": "k"}, target, _boundary(tmp_path)) + assert target.read_text(encoding="utf-8") == "old\n" + assert sorted(p.name for p in tmp_path.iterdir()) == ["snapshot.json"] + + +def test_a_rewrite_keeps_the_snapshots_permissions(tmp_path) -> None: + target = tmp_path / "snapshot.json" + target.write_text("old\n", encoding="utf-8") + target.chmod(0o600) + snapshot_mod.write_snapshot({"kind": "k"}, target, _boundary(tmp_path)) + assert (target.stat().st_mode & 0o777) == 0o600 + + +def test_the_boundary_still_decides_the_destination(tmp_path) -> None: + from opendox.boundary import BoundaryViolation + + with pytest.raises(BoundaryViolation): + snapshot_mod.write_snapshot({"kind": "k"}, tmp_path / "elsewhere.json", + _boundary(tmp_path)) + assert list(tmp_path.iterdir()) == [] + + +def test_the_bytes_are_the_canonical_render(tmp_path) -> None: + snapshot = {"b": 1, "a": {"d": [2, 1], "c": "é"}} + target = tmp_path / "snapshot.json" + snapshot_mod.write_snapshot(snapshot, target, _boundary(tmp_path)) + assert target.read_bytes() == snapshot_mod.canonical_bytes(snapshot) + + +# -------------------------------------------------------------------------- +# 4: every read waits for a held read-modify-write +# -------------------------------------------------------------------------- + +READS = { + "get": lambda r: r.get("alpha", "session/x"), + "active": lambda r: r.active, + "resolve": lambda r: r.resolve(None), + "entries": lambda r: r.entries(), + "keys": lambda r: r.keys(), + "len": lambda r: len(r), + "aggregates": lambda r: r.aggregates(), +} + + +@pytest.mark.parametrize("read", sorted(READS)) +def test_a_read_waits_for_a_held_read_modify_write(read) -> None: + """A block holds the registry, registers a session and promotes it, and + only then puts `main` back. A read started while the block holds must + answer as the block left the registry, never from its middle.""" + registry = reg.SnapshotRegistry() + registry.register(_entry("alpha")) + held = threading.Event() + answer = [] + + def reader() -> None: + held.wait() + answer.append(READS[read](registry)) + + thread = threading.Thread(target=reader) + thread.start() + with registry.atomically(): + registry.register(_entry("alpha", "session/x"), active=True) + registry.register_aggregate(reg.Aggregate(id="agg")) + held.set() + time.sleep(0.2) + assert not answer, f"{read} answered while the block held the registry" + registry.set_active("alpha") + thread.join(timeout=5) + assert answer, f"{read} never answered" From 9bbee754b1aa84fc2ccab9868d111da4dd23a7bc Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:46:58 +0000 Subject: [PATCH 03/25] openXdox contributes its governed generator, registry and source through openDox's seams (plan 034 T059, 5.4a) The governed half of R1Q10 (a). openxdox.projection_contributions registers openXdox's generator at opendox.generator_seam, and its snapshot registry, corpus-root predicate, writer and validators for the three governed kinds at opendox.projection_seams. openXdox keeps generator.py, snapshot.py, snapshot_registry.py, completeness.py and corpus_root.py; the contributions reach generator, corpus_root and completeness, which read openxFactory's doc_health at module level, only when they are used. register() is explicit, idempotent, and all or none: a seam's refusal takes back every seam it wrote, in reverse, and reaches the caller. openxdox.domain_profile.register() calls it; load() registers nothing. This is the holder's ruling on T059 (option (c)): a host that registers openXdox's profile with openDox alone, as openxFactory does, calls register() itself, which is T064's one line in openxFactory's opendox_host.register_openxfactory(). tests/test_generated_at_anchor.py patched snapshot._locate_validator, which openDox's generate verb no longer reaches; it now patches the registered validator's locate(). tests/conftest.py registers the home corpus openxFactory's host registers (corpus_adapter_openxfactory.home_corpus) where F5.2's environment composes openxFactory's scripts/, and nothing where it is absent, as #1144's 4.1a has every seam refuse unregistered (holder's ruling on T059). The ratchet is lowered for T055's reaches: cli and serve leave OPENDOX_BACK_IMPORTS, and branch_session goes to (0, 2). snapshot_registry's doc_health read is deferred, so DOC_HEALTH_SURFACE records it as (0, 1). Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- src/openxdox/domain_profile.py | 14 + src/openxdox/projection_contributions.py | 365 +++++++++++++++++++++ tests/conftest.py | 57 ++++ tests/test_dependency_direction.py | 42 ++- tests/test_generated_at_anchor.py | 12 +- tests/test_projection_contributions.py | 398 +++++++++++++++++++++++ 6 files changed, 875 insertions(+), 13 deletions(-) create mode 100644 src/openxdox/projection_contributions.py create mode 100644 tests/test_projection_contributions.py diff --git a/src/openxdox/domain_profile.py b/src/openxdox/domain_profile.py index 0cb7057..1f7c518 100644 --- a/src/openxdox/domain_profile.py +++ b/src/openxdox/domain_profile.py @@ -1095,6 +1095,17 @@ def register(profile: DomainProfile) -> DomainProfile: """THE one registration. Called by the host's own adapter at process start. Returns the profile so a host can register and hold it in one expression. + + AND openXdox's GOVERNED PROJECTION WITH IT (plan 034 T059). A process that + registers openXdox's profile is a governed host, so this also registers + openXdox's generator, snapshot registry, corpus-root predicate, writer and + validators at openDox's seams (`openxdox.projection_contributions`), before + any of openDox's entry points reads a default. It is idempotent, and all + or none: a seam's refusal reaches the caller with no seam written and no + profile registered. `load()` registers nothing, so reading or validating + a profile never changes what a process serves. A host that registers its + profile with openDox alone, as openxFactory does, calls + `projection_contributions.register()` itself (plan 034 T064). """ global _registered if not isinstance(profile, DomainProfile): @@ -1117,6 +1128,9 @@ def register(profile: DomainProfile) -> DomainProfile: "already read the first. Call " "openxdox.domain_profile.unregister() first if the swap is " "deliberate.") + from . import projection_contributions + + projection_contributions.register() _registered = profile return profile diff --git a/src/openxdox/projection_contributions.py b/src/openxdox/projection_contributions.py new file mode 100644 index 0000000..a3d5a79 --- /dev/null +++ b/src/openxdox/projection_contributions.py @@ -0,0 +1,365 @@ +"""openXdox's governed projection, contributed through openDox's seams. + +WHY THIS FILE EXISTS. Box 5.4a of openxFactory's +`add-neutral-product-standalone-operability` (RATIFIED, `openxFactory#656` +comment `5815412869`) has openXdox KEEP `generator.py`, `snapshot.py`, +`snapshot_registry.py`, `completeness.py` and `corpus_root.py`, and contribute +its governed generator through the seam 5.4 declares. R1Q10 (a) (comment +`5850003126`, in R-G3's pattern) gives every consumer mechanism on release 1's +path an openDox-owned neutral default, and has openXdox contribute its +governed one through the same seam. openDox-code declares the seams: the +generator seam (`opendox.generator_seam`, plan 034 T052) and the four +projection seams (`opendox.projection_seams`, T055). This module is openXdox's +half (plan 034 T059): it registers + +* the governed generator (`generator.generate_snapshot`) at the generator seam, + writing `ideation-dashboard-snapshot` and declaring the two inputs a governed + generation takes, `project_register_source` and `possibles_source`; +* the snapshot registry and the source over it (`snapshot_registry`) at + `projection_seams.registry`; +* the corpus-root predicate (`corpus_root`), with the corpus's change rows, at + `projection_seams.corpus_root`; +* the canonical writer (`snapshot`) at `projection_seams.writer`; +* this product's validator (`snapshot.validate_snapshot`) at + `projection_seams.validators`, for each of openXdox-spec's three kinds. + openDox's own kinds (`opendox-snapshot`, `ideation-workbench`) keep openDox's + own validator: a host that contributes its governed validator does not take + openDox's kinds with it (`projection_seams`' own rule). + +WHO CALLS `register()`. It is the one call, made ONCE, at process start, before +any of openDox's entry points reads a default: a default is sealed once read, +and a host registration over a read default is refused. + +* `openxdox.domain_profile.register()` calls it, so openXdox's own + registration path (this leg's root `conftest.py`, and any openXdox-only + host) gets the contributions with no second call. +* openxFactory registers its profile with openDox, never with openXdox, so it + makes this call itself: plan 034's T064 adds it to + `scripts/opendox_host.register_openxfactory()`. The holder decided this on + 2026-09-29 and revised the 2026-09-27 decision in openDox-code#54's body, + which assumed openxFactory already called openXdox's registration hook. +* `domain_profile.load()` does NOT call it. Loading or validating a profile + registers nothing, so a verifier, a sync or a test that only reads a profile + never changes what a process serves. + +ALL OR NONE. Each seam refuses a registration over a host's that differs, and +over openDox's default once a consumer has read it. If any seam refuses, every +seam this call wrote is emptied again, in reverse order, before the refusal +reaches the caller. A seam that already held this module's contribution was +not written, since its registration is a no-op, and it is left as it was. A +seam where this call replaced openDox's unread default is emptied too, and +openDox's entry points register the default there again, as they do wherever +nothing is registered. So a refused call never leaves one process projecting +through some governed mechanisms and some neutral ones. + +IDEMPOTENT. Each contribution below is one object, made once at import. A +second `register()` finds each seam holding the same object, and every seam +treats that as a no-op. + +RESOLVED AT USE, AS THE REACH IT REPLACES WAS. `generator.py`, `corpus_root.py` +and `completeness.py` import openxFactory's `doc_health` at module level, which +a lone openXdox-code checkout does not carry (R1Q6 (d), comment `5817152735`; +the direction arc is plan 034's T008). openDox probes a registration's names +when it is made, so a contribution that imported those modules would make the +registration itself fail in a lone checkout, and with it every process that +registers a profile. So the generator and the corpus-root predicate are +adapters that import the governed module when they are CALLED, as openDox's +`consumer_reach` stand-ins did before T055. In a lone checkout a governed +generation then fails where it always failed, on `doc_health`, and registering +does not. `snapshot_registry.py` reads `doc_health` only for one sentinel, so it +imports it there (T059), and the module itself is registered. + +A CREATED FILE: no row in openxFactory's `docs/opendox-carve-manifest.yaml`, +which declares what LEAVES openxFactory, never what a destination assembles +(RULED OQ-C). +""" + +from __future__ import annotations + +import importlib +import threading +from collections.abc import Sequence +from pathlib import Path +from typing import Any + +from opendox import generator_seam, projection_seams + +from . import snapshot as snapshot_mod +from . import snapshot_registry as snapshot_registry_mod + +__all__ = [ + "CORPUS_ROOT", + "GENERATOR", + "GENERATOR_INPUTS", + "GOVERNED_KINDS", + "GOVERNED_SNAPSHOT_KIND", + "GovernedCorpusRoot", + "GovernedValidator", + "REGISTRY", + "VALIDATOR", + "WRITER", + "generate", + "is_registered", + "register", + "unregister", +] + +#: The contract openXdox's governed generator writes, and the kind every +#: snapshot it answers carries. openXdox-spec owns its schema. +GOVERNED_SNAPSHOT_KIND = "ideation-dashboard-snapshot" + +#: The kinds this product's validator is registered for: openXdox-spec's three +#: (#1144 7.1's second row), which are this consumer's own. +GOVERNED_KINDS: tuple[str, ...] = ( + GOVERNED_SNAPSHOT_KIND, + "ideation-dashboard-snapshot-index", + "gate-action-record", +) + +#: The inputs a governed generation takes beyond the seam's own four. Both are +#: optional, as the seam requires: openDox passes one only when its caller has +#: a value for it. `generate_snapshot`'s test-only keywords (`git`, +#: `generator_version`, `excluded_documents`) are not declared, so the seam +#: never passes them. +GENERATOR_INPUTS: tuple[str, ...] = ("project_register_source", "possibles_source") + + +def _governed(name: str) -> Any: + """`openxdox.`, imported now. The one place a contribution reaches a + governed module that needs `doc_health`, so a lone checkout fails here, at + use, naming the missing module.""" + return importlib.import_module(f"{__package__}.{name}") + + +# -------------------------------------------------------------------------- +# the generator +# -------------------------------------------------------------------------- + +def generate(repo_root: Path | str, repository: str, *, + source_revision: str | None = None, + generated_at: str | None = None, + project_register_source: Path | None = None, + possibles_source: Path | None = None) -> dict: + """openXdox's governed generation, the seam's operation: + `generator.generate_snapshot`, resolved at each call.""" + return _governed("generator").generate_snapshot( + repo_root, repository, source_revision=source_revision, + generated_at=generated_at, + project_register_source=project_register_source, + possibles_source=possibles_source) + + +GENERATOR = generator_seam.SnapshotGenerator( + contract=GOVERNED_SNAPSHOT_KIND, generate=generate, inputs=GENERATOR_INPUTS) + + +# -------------------------------------------------------------------------- +# the corpus-root predicate +# -------------------------------------------------------------------------- + +class _ScannedRoots(Sequence): + """`corpus_root.SCANNED_ROOTS`, read when it is used. + + The roots derive from openxFactory's doc-health scan + (`corpus.GOVERNED_ROOTS`), so they cannot be read before `doc_health` is + reached. openDox takes the value when the registration is made and reads + it only through sequence operations (`tuple(...)`), so this defers to the + first of them. `repr` does not resolve, so a debugger or an assertion + rewrite never makes the reach.""" + + def _roots(self) -> tuple[str, ...]: + return tuple(_governed("corpus_root").SCANNED_ROOTS) + + def __getitem__(self, index): + return self._roots()[index] + + def __len__(self) -> int: + return len(self._roots()) + + def __iter__(self): + return iter(self._roots()) + + def __contains__(self, item: Any) -> bool: + return item in self._roots() + + def __repr__(self) -> str: + return "" + + +class GovernedCorpusRoot: + """openXdox's corpus-root predicate at `projection_seams.corpus_root`. + + `corpus_scan_defect`, `corpus_root_refusal` and `SCANNED_ROOTS` are + `corpus_root`'s own. `change_rows` is the corpus's change enumeration with + each change's declared staged origin, which `branch_session._change_rows` + computed from `generator.iter_changes` and + `generator.declared_origin_state` before T055 routed it here.""" + + SCANNED_ROOTS: Sequence = _ScannedRoots() + + @staticmethod + def corpus_scan_defect(repo_root: Path | str) -> str | None: + return _governed("corpus_root").corpus_scan_defect(repo_root) + + @staticmethod + def corpus_root_refusal(repo_root: Path | str, *, flag: str = "--repo-root", + shape: str = "") -> str | None: + return _governed("corpus_root").corpus_root_refusal( + repo_root, flag=flag, shape=shape) + + @staticmethod + def change_rows(checkout_root: Path | str) -> tuple: + """`(change id, status, folder, origin state, origin)` per change.""" + generator = _governed("generator") + return tuple( + (change_id, status, folder, *generator.declared_origin_state(folder)) + for change_id, status, folder, _archive_date + in generator.iter_changes(Path(checkout_root))) + + +CORPUS_ROOT = GovernedCorpusRoot() + + +# -------------------------------------------------------------------------- +# the validator +# -------------------------------------------------------------------------- + +class GovernedValidator: + """This product's validator at `projection_seams.validators`, for + openXdox-spec's three kinds. + + openDox hands it the roots a search may start from, the written snapshot's + directory first and the served checkout second. `locate()` asks + `snapshot.find_validator` from each in turn, as openDox's + `cli._locate_validator` did before T055, and the first validator found + runs. `snapshot.validate_snapshot` reaches the verdict, with its three + outcomes, and its result carries every attribute openDox reads.""" + + #: The remedy openDox prints when the validator is found but cannot run. + dependency_remedy = snapshot_mod.DEPENDENCY_REMEDY + + def locate(self, search_from: tuple = ()) -> Path | None: + """The validator from the first root that reaches one, or None.""" + for start in tuple(search_from) or (None,): + found = snapshot_mod.find_validator(None if start is None else Path(start)) + if found is not None: + return found + return None + + def validate(self, path: Path | str, *, strict: bool = False, + search_from: tuple = ()) -> snapshot_mod.ValidationResult: + validator = self.locate(search_from) + if validator is not None: + return snapshot_mod.validate_snapshot(path, validator=validator, + strict=strict) + starts = tuple(search_from) or (None,) + reasons = [] + for start in starts: + reason = snapshot_mod._validator_not_found_reason( + None if start is None else Path(start)) + if reason not in reasons: + reasons.append(reason) + return snapshot_mod.ValidationResult( + False, -1, "", "validator not found", None, + snapshot_mod.VALIDATOR_UNAVAILABLE, + f"no {snapshot_mod.VALIDATOR_RELPATH} of this product's own was " + f"found from any root offered: " + "; ".join(reasons)) + + +VALIDATOR = GovernedValidator() + +#: The registry module carries every name `projection_seams.registry` asks for. +REGISTRY = snapshot_registry_mod + +#: The canonical writer: `snapshot.write_snapshot(snapshot, path, boundary)`. +WRITER = snapshot_mod + + +# -------------------------------------------------------------------------- +# the one registration +# -------------------------------------------------------------------------- + +_lock = threading.Lock() + + +def _contributions() -> tuple[tuple[str, Any, str | None, Any], ...]: + """`(name, seam, kind, contribution)`, in the order they are registered.""" + return ( + ("generator", generator_seam, None, GENERATOR), + ("registry", projection_seams.registry, None, REGISTRY), + ("corpus_root", projection_seams.corpus_root, None, CORPUS_ROOT), + ("writer", projection_seams.writer, None, WRITER), + *((f"validators[{kind}]", projection_seams.validators, kind, VALIDATOR) + for kind in GOVERNED_KINDS), + ) + + +def _holds(seam: Any, kind: str | None, contribution: Any) -> bool: + """Does `seam` hold `contribution` now? Answered without reading it. + + The generator seam's `current()` records nothing, so it is asked. A + projection seam's `current()` and `for_kind()` close the entry point's + default's window, and asking one would turn a replaceable default into a + refusal. So its registration is read where the seam keeps it. The names + read are pinned by `tests/test_projection_contributions.py`, so a pin move + that renames them fails there rather than here.""" + if seam is generator_seam: + try: + return generator_seam.current() is contribution + except generator_seam.GeneratorNotRegistered: + return False + if kind is None: + return seam._registered is contribution + held = seam._registered.get(kind) + return held is not None and held[0] is contribution + + +def _register_one(seam: Any, kind: str | None, contribution: Any) -> None: + if kind is None: + seam.register(contribution) + else: + seam.register(kind, contribution) + + +def _take_back(seam: Any, kind: str | None) -> None: + if kind is None: + seam.unregister() + else: + seam.unregister(kind) + + +def register() -> tuple[str, ...]: + """Register every contribution at its seam, all or none. Returns the + seams' names. + + A refusal is openDox's own (`GeneratorAlreadyRegistered`, + `SeamAlreadyRegistered`), raised unchanged once every seam this call wrote + has been emptied again.""" + with _lock: + written: list[tuple[Any, str | None]] = [] + try: + for _name, seam, kind, contribution in _contributions(): + if _holds(seam, kind, contribution): + continue + _register_one(seam, kind, contribution) + written.append((seam, kind)) + except BaseException: + for seam, kind in reversed(written): + _take_back(seam, kind) + raise + return tuple(name for name, *_ in _contributions()) + + +def is_registered() -> bool: + """Does every seam hold this module's contribution?""" + return all(_holds(seam, kind, contribution) + for _name, seam, kind, contribution in _contributions()) + + +def unregister() -> None: + """Empty every seam that holds this module's contribution, and leave every + other seam as it is. For test isolation and for a host tearing down.""" + with _lock: + for _name, seam, kind, contribution in reversed(_contributions()): + if _holds(seam, kind, contribution): + _take_back(seam, kind) diff --git a/tests/conftest.py b/tests/conftest.py index 02899a5..e1c8837 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -363,3 +363,60 @@ def unregistered_profile(): _domain_profile.unregister() if previous is not None: _domain_profile.register(previous) + + +# --------------------------------------------------------------------------- +# THE HOME CORPUS, REGISTERED AS openxFactory'S HOST REGISTERS IT (plan 034 +# T059; holder's ruling on T059, 2026-09-30). +# +# openDox's `authoring.create_scaffold` asks the registered home corpus which +# fields it obliges (`scaffold_lead_fields()`, openDox-code#57, plan 034 T054), +# and with nothing registered it refuses, as #1144's 4.1a has every seam do: +# "A process in which no entry point was built and nothing registered anything +# — an import, a test, a library caller — still refuses with 4.2's +# ADAPTER_NOT_REGISTERED, so the default is a registration the entry point +# makes and never a fallback inside the seam." This leg's governed suites call +# the gate verbs in-process, as a library caller, so from that pin they refused +# on the home corpus where they passed before. +# +# So this harness registers the home a host would, as the root `conftest.py` +# registers the profile a host would: the SAME factory openxFactory's host +# registers (`corpus_adapter_openxfactory.home_corpus`, through +# `scripts/opendox_host.register_seams()`). It is found where F5.2's +# environment composes openxFactory's `scripts/` on `PYTHONPATH` (T007 batch G, +# R1Q23 (a)). The governed layout the suites were written against is that +# adapter's answer: it obliges neither `title` nor `summary`, so a scaffold +# keeps its H1 first. +# +# WHERE IT IS ABSENT, NOTHING IS REGISTERED. A lone checkout carries no +# openxFactory `scripts/`, and there the suites refuse as 4.1a says, which is +# the right answer. Every suite that reaches the home corpus also reaches +# `doc_health`, so each is in the declared exclusion already +# (`tests/declared_exclusion.yaml`), and its evidence is unchanged. Only a +# missing `corpus_adapter_openxfactory` itself means "absent": a present one +# that cannot be imported fails here, loudly. +# +# THE SUITES KEEP READING WHAT THIS CHECKOUT RESOLVES. openxFactory's adapter +# puts its own pinned openDox leg's `src/` FIRST on `sys.path` when it is +# imported (`corpus_adapter_openxfactory/adapter.py`), because that is how +# openxFactory consumes the interface. `opendox` itself is already imported by +# then, from the installed distribution, so its modules keep coming from there. +# The two top-level modules beside it, `route_extension` and +# `subcommand_extension`, are not imported yet, and a later import would have +# found the aggregation's pinned copies ahead of the ones this checkout's own +# path finds (`src/`, then the installed openDox). So both are imported first, +# from there. +import route_extension # noqa: E402,F401 +import subcommand_extension # noqa: E402,F401 + +try: + import corpus_adapter_openxfactory as _openxfactory_corpus # noqa: E402 +except ModuleNotFoundError as _absent: + if _absent.name != "corpus_adapter_openxfactory": + raise + _openxfactory_corpus = None + +if _openxfactory_corpus is not None: + from opendox import corpus_adapter as _corpus_adapter # noqa: E402 + + _corpus_adapter.register_home(_openxfactory_corpus.home_corpus) diff --git a/tests/test_dependency_direction.py b/tests/test_dependency_direction.py index 7c19eee..52c253b 100644 --- a/tests/test_dependency_direction.py +++ b/tests/test_dependency_direction.py @@ -285,7 +285,12 @@ def test_the_lawful_direction_is_actually_exercised() -> None: #: IMPLEMENTATION SURFACE here, where importing doc-health is lawful." #: MEASURED at the leg's own tree rather than carried: 13 statements over 8 #: modules under `src/` (8 at import time, 5 deferred); 25 over 15 files -#: counting the carved `tests/`. The box's "23" is a whole-package figure from +#: counting the carved `tests/`. Plan 034 T059 moved `snapshot_registry.py`'s +#: one statement from import time into `SnapshotEntry.index_entry`, where its +#: sentinel is read, so this leg's registry can be registered at openDox's +#: registry seam in a checkout without `doc_health` +#: (`openxdox.projection_contributions`). The surface is the same 13 +#: statements over the same 8 modules, now 7 at import time and 6 deferred. The box's "23" is a whole-package figure from #: design § D3's `serve.py` paragraph and is not exactly measurable as written #: — recorded, on the reality check of 2026-09-10, as an arithmetic note and #: not a defect. What this test does is make the surface ENUMERATED, so a new @@ -299,7 +304,7 @@ def test_the_lawful_direction_is_actually_exercised() -> None: "src/openxdox/gate_routes.py": (0, 1), "src/openxdox/generator.py": (3, 0), "src/openxdox/round_trip.py": (1, 0), - "src/openxdox/snapshot_registry.py": (1, 0), + "src/openxdox/snapshot_registry.py": (0, 1), } @@ -343,9 +348,9 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: this repository has since had to sweep for. It happened once more: the pin #: named `5c137a90` (openDox-code#27, § 3.4 RULED Q7) from 2026-09-17, and #: this note went on saying so after #29 moved it, until plan 034 T043 swept -#: it. The pin names `2d116415` (openDox-code#55, plan 034 T037) since -#: 2026-09-27, and `_back_import_census()` recomputed against THAT tree returns -#: exactly the five rows below. FROM `a99eba03` (#11, § 4.3) TO `5c137a90` THE +#: it. The pin named `2d116415` (openDox-code#55, plan 034 T037) from +#: 2026-09-27, and `_back_import_census()` recomputed against that tree +#: returned exactly the five rows T040 carried. FROM `a99eba03` (#11, § 4.3) TO `5c137a90` THE #: PIN CROSSED THIRTEEN openDox-code LANDINGS, from #13 (`e86deb2`) to #27 #: (`git rev-list --count --first-parent a99eba03..5c137a90` in openDox-code). #: This note named five of them, the correction Copilot's round-1 review of @@ -362,6 +367,21 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: the five rows below. The numbers here are that measurement, not a #: carried-forward memory. #: +#: PLAN 034 T059 LOWERED IT, the first fall since slice 2b. The pin moved from +#: `2d116415` to openDox-code `e3ef506a`, the head of openDox-code#59 (T055, +#: openDox's own snapshot registry and source, corpus-root predicate, writer +#: and validator lookup behind seams of their own), twenty-three first-parent +#: commits later. At that tree `_back_import_census()` returns three rows: +#: `cli.py` (0, 1) and `serve.py` (0, 2) reach (0, 0) and leave the table, and +#: `branch_session.py` falls from (0, 7) to (0, 2). The eight reaches T055 +#: closed are the ones its falsifier names: `branch_session`'s `_change_rows`, +#: `session_entry`, `register_session_entry`, `refresh_session_snapshot` and +#: `refresh_main_view`, `cli`'s `_session_registry`, and `serve`'s +#: `_checkout_real` and `_refuse_impossible_checkout_root`. Each now asks a +#: seam, and this leg contributes its governed mechanism there +#: (`openxdox.projection_contributions`). The eleven that remain are plan 034 +#: T084's, and T086 takes the table to (0, 0). +#: #: THE WHOLE OF THE INVERSION IS GONE, which is worth stating plainly because #: this table has never been able to say it before: no module of the pinned #: openDox names `openxdox` at import time. The 19 deferred reaches are NOT a @@ -397,10 +417,12 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: imports each converted module in a subprocess with `openxdox` blocked — and #: that is the right home for it: this leg measures a repository it does not #: write, and cannot import openDox's modules to find out. That file's -#: `NEUTRAL_MODULES` is the asserted half, and at `2d116415` it holds nine: +#: `NEUTRAL_MODULES` is the asserted half. At `2d116415` it held nine: #: `workbench`, `serve_workbench`, `consumer_reach`, `branch_session`, -#: `domain_profile`, `profile_proxy`, `view_extension`, `cli` and `serve`. Its -#: `STILL_REACHING` is empty there. +#: `domain_profile`, `profile_proxy`, `view_extension`, `cli` and `serve`. At +#: `e3ef506a` it holds thirteen, T055's four new modules with them: +#: `projection_seams`, `default_registry`, `default_projection` and `rfc3339`. +#: Its `STILL_REACHING` is empty at both. #: #: THE TWO MODULES THAT DID NOT IMPORT WITHOUT A CONSUMER NOW DO. This note #: said `opendox.serve` and `opendox.cli` were blocked by `ideation_dashboard` @@ -419,9 +441,7 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: improvement as well as a regression, so the number in the tree stays true. OPENDOX_BACK_IMPORTS: dict[str, tuple[int, int]] = { # module (import-time, deferred) - "opendox/branch_session.py": (0, 7), - "opendox/cli.py": (0, 1), - "opendox/serve.py": (0, 2), + "opendox/branch_session.py": (0, 2), "opendox/serve_project.py": (0, 2), "opendox/serve_workbench.py": (0, 7), } diff --git a/tests/test_generated_at_anchor.py b/tests/test_generated_at_anchor.py index 2c90b89..e47023b 100644 --- a/tests/test_generated_at_anchor.py +++ b/tests/test_generated_at_anchor.py @@ -219,8 +219,16 @@ def test_the_predicate_checks_the_instant_not_only_the_shape(): def test_a_pinned_anchor_passes_strict_validation(tmp_path, monkeypatch): """`--strict` is the lane's gate (design Decision 3 step 3), so the anchor has to satisfy the schema's `format: date-time` for real, not merely the - CLI's own predicate.""" - monkeypatch.setattr(cli_mod, "_locate_validator", lambda *a, **k: VALIDATOR) + CLI's own predicate. + + The validator is the one registered for the snapshot's kind at openDox's + validator lookup (plan 034 T059, `openxdox.projection_contributions`), + which replaced `cli._locate_validator` (T055). So the pinned validator is + handed to that registration's `locate`.""" + from openxdox import projection_contributions + + monkeypatch.setattr(projection_contributions.VALIDATOR, "locate", + lambda *a, **k: VALIDATOR) output = tmp_path / "out" / "snapshot.json" assert cli_mod.main([ "generate", "--repo-root", str(BASE_REPO), "--repository", "fixture-repo", diff --git a/tests/test_projection_contributions.py b/tests/test_projection_contributions.py new file mode 100644 index 0000000..3a80355 --- /dev/null +++ b/tests/test_projection_contributions.py @@ -0,0 +1,398 @@ +"""openXdox's governed projection is what openDox's seams hold (plan 034 T059). + +T059's falsifier opens with "seam tests that show openXdox's contributions are +the registered ones". These are they. Each seam holds openXdox's own +contribution once `projection_contributions.register()` has run: the governed +generator, this leg's snapshot registry, its corpus-root predicate, its writer, +and its validator for openXdox-spec's three kinds. openDox's own kinds keep +openDox's validator. + +The rest pin the registration's contract: it is idempotent, all or none, made +by `domain_profile.register()` and never by `domain_profile.load()`, and +import-free of `doc_health`, so a lone checkout registers and fails only where +a governed generation runs. + +This leg's root `conftest.py` registers the fixture profile at import, and so +the contributions. A case that empties a seam runs inside `isolated_seams`, +which puts the contributions back afterwards. + +A CREATED file: no manifest row (RULED OQ-C). +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + +from opendox import generator_seam, projection_seams +from openxdox import domain_profile, projection_contributions as pc +from openxdox import snapshot as snapshot_mod +from openxdox import snapshot_registry + +REPO_ROOT = Path(__file__).resolve().parents[1] +SRC = REPO_ROOT / "src" +PROFILE_FIXTURE = REPO_ROOT / "tests" / "fixtures" / "openxfactory-engineering-profile.yaml" + + +def _empty_every_seam() -> None: + generator_seam.unregister() + projection_seams.registry.unregister() + projection_seams.corpus_root.unregister() + projection_seams.writer.unregister() + projection_seams.validators.unregister() + + +@pytest.fixture +def isolated_seams(): + """Run one case with every seam empty, then put the contributions back. + + openDox's entry points register their own defaults again wherever nothing + is registered, so a later case that runs one gets them as before.""" + _empty_every_seam() + try: + yield + finally: + _empty_every_seam() + pc.register() + + +def _registered_profile(): + return domain_profile.current() if domain_profile.is_registered() else None + + +# -------------------------------------------------------------------------- +# the registered ones +# -------------------------------------------------------------------------- + +def test_each_seam_holds_openxdoxs_contribution() -> None: + """The process this suite runs in registered the fixture profile at + import, so every seam holds openXdox's own, and nothing else.""" + assert pc.is_registered() + assert generator_seam.current() is pc.GENERATOR + assert projection_seams.registry.current() is snapshot_registry + assert projection_seams.corpus_root.current() is pc.CORPUS_ROOT + assert projection_seams.writer.current() is snapshot_mod + for kind in pc.GOVERNED_KINDS: + assert projection_seams.validators.for_kind(kind) is pc.VALIDATOR + + +def test_the_generator_is_the_governed_one_declaring_its_two_inputs() -> None: + assert pc.GENERATOR.contract == "ideation-dashboard-snapshot" + assert pc.GENERATOR.inputs == ("project_register_source", "possibles_source") + assert pc.GOVERNED_KINDS == ("ideation-dashboard-snapshot", + "ideation-dashboard-snapshot-index", + "gate-action-record") + + +def test_the_generator_hands_the_seams_call_to_generate_snapshot(monkeypatch) -> None: + """Every argument the seam passes reaches `generate_snapshot` unchanged, + and nothing else does.""" + calls = [] + + class Generator: + @staticmethod + def generate_snapshot(*args, **kwargs): + calls.append((args, kwargs)) + return {"kind": "ideation-dashboard-snapshot", "schema_version": 1} + + monkeypatch.setattr(pc, "_governed", + lambda name: Generator if name == "generator" else None) + answered = generator_seam.generate( + Path("/corpus"), "repo", source_revision="a" * 40, + generated_at="2026-09-30T00:00:00Z", + project_register_source=Path("/register.yaml")) + assert answered == {"kind": "ideation-dashboard-snapshot", "schema_version": 1} + assert calls == [((Path("/corpus"), "repo"), { + "source_revision": "a" * 40, "generated_at": "2026-09-30T00:00:00Z", + "project_register_source": Path("/register.yaml"), + "possibles_source": None})] + + +def test_openDoxs_own_kinds_keep_openDoxs_validator(isolated_seams) -> None: + """The entry points' defaults land on openDox's kinds, and none of ours.""" + pc.register() + projection_seams.register_defaults() + from opendox import default_projection + + for kind in default_projection.OWN_KINDS: + assert projection_seams.validators.for_kind(kind) is default_projection.VALIDATOR + for kind in pc.GOVERNED_KINDS: + assert projection_seams.validators.for_kind(kind) is pc.VALIDATOR + assert projection_seams.registry.current() is snapshot_registry + + +def test_the_entry_points_defaults_do_not_displace_it(isolated_seams) -> None: + pc.register() + projection_seams.register_defaults() + from opendox import default_generator + + generator_seam.register_default(default_generator.GENERATOR) + assert pc.is_registered() + + +# -------------------------------------------------------------------------- +# idempotent, and all or none +# -------------------------------------------------------------------------- + +def test_a_second_registration_is_a_no_op() -> None: + names = pc.register() + assert names == pc.register() + assert pc.is_registered() + + +def test_a_refusal_takes_back_every_seam_this_call_wrote(isolated_seams) -> None: + """A host's other writer is registered, so the writer seam refuses. The + generator, registry and corpus-root seams, written before it, are empty + again, and the host's writer is untouched.""" + + class OtherWriter: + @staticmethod + def write_snapshot(snapshot, path, boundary): # pragma: no cover + raise AssertionError("never called") + + other = OtherWriter() + projection_seams.writer.register(other) + with pytest.raises(projection_seams.SeamAlreadyRegistered): + pc.register() + assert not generator_seam.is_registered() + assert not projection_seams.registry.is_registered() + assert not projection_seams.corpus_root.is_registered() + assert projection_seams.writer.current() is other + assert projection_seams.validators.kinds() == () + + +def test_a_replaced_unread_default_is_taken_back_too(isolated_seams) -> None: + """The entry points registered their defaults, and something read the + writer's. This call replaces the unread defaults before the writer + refuses, and empties them again, so no seam is left governed while the + writer stays neutral.""" + from opendox import default_generator, default_projection + + generator_seam.register_default(default_generator.GENERATOR) + projection_seams.register_defaults() + assert projection_seams.writer.current() is default_projection.WRITER # read + with pytest.raises(projection_seams.SeamAlreadyRegistered): + pc.register() + assert not generator_seam.is_registered() + assert not projection_seams.registry.is_registered() + assert not projection_seams.corpus_root.is_registered() + assert projection_seams.writer.current() is default_projection.WRITER + + +def test_a_seam_already_holding_the_contribution_is_not_taken_back(isolated_seams) -> None: + """The registry already held this leg's module, so this call did not write + it, and a refusal later leaves it where it was.""" + + class OtherWriter: + @staticmethod + def write_snapshot(snapshot, path, boundary): # pragma: no cover + raise AssertionError("never called") + + projection_seams.registry.register(snapshot_registry) + projection_seams.writer.register(OtherWriter()) + with pytest.raises(projection_seams.SeamAlreadyRegistered): + pc.register() + assert projection_seams.registry.current() is snapshot_registry + assert not generator_seam.is_registered() + + +def test_asking_whether_it_is_registered_leaves_a_default_replaceable(isolated_seams) -> None: + """`is_registered()` reads each projection seam where it keeps its + registration, because `current()` would close the default's window. So a + default asked about stays replaceable, and the names read are pinned + here: a pin move that renamed them fails this case.""" + projection_seams.register_defaults() + assert not pc.is_registered() + assert hasattr(projection_seams.registry, "_registered") + assert isinstance(projection_seams.validators._registered, dict) + pc.register() + assert pc.is_registered() + + +def test_unregister_empties_only_what_it_holds(isolated_seams) -> None: + pc.register() + from opendox import default_projection + + projection_seams.validators.register_default( + "opendox-snapshot", default_projection.VALIDATOR) + pc.unregister() + assert not generator_seam.is_registered() + assert not projection_seams.writer.is_registered() + assert projection_seams.validators.kinds() == ("opendox-snapshot",) + + +# -------------------------------------------------------------------------- +# who registers it +# -------------------------------------------------------------------------- + +def test_registering_openxdoxs_profile_registers_the_contributions(isolated_seams) -> None: + held = _registered_profile() + domain_profile.unregister() + try: + domain_profile.register(domain_profile.load(PROFILE_FIXTURE)) + assert pc.is_registered() + finally: + domain_profile.unregister() + if held is not None: + domain_profile.register(held) + + +def test_a_refused_contribution_leaves_no_profile_registered(isolated_seams) -> None: + class OtherWriter: + @staticmethod + def write_snapshot(snapshot, path, boundary): # pragma: no cover + raise AssertionError("never called") + + held = _registered_profile() + domain_profile.unregister() + projection_seams.writer.register(OtherWriter()) + try: + with pytest.raises(projection_seams.SeamAlreadyRegistered): + domain_profile.register(domain_profile.load(PROFILE_FIXTURE)) + assert not domain_profile.is_registered() + finally: + projection_seams.writer.unregister() + if held is not None: + domain_profile.register(held) + + +def test_loading_a_profile_registers_nothing(isolated_seams) -> None: + domain_profile.load(PROFILE_FIXTURE) + assert not generator_seam.is_registered() + assert not projection_seams.registry.is_registered() + assert projection_seams.validators.kinds() == () + + +# -------------------------------------------------------------------------- +# resolved at use +# -------------------------------------------------------------------------- + +_BLOCKED = textwrap.dedent(''' + import importlib.abc, sys + + class Block(importlib.abc.MetaPathFinder): + def find_spec(self, name, path=None, target=None): + if name == "doc_health" or name.startswith("doc_health."): + raise ModuleNotFoundError(f"No module named {name!r}", name=name) + return None + + sys.meta_path.insert(0, Block()) +''') + + +def _run_blocked(body: str) -> subprocess.CompletedProcess: + env = {**os.environ, "PYTHONPATH": str(SRC)} + return subprocess.run( + [sys.executable, "-c", _BLOCKED + textwrap.dedent(body)], + capture_output=True, text=True, env=env, cwd=str(REPO_ROOT)) + + +def test_registering_reaches_no_module_that_needs_doc_health() -> None: + """With `doc_health` unimportable, the registration succeeds and imports + none of the governed modules that need it.""" + done = _run_blocked(''' + import json, sys + from openxdox import projection_contributions as pc + pc.register() + print(json.dumps({ + "registered": pc.is_registered(), + "loaded": sorted(m for m in ("openxdox.generator", "openxdox.corpus_root", + "openxdox.completeness", "doc_health") + if m in sys.modules)})) + ''') + assert done.returncode == 0, done.stderr + assert json.loads(done.stdout.splitlines()[-1]) == {"registered": True, "loaded": []} + + +def test_without_doc_health_a_governed_generation_fails_on_doc_health() -> None: + """Where it always failed, and with the same final exception, which is + what the declared exclusion's `doc_health` evidence reads.""" + done = _run_blocked(''' + from pathlib import Path + from openxdox import projection_contributions as pc + pc.register() + from opendox import generator_seam + try: + generator_seam.generate(Path("."), "repo") + except ModuleNotFoundError as exc: + print("refused:", exc.name) + else: + print("generated") + ''') + assert done.returncode == 0, done.stderr + assert done.stdout.strip().splitlines()[-1] == "refused: doc_health" + + +def test_the_scanned_roots_are_read_when_used(monkeypatch) -> None: + reads = [] + + class CorpusRoot: + @property + def SCANNED_ROOTS(self): # noqa: N802 - the governed module's own name + reads.append(1) + return ("docs", "openspec") + + monkeypatch.setattr(pc, "_governed", lambda name: CorpusRoot()) + roots = pc.CORPUS_ROOT.SCANNED_ROOTS + assert "read when used" in repr(roots) and reads == [] + assert tuple(roots) == ("docs", "openspec") + assert len(roots) == 2 and roots[1] == "openspec" and "docs" in roots + + +def test_the_change_rows_are_the_governed_enumeration_with_each_origin(monkeypatch) -> None: + """What `branch_session._change_rows` computed before T055 routed it here.""" + folder = Path("/corpus/openspec/changes/add-x") + + class Generator: + @staticmethod + def iter_changes(root): + assert root == Path("/corpus") + return [("add-x", "active", folder, None)] + + @staticmethod + def declared_origin_state(path): + assert path == folder + return ("staged", "ideation/staging/x") + + monkeypatch.setattr(pc, "_governed", lambda name: Generator) + assert projection_seams.corpus_root.current().change_rows("/corpus") == ( + ("add-x", "active", folder, "staged", "ideation/staging/x"),) + + +# -------------------------------------------------------------------------- +# the validator +# -------------------------------------------------------------------------- + +def test_the_validator_is_located_from_each_root_in_turn(monkeypatch, tmp_path) -> None: + found = tmp_path / "validator.py" + asked = [] + + def find_validator(start): + asked.append(start) + return found if start == tmp_path / "second" else None + + ran = [] + monkeypatch.setattr(snapshot_mod, "find_validator", find_validator) + monkeypatch.setattr(snapshot_mod, "validate_snapshot", + lambda path, **kw: ran.append((path, kw)) or "result") + result = pc.VALIDATOR.validate(tmp_path / "s.json", strict=True, + search_from=(tmp_path / "first", tmp_path / "second")) + assert result == "result" + assert asked == [tmp_path / "first", tmp_path / "second"] + assert ran == [(tmp_path / "s.json", {"validator": found, "strict": True})] + + +def test_no_validator_from_any_root_is_unavailable_naming_the_script(monkeypatch, tmp_path) -> None: + monkeypatch.setattr(snapshot_mod, "find_validator", lambda start: None) + result = pc.VALIDATOR.validate(tmp_path / "s.json", + search_from=(tmp_path / "a", tmp_path / "b")) + assert result.outcome == projection_seams.VALIDATOR_UNAVAILABLE + assert not result.available and result.validator is None + assert str(snapshot_mod.VALIDATOR_RELPATH) in result.unavailable_reason + assert pc.VALIDATOR.dependency_remedy == snapshot_mod.DEPENDENCY_REMEDY From e02318399ce8dbb3a9c1c8fce9573b0f2fd649c4 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:47:12 +0000 Subject: [PATCH 04/25] The seam-assembly file leaves the declared exclusion (plan 034 T059) tests/test_seam_assembly_beside_gate_and_projection.py was declared under doc_health (R1Q6 (d)) by T044 because serve_projection reached doc_health through snapshot_registry at import. That read is now deferred to where the sentinel is written, so the file passes alone in a lone checkout, and T041's test_declared_exclusion would refuse it as declared. Its entry leaves (67 -> 66 files), and its six cases run in this leg's required check (holder's ruling on T059). T059 joins the exclusion file's single-writer chain between T044 and T061. The four seam files' docstrings and validate.yml's paragraphs stop saying the file is declared, and validate.yml's pin prose names the new openDox pin. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 16 ++++++----- tests/declared_exclusion.yaml | 3 +-- tests/test_declared_exclusion.py | 4 +-- .../test_evidence_provenance_surface_seam.py | 10 +++---- tests/test_model_scenario_workbench_seam.py | 10 +++---- tests/test_role_authority_projection_seam.py | 16 +++++------ ...eam_assembly_beside_gate_and_projection.py | 27 ++++++++++--------- 7 files changed, 45 insertions(+), 41 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1723c63..d59134a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -131,9 +131,10 @@ jobs: # `tests/integration/`. The chain was off because `tests/conftest.py` # imported carved session fixtures that reached `ideation_dashboard` and # `doc_health`, two packages neither leg carries. At the openDox this - # leg now pins (openDox-code `2d116415`, since plan 034 T040), the chain - # loads in a lone checkout, and what still reaches openxFactory is what - # the declaration and `LEFT_OUT` hold. Each of the sixteen files' + # leg pinned from plan 034 T040 (openDox-code `2d116415`), and at the one + # it pins since plan 034 T059 (`e3ef506a`), the chain loads in a lone + # checkout, and what still reaches openxFactory is what the declaration + # and `LEFT_OUT` hold. Each of the sixteen files' # account of itself (why it joined the list, its counts, its review # rounds) is in this file's history at `d84b5048` # (`git show d84b5048:.github/workflows/validate.yml`), and is not @@ -261,8 +262,8 @@ jobs: # gap were the three seam suites' `doc_health` pairs. T044 made each of # them assert for real and moved the six into # `tests/test_seam_assembly_beside_gate_and_projection.py`, which the - # declaration lists under `doc_health` (R1Q6 (d)), so SELECTED and - # SKIPPED each lose those six. The declaration's own check gains one + # declaration then listed under `doc_health` (R1Q6 (d); it left in plan + # 034 T059, below), so SELECTED and SKIPPED each lose those six. The declaration's own check gains one # case, that file run alone, which passes, so SELECTED and PASSED each # gain one. SELECTED goes 890 -> 885, PASSED 880 -> 881 and SKIPPED # 10 -> 4. The 24 seam cases that need neither column stay where they @@ -286,8 +287,9 @@ jobs: # `tests/test_model_scenario_workbench_seam.py` and # `tests/test_role_authority_projection_seam.py` into # `tests/test_seam_assembly_beside_gate_and_projection.py`, which the - # declaration lists under `doc_health`. So none of them is a skip any - # more, and none is in these numbers. None comes from + # declaration listed under `doc_health` until plan 034 T059 took it + # out. So none of them is a skip any more: from T044 to T059 none was + # in these numbers, and since T059 all six are, as passes. None comes from # `tests/integration/`, where nothing skips (its rule 2). If the count # moves, move the pin WITH the reason — never relax the comparison. # SKIPPED is `==` where the other two are `>=` because a file that diff --git a/tests/declared_exclusion.yaml b/tests/declared_exclusion.yaml index b40ebb9..fa631c6 100644 --- a/tests/declared_exclusion.yaml +++ b/tests/declared_exclusion.yaml @@ -57,7 +57,7 @@ schema_version: 1 kind: declared-test-exclusion -count: 67 +count: 66 reasons: - id: doc_health @@ -123,7 +123,6 @@ entries: - {path: tests/test_repo_root_guard.py, reasons: [doc_health]} - {path: tests/test_repo_selector.py, reasons: [doc_health]} - {path: tests/test_round_trip.py, reasons: [doc_health]} - - {path: tests/test_seam_assembly_beside_gate_and_projection.py, reasons: [doc_health], note: "six cases plan 034 T044 moved here from the three route seam suites, where each skipped on doc_health"} - {path: tests/test_session_commits.py, reasons: [doc_health]} - {path: tests/test_session_confinement.py, reasons: [doc_health]} - {path: tests/test_session_document_ownership.py, reasons: [doc_health]} diff --git a/tests/test_declared_exclusion.py b/tests/test_declared_exclusion.py index a0ea956..23d0163 100644 --- a/tests/test_declared_exclusion.py +++ b/tests/test_declared_exclusion.py @@ -123,8 +123,8 @@ def _load() -> dict: # its own. So a cause followed by an unrelated failure, a cause raised while # an unrelated exception was being handled, a cause beside an unrelated one, # and a cause's words quoted in some other failure are each unattributed, and -# no two reasons can take one result. The shapes are the ones the 67 listed -# files' 234 red results take when each file runs alone. +# no two reasons can take one result. The shapes are the ones the 67 files +# listed at plan 034 T044 took, 234 red results, when each ran alone. # --------------------------------------------------------------------------- #: `doc_health` raised missing. Where it is absent, the interpreter names diff --git a/tests/test_evidence_provenance_surface_seam.py b/tests/test_evidence_provenance_surface_seam.py index 11907a6..416391b 100644 --- a/tests/test_evidence_provenance_surface_seam.py +++ b/tests/test_evidence_provenance_surface_seam.py @@ -20,13 +20,13 @@ authority alone, which needs nothing a lone checkout lacks, it is asserted below. Beside the gate and projection columns it is asserted in `tests/test_seam_assembly_beside_gate_and_projection.py`, because -`serve_projection` reaches openxFactory's `doc_health` when it is imported +`serve_projection` reached openxFactory's `doc_health` when it was imported (through `snapshot_registry`), which no lone checkout supplies. Those two cases sat here behind a guard that skipped on every lone run, until T044 made -them assert for real and moved them. That file is in the declared exclusion -(`tests/declared_exclusion.yaml`) under `doc_health`. It runs wherever -`doc_health` is present, and everywhere else it is reported as an open -extraction. +them assert for real and moved them. That file was in the declared exclusion +(`tests/declared_exclusion.yaml`) under `doc_health` until plan 034 T059 moved +`snapshot_registry`'s one `doc_health` read out of its module level. It runs +in this leg's required check now. """ from __future__ import annotations diff --git a/tests/test_model_scenario_workbench_seam.py b/tests/test_model_scenario_workbench_seam.py index 7a9c228..178bc25 100644 --- a/tests/test_model_scenario_workbench_seam.py +++ b/tests/test_model_scenario_workbench_seam.py @@ -27,13 +27,13 @@ (d) IS ASSERTED IN TWO PLACES, since T044. Beside the other two § 4.5 features, which need nothing a lone checkout lacks, it is asserted below. Beside the gate and projection columns it is asserted in that file, because -`serve_projection` reaches openxFactory's `doc_health` when it is imported +`serve_projection` reached openxFactory's `doc_health` when it was imported (through `snapshot_registry`), which no lone checkout supplies. Those two cases sat here behind a guard that skipped on every lone run, until T044 made -them assert for real and moved them. That file is in the declared exclusion -(`tests/declared_exclusion.yaml`) under `doc_health`. It runs wherever -`doc_health` is present, and everywhere else it is reported as an open -extraction. +them assert for real and moved them. That file was in the declared exclusion +(`tests/declared_exclusion.yaml`) under `doc_health` until plan 034 T059 moved +`snapshot_registry`'s one `doc_health` read out of its module level. It runs +in this leg's required check now. """ from __future__ import annotations diff --git a/tests/test_role_authority_projection_seam.py b/tests/test_role_authority_projection_seam.py index f6c1ad5..79f8a20 100644 --- a/tests/test_role_authority_projection_seam.py +++ b/tests/test_role_authority_projection_seam.py @@ -14,14 +14,14 @@ class it will be mixed into (`resolve_handlers` — the "a route that cannot be (d) IS ASSERTED ELSEWHERE, since plan 034 task T044. The two columns are `serve_gate.GateRoutesExtension` and `serve_projection.ProjectionRoutesExtension`, -and `serve_projection` reaches openxFactory's `doc_health` when it is imported -(through `snapshot_registry`), which no lone checkout supplies. So this -suite's two cases for (d) sat behind a guard that skipped on every lone run, -until T044 made them assert for real and moved them to -`tests/test_seam_assembly_beside_gate_and_projection.py`. That file is in the -declared exclusion (`tests/declared_exclusion.yaml`) under `doc_health`. It -runs wherever `doc_health` is present, and everywhere else it is reported as -an open extraction. +and `serve_projection` reached openxFactory's `doc_health` when it was +imported (through `snapshot_registry`), which no lone checkout supplies. So +this suite's two cases for (d) sat behind a guard that skipped on every lone +run, until T044 made them assert for real and moved them to +`tests/test_seam_assembly_beside_gate_and_projection.py`. That file was in the +declared exclusion (`tests/declared_exclusion.yaml`) under `doc_health` until +plan 034 T059 moved `snapshot_registry`'s one `doc_health` read out of its +module level. It runs in this leg's required check now. """ from __future__ import annotations diff --git a/tests/test_seam_assembly_beside_gate_and_projection.py b/tests/test_seam_assembly_beside_gate_and_projection.py index 7404bc8..3ef98ca 100644 --- a/tests/test_seam_assembly_beside_gate_and_projection.py +++ b/tests/test_seam_assembly_beside_gate_and_projection.py @@ -26,18 +26,21 @@ `test_assembly_is_order_insensitive`. The two columns are imported at module level, beside the three § 4.5 extensions, because every case in this file needs both. Where `doc_health` is present, as it is with openxFactory's -`scripts/` on `PYTHONPATH`, all six run and pass. In a lone checkout this -module cannot be imported, so it fails at collection, on `doc_health` alone. - -WHY IT IS DECLARED. That makes this file one that a lone checkout cannot run. -So `tests/declared_exclusion.yaml` lists it under `doc_health` (R1Q6 (d), -openxFactory#656 comment 5817152735), and the root `conftest.py` leaves it -out of every whole-suite run and reports it as an OPEN extraction. -`tests/test_declared_exclusion.py` holds it to that entry: run alone, it must -fail, and only on `doc_health`. Once the doc_health direction arc (plan 034 -T008) lets these modules import in a lone checkout, this file stops failing -and that check turns red. The file then leaves the declaration, in the pull -request that clears the reason. +`scripts/` on `PYTHONPATH`, all six run and pass. + +WHY IT WAS DECLARED, AND WHY IT IS NOT NOW. At T044 this module could not be +imported in a lone checkout: `serve_projection` imports `snapshot_registry`, +which read openxFactory's `doc_health` at module level. So +`tests/declared_exclusion.yaml` listed this file under `doc_health` (R1Q6 (d), +openxFactory#656 comment 5817152735), and the root `conftest.py` left it out +of every whole-suite run. Plan 034 T059 moved that read into the one method +that uses it (`SnapshotEntry.index_entry`), so openXdox's registry can be +registered at openDox's registry seam in a checkout without `doc_health` +(`openxdox.projection_contributions`). The module now imports in a lone +checkout, and all six cases pass there, so `tests/test_declared_exclusion.py` +turned red on this file's entry, as it is built to. The entry left the +declaration in T059, the pull request that cleared its reason, and the six +cases run in this leg's required check. WHAT STAYS BEHIND. The 24 seam cases that need neither column stay in their three suites, in the required check. They cover structural conformance, the From 7dfe70ca228025e551bd67ba479bbd673edd96fa Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:47:23 +0000 Subject: [PATCH 05/25] Wire the reviewed allow-list's subtraction into the unedited-by-the-arc check (plan 034 T059, T007 batch C) F5.2 and 12.5's falsifier each end by refusing any protected suite an arc landing touched. Batch C (RULED R1Q7 (a), openxFactory#656 comment 5817152735) has the check subtract the edits entered in tests/protected_suite_respellings.yaml, and only after validating that the landing's diff for that suite is exactly the entry's recorded text. scripts/protected_suites.py is that last step. The falsifier still lists the landings and the protected suites; the script admits a touched suite only where one entry holds at the landing (before and after blobs, old occurring once and giving the after text byte for byte, and both inside the named test), and exits 2, subtracting nothing, when the allow-list breaks its own rules. tests/test_protected_suite_check.py holds each rule against scratch histories. The allow-list's header now says who reads it and states the named-test condition. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/protected_suites.py | 315 +++++++++++++++++++++++++ tests/protected_suite_respellings.yaml | 15 +- tests/test_protected_suite_check.py | 255 ++++++++++++++++++++ 3 files changed, 580 insertions(+), 5 deletions(-) create mode 100644 scripts/protected_suites.py create mode 100644 tests/test_protected_suite_check.py diff --git a/scripts/protected_suites.py b/scripts/protected_suites.py new file mode 100644 index 0000000..f02e188 --- /dev/null +++ b/scripts/protected_suites.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +"""The "unedited by the arc" check of F5.2 and 12.5's falsifier, with the +reviewed allow-list subtracted (plan 034 T059; T007's batch C). + +WHAT IT REPLACES. #1144's F5.2 (5.4a's falsifier) and 12.5's falsifier each end +the same way. They list the arc's landings in this repository +(`git log --first-parent --grep='^Arc: neutral-product-standalone-operability$' +"$ARC_BASE..HEAD"`), collect every path each landing touched against the main +before it (`git diff --name-only "$c^1" "$c"`), and refuse if any of those paths +is one of their protected suites. T007's batch C amends both (RULED R1Q7 (a), +openxFactory#656 comment `5817152735`): the check SUBTRACTS the edits entered in +this repository's reviewed allow-list, `tests/protected_suite_respellings.yaml`, +and it must validate, before trusting any subtraction, that the landing's +actual diff for that path contains ONLY the entry's recorded text. A path whose +landing diff does not match stays refused, exactly like an unentered edit. +Batch C gives the wiring to T059 and T086. This is T059's. + +HOW EACH FALSIFIER CALLS IT. The protected set and the landings are computed in +the falsifier's own block, as #1144 writes them. The last step, the inline +Python that intersected them, becomes this call, from the checkout's root: + + python3 scripts/protected_suites.py "$W/x-arc.txt" "$W/gen-suites.txt" # F5.2 + python3 scripts/protected_suites.py "$W/x-arc.txt" "$W/governed.txt" # 12.5 + +where `x-arc.txt` holds the landings, one commit per line, and the second file +the protected suites, one path per line. It exits 0 when no landing touched a +protected suite outside an entry that holds, 1 when one did (naming each +landing and path), and 2 when the allow-list itself breaks its rules. + +WHEN AN ENTRY HOLDS (the file's own header states the rule, and T060 wrote it). +For a landing L that touches a protected `suite`, an entry for that suite holds +at L when all of these are true: + +* `git rev-parse L^1:` is its `before_blob`, and `git rev-parse + L:` is its `after_blob`; +* its `old` text occurs exactly once in the `before_blob` text, and replacing + it with `new` gives the `after_blob` text byte for byte; +* the replaced text lies inside the one test the entry names, in the before + text, and its replacement lies inside that test in the after text. So an + entry cannot admit an edit to any other test of the suite. + +A landing that touches a protected suite is admitted for that suite only if one +entry holds at it. Every other protected path it touches is refused. + +THE ALLOW-LIST'S OWN RULES are checked before anything is subtracted, and a +file that breaks one refuses the whole check (exit 2) rather than subtracting +less: `schema_version` 1, `kind` `protected-suite-respellings`, no key given +twice, and each entry carrying exactly its declared keys, with blobs that are +full object ids, `old` and `new` that are whole lines ending in a newline, and +entries for one suite that chain (each `before_blob` is the previous entry's +`after_blob`). + +WHAT IT DOES NOT DO. It does not decide what is protected, and it does not find +the landings: both are the falsifier's, so the check here cannot drift from the +text #1144 ratified. It reads the allow-list from the working tree, at the head +the falsifier runs at. A CREATED file: no row in openxFactory's +`docs/opendox-carve-manifest.yaml` (RULED OQ-C). +""" + +from __future__ import annotations + +import ast +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import yaml + +ALLOW_LIST = Path("tests") / "protected_suite_respellings.yaml" +KIND = "protected-suite-respellings" +SCHEMA_VERSION = 1 + +#: Every key an entry may carry, and the keys each kind of edit requires. +COMMON_KEYS = frozenset({"suite", "test", "landing", "edit", "ruled", "review", + "before_blob", "after_blob", "old", "new"}) +EDIT_KEYS = {"respelling": frozenset({"respelled"}), + "admitted": frozenset({"reason"})} + +_BLOB = re.compile(r"[0-9a-f]{40}") +_SUITE = re.compile(r"tests/test_[A-Za-z0-9_]+\.py") +_LANDING = re.compile(r"opensoft/openXdox-code#[1-9][0-9]*") +_TEST = re.compile(r"test_[A-Za-z0-9_]+") + + +class AllowListInvalid(ValueError): + """The allow-list breaks one of its own rules. Nothing is subtracted.""" + + +class _UniqueKeyLoader(yaml.SafeLoader): + """YAML keeps only the last of two equal keys. Refuse the second instead.""" + + +def _mapping(loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False): + seen: set[Any] = set() + for key_node, _value in node.value: + key = loader.construct_object(key_node, deep=deep) + if key in seen: + raise AllowListInvalid( + f"the key {key!r} is given twice (line {key_node.start_mark.line + 1})") + seen.add(key) + return loader.construct_mapping(node, deep=deep) + + +_UniqueKeyLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _mapping) + + +def _text(value: Any, where: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise AllowListInvalid(f"{where} is not a non-empty text") + return value + + +def _whole_lines(value: Any, where: str) -> str: + text = _text(value, where) + if not text.endswith("\n"): + raise AllowListInvalid(f"{where} does not end in a newline, so it is not whole lines") + return text + + +def load_allow_list(path: Path) -> list[dict]: + """The allow-list's entries, in landing order, once every rule holds.""" + try: + raw = yaml.load(path.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader) + except OSError as exc: + raise AllowListInvalid(f"{path} could not be read: {exc}") from exc + except yaml.YAMLError as exc: + raise AllowListInvalid(f"{path} is not valid YAML: {exc}") from exc + if not isinstance(raw, dict): + raise AllowListInvalid(f"{path} is not a mapping") + if set(raw) != {"schema_version", "kind", "entries"}: + raise AllowListInvalid( + f"{path} carries {sorted(map(str, raw))}, not schema_version, kind and entries") + version = raw["schema_version"] + if isinstance(version, bool) or version != SCHEMA_VERSION: + raise AllowListInvalid(f"schema_version is {version!r}, not {SCHEMA_VERSION}") + if raw["kind"] != KIND: + raise AllowListInvalid(f"kind is {raw['kind']!r}, not {KIND!r}") + entries = raw["entries"] + if not isinstance(entries, list): + raise AllowListInvalid("entries is not a list") + last_after: dict[str, str] = {} + for n, entry in enumerate(entries, 1): + where = f"entry {n}" + if not isinstance(entry, dict): + raise AllowListInvalid(f"{where} is not a mapping") + edit = entry.get("edit") + if edit not in EDIT_KEYS: + raise AllowListInvalid(f"{where}: edit is {edit!r}, not one of {sorted(EDIT_KEYS)}") + wanted = COMMON_KEYS | EDIT_KEYS[edit] + if set(entry) != wanted: + raise AllowListInvalid( + f"{where}: carries {sorted(map(str, entry))}, and a {edit} entry " + f"carries exactly {sorted(wanted)}") + for key in wanted - {"old", "new"}: + _text(entry[key], f"{where}: {key}") + if not _SUITE.fullmatch(entry["suite"]): + raise AllowListInvalid(f"{where}: suite {entry['suite']!r} is not tests/test_.py") + if not _TEST.fullmatch(entry["test"]): + raise AllowListInvalid(f"{where}: test {entry['test']!r} is not a test's name") + if not _LANDING.fullmatch(entry["landing"]): + raise AllowListInvalid( + f"{where}: landing {entry['landing']!r} is not opensoft/openXdox-code#") + for key in ("before_blob", "after_blob"): + if not _BLOB.fullmatch(entry[key]): + raise AllowListInvalid(f"{where}: {key} is not a full object id") + old = _whole_lines(entry["old"], f"{where}: old") + new = _whole_lines(entry["new"], f"{where}: new") + if old == new: + raise AllowListInvalid(f"{where}: old and new are the same text") + suite = entry["suite"] + if suite in last_after and entry["before_blob"] != last_after[suite]: + raise AllowListInvalid( + f"{where}: its before_blob is not the after_blob of the entry before it " + f"for {suite}, so the two do not chain") + last_after[suite] = entry["after_blob"] + return entries + + +def _git(repo: Path, *args: str) -> str: + return subprocess.run(("git", "-C", str(repo), *args), check=True, + capture_output=True, text=True).stdout + + +def _blob(repo: Path, revision: str, path: str) -> str | None: + done = subprocess.run(("git", "-C", str(repo), "rev-parse", "--verify", "--quiet", + f"{revision}:{path}"), capture_output=True, text=True) + return done.stdout.strip() if done.returncode == 0 else None + + +def _blob_text(repo: Path, blob: str) -> str: + return subprocess.run(("git", "-C", str(repo), "cat-file", "blob", blob), + check=True, capture_output=True).stdout.decode("utf-8") + + +def _test_lines(text: str, test: str) -> tuple[int, int] | None: + """The 1-based line span of the module-level test function `test`, or None.""" + try: + tree = ast.parse(text) + except SyntaxError: + return None + found = [node for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name == test] + if len(found) != 1: + return None + node = found[0] + return min([node.lineno, *(d.lineno for d in node.decorator_list)]), node.end_lineno + + +def _inside_the_test(text: str, start: int, piece: str, test: str) -> bool: + span = _test_lines(text, test) + if span is None: + return False + first_line = text.count("\n", 0, start) + 1 + last_line = first_line + piece.count("\n") - 1 + return span[0] <= first_line and last_line <= span[1] + + +def entry_holds(repo: Path, landing: str, entry: dict) -> str | None: + """None when `entry` holds at `landing`, else why it does not.""" + suite = entry["suite"] + before = _blob(repo, f"{landing}^1", suite) + after = _blob(repo, landing, suite) + if before != entry["before_blob"]: + return f"{suite} before the landing is {before}, not the entry's {entry['before_blob']}" + if after != entry["after_blob"]: + return f"{suite} at the landing is {after}, not the entry's {entry['after_blob']}" + before_text, after_text = _blob_text(repo, before), _blob_text(repo, after) + old, new = entry["old"], entry["new"] + if before_text.count(old) != 1: + return f"the entry's old text occurs {before_text.count(old)} times before the landing, not once" + at = before_text.index(old) + if before_text.replace(old, new, 1) != after_text: + return "replacing the entry's old text with its new text does not give the suite at the landing" + if not _inside_the_test(before_text, at, old, entry["test"]): + return f"the entry's old text is not inside {entry['test']} before the landing" + if not _inside_the_test(after_text, at, new, entry["test"]): + return f"the entry's new text is not inside {entry['test']} at the landing" + return None + + +@dataclass +class Finding: + landing: str + path: str + admitted_by: int | None + why: str + + +def check(repo: Path, landings: list[str], protected: set[str], + entries: list[dict]) -> list[Finding]: + """One finding per protected path each landing touched: admitted by the + entry (1-based) that holds there, or refused with every entry's reason.""" + findings: list[Finding] = [] + for landing in landings: + touched = {line.strip() for line in + _git(repo, "diff", "--name-only", f"{landing}^1", landing).splitlines() + if line.strip()} + for path in sorted(touched & protected): + reasons = [] + admitted = None + for n, entry in enumerate(entries, 1): + if entry["suite"] != path: + continue + why = entry_holds(repo, landing, entry) + if why is None: + admitted = n + break + reasons.append(f"entry {n}: {why}") + findings.append(Finding( + landing, path, admitted, + "" if admitted else ("; ".join(reasons) or "no entry names this suite"))) + return findings + + +def _lines(path: str) -> list[str]: + return [line.strip() for line in Path(path).read_text(encoding="utf-8").splitlines() + if line.strip()] + + +def main(argv: list[str] | None = None) -> int: + argv = sys.argv[1:] if argv is None else argv + if len(argv) != 2: + print("usage: protected_suites.py ", + file=sys.stderr) + return 2 + repo = Path.cwd() + try: + entries = load_allow_list(repo / ALLOW_LIST) + except AllowListInvalid as exc: + print(f"FAIL: {ALLOW_LIST} breaks its own rules, so nothing is subtracted: {exc}", + file=sys.stderr) + return 2 + findings = check(repo, _lines(argv[0]), set(_lines(argv[1])), entries) + refused = [] + for f in findings: + if f.admitted_by is not None: + print(f"admitted: {f.landing[:12]} {f.path}, by entry {f.admitted_by} of {ALLOW_LIST}") + else: + print(f"refused: {f.landing[:12]} {f.path}: {f.why}") + refused.append(f.path) + if refused: + print("FAIL: the arc edited protected suites outside the reviewed allow-list: " + + ", ".join(sorted(set(refused))), file=sys.stderr) + return 1 + print(f"ok: {len(findings)} protected edit(s), each entered and holding") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/protected_suite_respellings.yaml b/tests/protected_suite_respellings.yaml index 226dc82..c93a645 100644 --- a/tests/protected_suite_respellings.yaml +++ b/tests/protected_suite_respellings.yaml @@ -33,10 +33,12 @@ # entry. Each later admitted edit appends its own entry in the pull request # that makes the edit. # -# WHO READS IT. F5.2 and 12.5's falsifier, once plan 034 T059 and T086 wire the -# subtraction into both checks (batch C). Until then nothing reads this file, -# and both falsifiers still refuse every protected path an arc landing touches, -# as written. +# WHO READS IT. `scripts/protected_suites.py`, since plan 034 T059 (batch C's +# wiring). It is the last step of F5.2 and of 12.5's falsifier, in place of the +# inline intersection each ended with: the falsifier still lists the landings +# and the protected suites itself, and the script subtracts the entries that +# hold. Its docstring gives both calls. Until T059 nothing read this file, and +# both falsifiers refused every protected path an arc landing touched. # # THE RULES. # * `schema_version` is the integer 1, and `kind` is @@ -62,7 +64,10 @@ # is `after_blob`; and replacing `old`, which occurs exactly once in the # `before_blob` text, with `new` gives the `after_blob` text byte for byte. # A landing whose diff for a protected path matches no entry is refused, -# exactly as an unentered edit is (batch C). +# exactly as an unentered edit is (batch C). The check T059 wired holds a +# fourth, from `test`: `old` lies inside the named test in the +# `before_blob` text, and `new` inside it in the `after_blob` text, so an +# entry cannot admit an edit to another test of the suite. # * Entries for one suite CHAIN: each one's `before_blob` is the previous # one's `after_blob`, because each edit is made to the suite the last one # left. diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py new file mode 100644 index 0000000..abafde4 --- /dev/null +++ b/tests/test_protected_suite_check.py @@ -0,0 +1,255 @@ +"""The allow-list subtraction F5.2 and 12.5's falsifier run +(`scripts/protected_suites.py`; plan 034 T059, T007's batch C). + +The check runs against a real history, so these cases build one: a scratch +repository whose commits play the arc's landings. A landing is a first-parent +commit carrying the `Arc:` line. Each case holds one rule of the check: + +* an arc landing that touches a protected suite with no entry is refused; +* one whose diff for that suite is exactly an entry's `old` to `new`, inside + the test the entry names, is admitted; +* an entry is refused as soon as its landing's diff differs from its recorded + text in any way: another edit beside it, a text that occurs twice, an edit in + another test, or blobs that are not the entry's; +* a commit without the `Arc:` line is not a landing, whatever it touches; +* entries for one suite chain, and a file that breaks its own rules refuses + the whole check rather than subtracting less. + +The last cases hold this repository's own allow-list to those rules. Which +landing each entry holds at is the falsifier's to show, at the head it runs +at: a pull request's checkout here has no history to walk. + +A CREATED file: no manifest row (RULED OQ-C). +""" + +from __future__ import annotations + +import os +import subprocess +import textwrap +from pathlib import Path + +import pytest +import yaml + +import protected_suites as ps + +REPO_ROOT = Path(__file__).resolve().parents[1] +ARC = "Arc: neutral-product-standalone-operability" +SUITE = "tests/test_governed.py" + +BEFORE = textwrap.dedent('''\ + def test_first() -> None: + assert 1 == 1 + + + def test_second() -> None: + """The second.""" + assert {"a": 1} == {"a": 1} +''') +OLD = ''' assert {"a": 1} == {"a": 1} +''' +NEW = ''' assert {"a": 1, "b": 2} == {"a": 1, "b": 2} +''' +AFTER = BEFORE.replace(OLD, NEW) + + +class Repo: + def __init__(self, root: Path) -> None: + self.root = root + self.env = {**os.environ, + "GIT_AUTHOR_NAME": "fixture", "GIT_AUTHOR_EMAIL": "fixture@example.invalid", + "GIT_COMMITTER_NAME": "fixture", "GIT_COMMITTER_EMAIL": "fixture@example.invalid"} + root.mkdir(parents=True) + self.git("init", "-q", "-b", "main") + + def git(self, *args: str) -> str: + return subprocess.run(("git", "-C", str(self.root), *args), check=True, + capture_output=True, text=True, env=self.env).stdout.strip() + + def commit(self, files: dict[str, str], message: str) -> str: + for rel, text in files.items(): + path = self.root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + self.git("add", "--", rel) + self.git("commit", "-q", "-m", message) + return self.git("rev-parse", "HEAD") + + def blob(self, text: str) -> str: + return subprocess.run(("git", "-C", str(self.root), "hash-object", "--stdin"), + input=text, check=True, capture_output=True, + text=True).stdout.strip() + + +@pytest.fixture +def repo(tmp_path: Path) -> Repo: + made = Repo(tmp_path / "repo") + made.commit({SUITE: BEFORE, "README.md": "base\n"}, "base") + return made + + +def _entry(repo: Repo, *, before: str = BEFORE, after: str = AFTER, old: str = OLD, + new: str = NEW, test: str = "test_second", **extra) -> dict: + entry = { + "suite": SUITE, "test": test, "landing": "opensoft/openXdox-code#1", + "edit": "admitted", "reason": "a reason", "ruled": "a ruling", + "review": "a review", "before_blob": repo.blob(before), + "after_blob": repo.blob(after), "old": old, "new": new, + } + entry.update(extra) + return entry + + +def _check(repo: Repo, entries: list[dict]) -> list[ps.Finding]: + landings = repo.git("log", "--first-parent", "--format=%H", f"--grep=^{ARC}$", + "HEAD").splitlines() + return ps.check(repo.root, landings, {SUITE}, entries) + + +# -------------------------------------------------------------------------- +# the check +# -------------------------------------------------------------------------- + +def test_an_unentered_edit_to_a_protected_suite_is_refused(repo) -> None: + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + [finding] = _check(repo, []) + assert finding.admitted_by is None and finding.path == SUITE + assert "no entry names this suite" in finding.why + + +def test_an_edit_that_is_exactly_its_entry_is_admitted(repo) -> None: + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo)]) + assert finding.admitted_by == 1 + + +def test_an_edit_beside_the_entered_one_is_refused(repo) -> None: + """The weakening batch C names: the entered text changes, and another + assertion is weakened in the same landing.""" + weakened = AFTER.replace("assert 1 == 1", "assert True") + repo.commit({SUITE: weakened}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo)]) + assert finding.admitted_by is None + assert "not the entry's" in finding.why + + +def test_an_entry_whose_blobs_match_but_whose_text_does_not_is_refused(repo) -> None: + """The blobs are the landing's, but the recorded text is not the edit.""" + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, new=NEW.replace('"b": 2', '"b": 3'))]) + assert finding.admitted_by is None + assert "does not give the suite at the landing" in finding.why + + +def test_an_old_text_that_occurs_twice_is_refused(repo) -> None: + twice = BEFORE + "\n\ndef test_third() -> None:\n" + OLD + repo.commit({SUITE: twice}, "the same assertion in a third test") + repo.commit({SUITE: twice.replace(OLD, NEW, 1)}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, before=twice, after=twice.replace(OLD, NEW, 1))]) + assert finding.admitted_by is None + assert "occurs 2 times" in finding.why + + +def test_an_edit_outside_the_named_test_is_refused(repo) -> None: + """The text is exact, but it lies in `test_second`, and the entry names + `test_first`.""" + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, test="test_first")]) + assert finding.admitted_by is None + assert "not inside test_first" in finding.why + + +def test_a_commit_without_the_trailer_is_not_a_landing(repo) -> None: + repo.commit({SUITE: AFTER}, "an edit that is no arc landing") + assert _check(repo, []) == [] + + +def test_a_landing_that_touches_no_protected_suite_is_not_reported(repo) -> None: + repo.commit({"README.md": "changed\n"}, f"docs\n\n{ARC}") + assert _check(repo, []) == [] + + +def test_two_landings_admitted_by_two_chained_entries(repo) -> None: + repo.commit({SUITE: AFTER}, f"first edit\n\n{ARC}") + third = AFTER.replace("assert 1 == 1", "assert 2 == 2") + repo.commit({SUITE: third}, f"second edit\n\n{ARC}") + first = _entry(repo) + second = _entry(repo, before=AFTER, after=third, test="test_first", + old=" assert 1 == 1\n", new=" assert 2 == 2\n") + findings = _check(repo, [first, second]) + assert sorted(f.admitted_by for f in findings) == [1, 2] + + +def test_the_command_exits_one_on_a_refusal_and_zero_when_every_edit_holds(repo, tmp_path, + monkeypatch) -> None: + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + landings = tmp_path / "x-arc.txt" + landings.write_text(repo.git("log", "--first-parent", "--format=%H", + f"--grep=^{ARC}$", "HEAD") + "\n", encoding="utf-8") + suites = tmp_path / "suites.txt" + suites.write_text(SUITE + "\n", encoding="utf-8") + allow = repo.root / ps.ALLOW_LIST + monkeypatch.chdir(repo.root) + allow.write_text(yaml.safe_dump({"schema_version": 1, "kind": ps.KIND, "entries": []}), + encoding="utf-8") + assert ps.main([str(landings), str(suites)]) == 1 + allow.write_text(yaml.safe_dump({"schema_version": 1, "kind": ps.KIND, + "entries": [_entry(repo)]}), encoding="utf-8") + assert ps.main([str(landings), str(suites)]) == 0 + allow.write_text("schema_version: 1\nschema_version: 1\n", encoding="utf-8") + assert ps.main([str(landings), str(suites)]) == 2 + + +# -------------------------------------------------------------------------- +# the allow-list's own rules +# -------------------------------------------------------------------------- + +def _write(tmp_path: Path, document: object) -> Path: + path = tmp_path / "allow.yaml" + path.write_text(document if isinstance(document, str) else yaml.safe_dump(document), + encoding="utf-8") + return path + + +def _valid_entry(**changes) -> dict: + entry = {"suite": SUITE, "test": "test_second", "landing": "opensoft/openXdox-code#7", + "edit": "admitted", "reason": "r", "ruled": "u", "review": "v", + "before_blob": "a" * 40, "after_blob": "b" * 40, + "old": "x\n", "new": "y\n"} + entry.update(changes) + return entry + + +@pytest.mark.parametrize("broken, why", [ + ({"schema_version": 2, "kind": ps.KIND, "entries": []}, "schema_version"), + ({"schema_version": True, "kind": ps.KIND, "entries": []}, "schema_version"), + ({"schema_version": 1, "kind": "other", "entries": []}, "kind"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [], "extra": 1}, "carries"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(edit="rewrite")]}, "edit is"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(respelled="x")]}, "carries"), + ({"schema_version": 1, "kind": ps.KIND, + "entries": [{k: v for k, v in _valid_entry().items() if k != "review"}]}, "carries"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(before_blob="abc")]}, "full object id"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(old="x")]}, "newline"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(new="x\n")] }, "same text"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(suite="src/x.py")]}, "suite"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(landing="#7")]}, "landing"), + ({"schema_version": 1, "kind": ps.KIND, + "entries": [_valid_entry(), _valid_entry(before_blob="c" * 40)]}, "do not chain"), + ("schema_version: 1\nschema_version: 1\n", "given twice"), +], ids=lambda value: value if isinstance(value, str) else None) +def test_a_list_that_breaks_its_own_rules_subtracts_nothing(tmp_path, broken, why) -> None: + with pytest.raises(ps.AllowListInvalid, match=why): + ps.load_allow_list(_write(tmp_path, broken)) + + +def test_a_respelling_names_what_it_respelled(tmp_path) -> None: + entry = _valid_entry(edit="respelling", respelled="a -> b") + del entry["reason"] + assert ps.load_allow_list(_write(tmp_path, { + "schema_version": 1, "kind": ps.KIND, "entries": [entry]})) == [entry] + + +def test_this_repositorys_allow_list_keeps_its_rules() -> None: + assert ps.load_allow_list(REPO_ROOT / ps.ALLOW_LIST), "the allow-list enters no edit" From 81a01cd0807b21df62d95ef154541afa2472542d Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:47:48 +0000 Subject: [PATCH 06/25] The overlay test expects the values block's six leaves: batch I's second admitted edit (plan 034 T059) At this pin openDox's SNAPSHOT_VALUES defaults are its neutral snapshot's values (T054), so openXdox's DISPLAY facet's values block (T060) changes six values.* leaves of the served display beside the four stage words and the named absence. test_the_overlay_changes_four_words_and_the_named_absence_and_nothing_else now expects eleven changed leaves, and no other assertion of the suite changes (RULED R1Q26 (a), openxFactory#656 comment 5851950767, on R1Q11 (a), comment 5850003126; batch I). The edit is entered in tests/protected_suite_respellings.yaml with its reason, as an admitted edit, in this pull request. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_gate_loop_views.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_gate_loop_views.py b/tests/test_gate_loop_views.py index a56906c..a5ce00c 100644 --- a/tests/test_gate_loop_views.py +++ b/tests/test_gate_loop_views.py @@ -1732,11 +1732,19 @@ def test_every_other_stage_still_renders_the_neutral_word(register_host) -> None def test_the_overlay_changes_four_words_and_the_named_absence_and_nothing_else( register_host) -> None: - """Against the same host WITHOUT the facet: five leaves differ, and they are these. + """Against the same host WITHOUT the facet: eleven leaves differ, and they are these. Both payloads come through the same chain. The facet-less host is registered second, after an explicit `unregister()`, because the registry refuses a second, different profile over a first. + + SIX OF THEM ARE THE `values` BLOCK'S, which is not a stage (plan 034 T059, + RULED R1Q26 (a), `opensoft/openxFactory#656` comment `5851950767`, on R1Q11 + (a), comment `5850003126`). At this pin openDox's `SNAPSHOT_VALUES` + defaults are its neutral snapshot's values (T054), so the facet's block + changes the six values a view matches the governed snapshot by. The edit + that admitted them is entered, with its reason, in + `tests/protected_suite_respellings.yaml`. """ from opendox import domain_profile as registry @@ -1756,6 +1764,12 @@ def test_the_overlay_changes_four_words_and_the_named_absence_and_nothing_else( "stages.completion.many": ("completed items", IMPLEMENTED_ITEMS), "stages.completion.short": ("completed", IMPLEMENTED), "stages.completion.label": ("completed", IMPLEMENTED), + "values.document_stage.captured": ("source", "brainstorm"), + "values.document_stage.organized": ("grouping", "staged"), + "values.register_state.captured": ("unselected", "latent"), + "values.register_state.proposed": ("selected", "picked"), + "values.register_state.retired": ("declined", "rejected"), + "values.register_state.superseded": ("replaced", "superseded"), } From 614f79f05c7d9570689bf4d3c07d9d6bfec6c8f6 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:47:48 +0000 Subject: [PATCH 07/25] Respell the session-source confinement test's reference to the seam T055 moved (plan 034 T059, R1Q7 (a)) openDox-code#59 (T055) routes /source through resolve_source_path over the resolved entry's own root (r4136863569), where the route used to call self.source.registry.resolve_source. The test's expected call is respelled to the route's one entry point, and its second assertion, that the registry's resolve_source reaches resolve_within exactly once, is unchanged (holder's ruling on T059). The respelling is entered in tests/protected_suite_respellings.yaml in this pull request. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_session_snapshot.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_session_snapshot.py b/tests/test_session_snapshot.py index b5e1be0..f78c445 100644 --- a/tests/test_session_snapshot.py +++ b/tests/test_session_snapshot.py @@ -445,13 +445,15 @@ def test_the_keyed_source_returns_the_worktree_bytes_and_never_falls_back( def test_the_session_source_read_is_the_existing_confinement_mechanism(): - """T035: the confinement is `registry.resolve_source` + `resolve_within`, not - a new check bolted onto the session path. Pinned on the route's own source so - a later "simplification" cannot re-implement containment beside it.""" + """T035: the confinement is the registry's per-entry `resolve_within`, reached + through the route's one entry point `resolve_source_path` over the resolved + entry's own root (openDox-code#59, r4136863569), not a new check bolted onto + the session path. Pinned on the route's own source so a later + "simplification" cannot re-implement containment beside it.""" import inspect source = inspect.getsource(serve_mod.DashboardHandler._serve_source) - assert "self.source.registry.resolve_source(" in source + assert "resolve_source_path(Path(root), rest)" in source assert inspect.getsource(reg.SnapshotRegistry.resolve_source).count( "resolve_within(") == 1 From f0f7f41c396d0c38746e5c6bf0fa9577f6c48950 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:51:44 +0000 Subject: [PATCH 08/25] Re-pin openDox-code 814516b7, openDox-code#59's head after T054 landed (plan 034 T059) openDox-code#59 merged main a691e4e4 (T054, openDox-code#57) after this branch pinned e3ef506a, so the pin follows it to 814516b7, and the comments that name the pin follow it too. The pin is still a draft's: it re-points to T062's commit before T059 lands. Measured at 814516b7: ViewBinding has styles and exports; web/, view_extension.py and tests/test_binding_stylesheets.py are unchanged from e3ef506a; tests/test_consumer_reach.py is unchanged, so NEUTRAL_MODULES still holds thirteen; the census returns the same three rows; and 2d116415..814516b7 is twenty-five first-parent commits. The whole suite reads the same at both heads (967 passed, 4 skipped, 1 deselected). Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 2 +- pyproject.toml | 2 +- src/openxdox/view_extensions.py | 8 ++++---- tests/integration/test_assembled_bundle.py | 2 +- tests/test_dependency_direction.py | 6 +++--- tests/test_gate_loop_probes.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d59134a..14b998e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -132,7 +132,7 @@ jobs: # imported carved session fixtures that reached `ideation_dashboard` and # `doc_health`, two packages neither leg carries. At the openDox this # leg pinned from plan 034 T040 (openDox-code `2d116415`), and at the one - # it pins since plan 034 T059 (`e3ef506a`), the chain loads in a lone + # it pins since plan 034 T059 (`814516b7`), the chain loads in a lone # checkout, and what still reaches openxFactory is what the declaration # and `LEFT_OUT` hold. Each of the sixteen files' # account of itself (why it joined the list, its counts, its review diff --git a/pyproject.toml b/pyproject.toml index 26cbb32..14d480d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ requires-python = ">=3.12" # PR) — and not an arbitrary choice: `Draft202012Validator`, the name # `gate_console.py:563` imports, was added in jsonschema 4.18.0. dependencies = [ - "opendox @ git+https://github.com/opensoft/openDox-code@e3ef506a8036c1360d88ff75cab50f8b8e5d5fca", + "opendox @ git+https://github.com/opensoft/openDox-code@814516b778b04d4d5022e486c657774b61c86f15", "PyYAML>=6.0", "jsonschema>=4.18", ] diff --git a/src/openxdox/view_extensions.py b/src/openxdox/view_extensions.py index e3b2c9b..3c1b64b 100644 --- a/src/openxdox/view_extensions.py +++ b/src/openxdox/view_extensions.py @@ -25,7 +25,7 @@ `exports` field RULED Q2 adds is newer still". THAT IS NO LONGER TRUE, and the old wording is quoted here as provenance rather than deleted: the pin named openDox-code#55 (`2d116415`, plan 034 T037's landing) from plan 034 T040, and -names openDox-code `e3ef506a` (the head of openDox-code#59, T055) since plan 034 +names openDox-code `814516b7` (the head of openDox-code#59, T055) since plan 034 T059; at both `view_extension` is importable and `ViewBinding` takes `exports` — measured, and the three materialization assertions in `tests/test_gate_loop_views.py` run and pass against it instead of skipping. @@ -33,14 +33,14 @@ which is where that wording was corrected; the pin then crossed `0e65b5f8` (#24) to `5c137a90` (openDox-code#27, § 3.4 RULED Q7), whose `ViewBinding` first carried a `styles` field — absent at `0b4e8bbf`, present at `5c137a90` -and still at `2d116415` and at `e3ef506a`, measured by `dataclasses.fields()` +and still at `2d116415` and at `814516b7`, measured by `dataclasses.fields()` in a venv at each pin. THE `5c137a90` BUMP ITSELF READ NOTHING, and the review of `ea6991b` was right to check that: it materialized `VIEW_BINDING_SPECS` unchanged and asked nothing about the installed `ViewBinding`. THE READING IS THIS ACT'S, and this act is the pull request that bump named as waiting on it: `specs_for()` below reads `dataclasses.fields(binding_cls)` and drops `styles` where the installed dataclass has no such field. MEASURED IN A VENV AT THAT PIN, and again at -`2d116415` and at `e3ef506a`: it has one, so nothing is dropped, every binding that owns +`2d116415` and at `814516b7`: it has one, so nothing is dropped, every binding that owns selectors declares its sheet, and the four contributed stylesheets are LIVE rather than inert — which is the one thing they waited on that bump for. @@ -519,7 +519,7 @@ def specs_for(binding_cls: Any) -> tuple[dict[str, Any], ...]: So the field is DROPPED where the installed class does not take it and the column mounts unstyled. AT THE PIN THIS LEG DECLARES TODAY THE DETECTION IS THE PLAIN PATH, not a fallback: `dataclasses.fields()` finds `styles` on - `e3ef506a`'s `ViewBinding`, as on `2d116415`'s and on `5c137a90`'s where the field first + `814516b7`'s `ViewBinding`, as on `2d116415`'s and on `5c137a90`'s where the field first reached the pin, every spec crosses whole, and the four contributed sheets are LIVE — same code, same behaviour, one branch not taken. This paragraph read "the pin bump that follows openDox-code's Q7 leg turns the sheets on with no edit here"; that bump landed, and that is what diff --git a/tests/integration/test_assembled_bundle.py b/tests/integration/test_assembled_bundle.py index 158b181..703029b 100644 --- a/tests/integration/test_assembled_bundle.py +++ b/tests/integration/test_assembled_bundle.py @@ -31,7 +31,7 @@ `_ST_DECLARATION` and `_declared_st_tokens`) and the `GATE_EXCLUSIVE` tuple are openDox-code's text at `55194335`, the last commit that carried all five, byte for byte. The tuple and the three helpers openDox-code kept are unchanged at -`2d116415` and at `e3ef506a`. Their comments are kept too, so "Copilot review, round N" in them +`2d116415` and at `814516b7`. Their comments are kept too, so "Copilot review, round N" in them is a round on openDox-code#27, where that file was written. Four things changed, each because this is the composition and not a lone leg: 1. A missing assembly FAILS here, where it skipped there. The composition is diff --git a/tests/test_dependency_direction.py b/tests/test_dependency_direction.py index 52c253b..49de0b4 100644 --- a/tests/test_dependency_direction.py +++ b/tests/test_dependency_direction.py @@ -368,9 +368,9 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: carried-forward memory. #: #: PLAN 034 T059 LOWERED IT, the first fall since slice 2b. The pin moved from -#: `2d116415` to openDox-code `e3ef506a`, the head of openDox-code#59 (T055, +#: `2d116415` to openDox-code `814516b7`, the head of openDox-code#59 (T055, #: openDox's own snapshot registry and source, corpus-root predicate, writer -#: and validator lookup behind seams of their own), twenty-three first-parent +#: and validator lookup behind seams of their own), twenty-five first-parent #: commits later. At that tree `_back_import_census()` returns three rows: #: `cli.py` (0, 1) and `serve.py` (0, 2) reach (0, 0) and leave the table, and #: `branch_session.py` falls from (0, 7) to (0, 2). The eight reaches T055 @@ -420,7 +420,7 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: `NEUTRAL_MODULES` is the asserted half. At `2d116415` it held nine: #: `workbench`, `serve_workbench`, `consumer_reach`, `branch_session`, #: `domain_profile`, `profile_proxy`, `view_extension`, `cli` and `serve`. At -#: `e3ef506a` it holds thirteen, T055's four new modules with them: +#: `814516b7` it holds thirteen, T055's four new modules with them: #: `projection_seams`, `default_registry`, `default_projection` and `rfc3339`. #: Its `STILL_REACHING` is empty at both. #: diff --git a/tests/test_gate_loop_probes.py b/tests/test_gate_loop_probes.py index 7355bc0..a908799 100644 --- a/tests/test_gate_loop_probes.py +++ b/tests/test_gate_loop_probes.py @@ -194,7 +194,7 @@ def bundle(tmp_path) -> Path: # Its four-row table is there, not restated here. Round 1 of the review on # #21 found this file claiming the install "came from somewhere older than # the declared pin" on a check that only tested for a marker. UNDER THAT - # CHECK, had the DECLARED leg (`0b4e8bbf` then, `e3ef506a` now) itself ever + # CHECK, had the DECLARED leg (`0b4e8bbf` then, `814516b7` now) itself ever # stopped shipping `web/**`, all thirteen # probes below would have skipped and this required check would have stayed # green over the regression. UNDER THE TABLE THEY OBEY NOW THEY FAIL: the From 619684c9be56661e497e006ed0618d63d2e8268b Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:54:15 +0000 Subject: [PATCH 09/25] Enter T059's two protected edits in the reviewed allow-list (plan 034 T059) Entry 2 is batch I's second admitted edit, to the overlay test in tests/test_gate_loop_views.py (R1Q26 (a)); it chains on entry 1, T060's, from a56906c6 to a5ce00cc. Entry 3 is the respelling in tests/test_session_snapshot.py of the route's call into the confinement, as T055 moved it (R1Q7 (a), r4136863569), from b5e1be02 to f78c4452. Both name opensoft/openXdox-code#35, this pull request, since the landing's commit is not known inside it (T019's rule). scripts/protected_suites.py admits each at its commit on this branch, and entry 1 at T060's landing. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/protected_suite_respellings.yaml | 141 +++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/tests/protected_suite_respellings.yaml b/tests/protected_suite_respellings.yaml index c93a645..97d6ad9 100644 --- a/tests/protected_suite_respellings.yaml +++ b/tests/protected_suite_respellings.yaml @@ -138,3 +138,144 @@ entries: "retired": "rejected", "superseded": "superseded"}}, } + - suite: tests/test_gate_loop_views.py + test: test_the_overlay_changes_four_words_and_the_named_absence_and_nothing_else + landing: opensoft/openXdox-code#35 + edit: admitted + reason: >- + 5.3a's `values` block at a pin past plan 034 T054 (plan 034 T059). At + that pin openDox's `SNAPSHOT_VALUES` defaults are its neutral snapshot's + values, so the DISPLAY facet's `values` block (T060) changes six + `values.*` leaves of the served display beside the four stage words and + the named absence. The test that pins what the overlay changes now + expects those six leaves too, and its docstring says why. No other + assertion of the suite changes. This is the second of the two edits + batch I admits; T060 made the first. + ruled: >- + R1Q26 (a), opensoft/openxFactory#656 comment 5851950767 (kept at + 5852513402), on R1Q11 (a), comment 5850003126. It is recorded in #1144's + 12.5 falsifier by T007's batch I (openxFactory#1180, landed as 8421603a). + review: >- + Reviewed in the landing pull request on batch C's basis. Its diff for + this suite is exactly `old` to `new`, inside the one test named. The + test still compares the whole set of changed leaves by equality, so a + leaf the overlay changed and the test did not name would still fail it; + the six added leaves are each the governed value over openDox's neutral + default, measured at the pin (T060's body lists the same six). No + assertion is weakened. + before_blob: a56906c6c56b1c2db5e45aee5e93e6f07740484a + after_blob: a5ce00cc7cafb54928e64d9f7a7db34a7d252c39 + old: | + def test_the_overlay_changes_four_words_and_the_named_absence_and_nothing_else( + register_host) -> None: + """Against the same host WITHOUT the facet: five leaves differ, and they are these. + + Both payloads come through the same chain. The facet-less host is registered + second, after an explicit `unregister()`, because the registry refuses a + second, different profile over a first. + """ + from opendox import domain_profile as registry + + display_profile, view_extension = _display_profile_or_skip() + register_host(_engineering_host("DISPLAY")) + declared = _flatten(_served_display(display_profile, view_extension)) + registry.unregister() + register_host(_engineering_host()) + absent = _flatten(_served_display(display_profile, view_extension)) + assert absent["host_facet"] == "absent" + assert declared.keys() == absent.keys() + changed = {path: (absent[path], declared[path]) + for path in declared if declared[path] != absent[path]} + assert changed == { + "host_facet": ("absent", "declared"), + "stages.completion.one": ("completed item", IMPLEMENTED_ITEM), + "stages.completion.many": ("completed items", IMPLEMENTED_ITEMS), + "stages.completion.short": ("completed", IMPLEMENTED), + "stages.completion.label": ("completed", IMPLEMENTED), + new: | + def test_the_overlay_changes_four_words_and_the_named_absence_and_nothing_else( + register_host) -> None: + """Against the same host WITHOUT the facet: eleven leaves differ, and they are these. + + Both payloads come through the same chain. The facet-less host is registered + second, after an explicit `unregister()`, because the registry refuses a + second, different profile over a first. + + SIX OF THEM ARE THE `values` BLOCK'S, which is not a stage (plan 034 T059, + RULED R1Q26 (a), `opensoft/openxFactory#656` comment `5851950767`, on R1Q11 + (a), comment `5850003126`). At this pin openDox's `SNAPSHOT_VALUES` + defaults are its neutral snapshot's values (T054), so the facet's block + changes the six values a view matches the governed snapshot by. The edit + that admitted them is entered, with its reason, in + `tests/protected_suite_respellings.yaml`. + """ + from opendox import domain_profile as registry + + display_profile, view_extension = _display_profile_or_skip() + register_host(_engineering_host("DISPLAY")) + declared = _flatten(_served_display(display_profile, view_extension)) + registry.unregister() + register_host(_engineering_host()) + absent = _flatten(_served_display(display_profile, view_extension)) + assert absent["host_facet"] == "absent" + assert declared.keys() == absent.keys() + changed = {path: (absent[path], declared[path]) + for path in declared if declared[path] != absent[path]} + assert changed == { + "host_facet": ("absent", "declared"), + "stages.completion.one": ("completed item", IMPLEMENTED_ITEM), + "stages.completion.many": ("completed items", IMPLEMENTED_ITEMS), + "stages.completion.short": ("completed", IMPLEMENTED), + "stages.completion.label": ("completed", IMPLEMENTED), + "values.document_stage.captured": ("source", "brainstorm"), + "values.document_stage.organized": ("grouping", "staged"), + "values.register_state.captured": ("unselected", "latent"), + "values.register_state.proposed": ("selected", "picked"), + "values.register_state.retired": ("declined", "rejected"), + "values.register_state.superseded": ("replaced", "superseded"), + - suite: tests/test_session_snapshot.py + test: test_the_session_source_read_is_the_existing_confinement_mechanism + landing: opensoft/openXdox-code#35 + edit: respelling + respelled: >- + The route's call into the confinement, as openDox-code#59 (T055) moved it: + `self.source.registry.resolve_source(` to `resolve_source_path(Path(root), + rest)`, the route's one entry point, which applies the registered + registry's `resolve_within` to the resolved entry's own root + (r4136863569). The docstring names the same move. + ruled: >- + R1Q7 (a), opensoft/openxFactory#656 comment 5817152735, recorded in + #1144's F5.2 and 12.5 falsifiers by T007's batch C. The holder's ruling + on T059 (2026-09-30) accepted this entry as a respelling, citing T055's + r4136863569, with the expected call respelled and the second assertion + left unchanged. + review: >- + Reviewed in the landing pull request on batch C's basis. Its diff for + this suite is exactly `old` to `new`, inside the one test named. The + first assertion still pins the route's own source to the one call that + confines the path, so a re-implementation of containment beside it + still fails; the second assertion, that the registry's `resolve_source` + reaches `resolve_within` exactly once, is unchanged. No assertion is + weakened. + before_blob: b5e1be021b0a23c891ffa0694682dca331cb982c + after_blob: f78c4452aebb9d2601f9043ceb8d9c552040e9b6 + old: | + def test_the_session_source_read_is_the_existing_confinement_mechanism(): + """T035: the confinement is `registry.resolve_source` + `resolve_within`, not + a new check bolted onto the session path. Pinned on the route's own source so + a later "simplification" cannot re-implement containment beside it.""" + import inspect + + source = inspect.getsource(serve_mod.DashboardHandler._serve_source) + assert "self.source.registry.resolve_source(" in source + new: | + def test_the_session_source_read_is_the_existing_confinement_mechanism(): + """T035: the confinement is the registry's per-entry `resolve_within`, reached + through the route's one entry point `resolve_source_path` over the resolved + entry's own root (openDox-code#59, r4136863569), not a new check bolted onto + the session path. Pinned on the route's own source so a later + "simplification" cannot re-implement containment beside it.""" + import inspect + + source = inspect.getsource(serve_mod.DashboardHandler._serve_source) + assert "resolve_source_path(Path(root), rest)" in source From 6578e47117468ec6ddd2f42e7cce05a9413f2d03 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:01:56 +0000 Subject: [PATCH 10/25] The arc check takes its lists as values, not paths, and answers SonarCloud's analysis of #35 (plan 034 T059) SonarCloud's quality gate failed #35 on Security Rating C, from one finding: pythonsecurity:S8707, path injection through a CLI argument, at protected_suites._lines, which opened whatever file argv named. The check now opens no file a caller names. --landings and --suites each carry the list itself, one item per line, and each item is held to its shape before any of it reaches git: a landing is a full commit id, a suite is tests/test_.py in ASCII. An item of neither shape is refused with exit 2. The falsifier's call becomes --landings="$(cat "$W/x-arc.txt")" --suites="$(cat "$W/gen-suites.txt")". The one file read is still the allow-list, at its fixed path. The same analysis flagged code smells, which are taken too: - load_allow_list and check are split into named rule checks, which brings each under the cognitive-complexity limit; - the character classes use \w and \d under re.ASCII; - composite assertions are split in the three new test files; - each pytest.raises block holds one call. tests/test_protected_suite_check.py gains the input-shape cases: a revision, an option, a path outside tests/, and a non-ASCII name. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/protected_suites.py | 173 ++++++++++++++------- tests/test_governed_registry_and_writer.py | 17 +- tests/test_projection_contributions.py | 13 +- tests/test_protected_suite_check.py | 51 ++++-- 4 files changed, 173 insertions(+), 81 deletions(-) diff --git a/scripts/protected_suites.py b/scripts/protected_suites.py index f02e188..3671f0e 100644 --- a/scripts/protected_suites.py +++ b/scripts/protected_suites.py @@ -19,13 +19,19 @@ the falsifier's own block, as #1144 writes them. The last step, the inline Python that intersected them, becomes this call, from the checkout's root: - python3 scripts/protected_suites.py "$W/x-arc.txt" "$W/gen-suites.txt" # F5.2 - python3 scripts/protected_suites.py "$W/x-arc.txt" "$W/governed.txt" # 12.5 - -where `x-arc.txt` holds the landings, one commit per line, and the second file -the protected suites, one path per line. It exits 0 when no landing touched a -protected suite outside an entry that holds, 1 when one did (naming each -landing and path), and 2 when the allow-list itself breaks its rules. + python3 scripts/protected_suites.py --landings="$(cat "$W/x-arc.txt")" \ + --suites="$(cat "$W/gen-suites.txt")" # F5.2 + python3 scripts/protected_suites.py --landings="$(cat "$W/x-arc.txt")" \ + --suites="$(cat "$W/governed.txt")" # 12.5 + +Each option carries its LIST, one item per line, and never a path to one: the +landings, each a full commit id as `git log --format=%H` prints it, and the +protected suites, each `tests/test_.py`. (The `=` keeps a value that +begins with a dash a value.) So the check opens no file a caller names, and it +refuses an item of neither shape (exit 2) rather than handing it to git. The one file it reads is the allow-list, at its fixed path +under the checkout it runs in. It exits 0 when no landing touched a protected +suite outside an entry that holds, 1 when one did (naming each landing and +path), and 2 when its input or the allow-list itself breaks its rules. WHEN AN ENTRY HOLDS (the file's own header states the rule, and T060 wrote it). For a landing L that touches a protected `suite`, an entry for that suite holds @@ -59,6 +65,7 @@ from __future__ import annotations +import argparse import ast import re import subprocess @@ -79,10 +86,13 @@ EDIT_KEYS = {"respelling": frozenset({"respelled"}), "admitted": frozenset({"reason"})} +#: A full object id: a blob in an entry, or a landing's commit in the input. _BLOB = re.compile(r"[0-9a-f]{40}") -_SUITE = re.compile(r"tests/test_[A-Za-z0-9_]+\.py") -_LANDING = re.compile(r"opensoft/openXdox-code#[1-9][0-9]*") -_TEST = re.compile(r"test_[A-Za-z0-9_]+") +_COMMIT = _BLOB +#: ASCII only: a suite, a test or a landing is never spelled outside it. +_SUITE = re.compile(r"tests/test_\w+\.py", re.ASCII) +_LANDING = re.compile(r"opensoft/openXdox-code#[1-9]\d*", re.ASCII) +_TEST = re.compile(r"test_\w+", re.ASCII) class AllowListInvalid(ValueError): @@ -121,8 +131,8 @@ def _whole_lines(value: Any, where: str) -> str: return text -def load_allow_list(path: Path) -> list[dict]: - """The allow-list's entries, in landing order, once every rule holds.""" +def _document(path: Path) -> dict: + """The allow-list as a mapping, read with no key given twice.""" try: raw = yaml.load(path.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader) except OSError as exc: @@ -139,38 +149,58 @@ def load_allow_list(path: Path) -> list[dict]: raise AllowListInvalid(f"schema_version is {version!r}, not {SCHEMA_VERSION}") if raw["kind"] != KIND: raise AllowListInvalid(f"kind is {raw['kind']!r}, not {KIND!r}") - entries = raw["entries"] - if not isinstance(entries, list): + if not isinstance(raw["entries"], list): raise AllowListInvalid("entries is not a list") + return raw + + +def _check_keys(entry: Any, where: str) -> None: + """An entry is a mapping carrying exactly its kind's keys, each a text.""" + if not isinstance(entry, dict): + raise AllowListInvalid(f"{where} is not a mapping") + edit = entry.get("edit") + if edit not in EDIT_KEYS: + raise AllowListInvalid(f"{where}: edit is {edit!r}, not one of {sorted(EDIT_KEYS)}") + wanted = COMMON_KEYS | EDIT_KEYS[edit] + if set(entry) != wanted: + raise AllowListInvalid( + f"{where}: carries {sorted(map(str, entry))}, and a {edit} entry " + f"carries exactly {sorted(wanted)}") + for key in wanted - {"old", "new"}: + _text(entry[key], f"{where}: {key}") + + +def _check_spellings(entry: dict, where: str) -> None: + """The suite, test, landing and blobs are each spelled as the rules say.""" + if not _SUITE.fullmatch(entry["suite"]): + raise AllowListInvalid(f"{where}: suite {entry['suite']!r} is not tests/test_.py") + if not _TEST.fullmatch(entry["test"]): + raise AllowListInvalid(f"{where}: test {entry['test']!r} is not a test's name") + if not _LANDING.fullmatch(entry["landing"]): + raise AllowListInvalid( + f"{where}: landing {entry['landing']!r} is not opensoft/openXdox-code#") + for key in ("before_blob", "after_blob"): + if not _BLOB.fullmatch(entry[key]): + raise AllowListInvalid(f"{where}: {key} is not a full object id") + + +def _check_texts(entry: dict, where: str) -> None: + """`old` and `new` are whole lines, and differ.""" + old = _whole_lines(entry["old"], f"{where}: old") + new = _whole_lines(entry["new"], f"{where}: new") + if old == new: + raise AllowListInvalid(f"{where}: old and new are the same text") + + +def load_allow_list(path: Path) -> list[dict]: + """The allow-list's entries, in landing order, once every rule holds.""" + entries = _document(path)["entries"] last_after: dict[str, str] = {} for n, entry in enumerate(entries, 1): where = f"entry {n}" - if not isinstance(entry, dict): - raise AllowListInvalid(f"{where} is not a mapping") - edit = entry.get("edit") - if edit not in EDIT_KEYS: - raise AllowListInvalid(f"{where}: edit is {edit!r}, not one of {sorted(EDIT_KEYS)}") - wanted = COMMON_KEYS | EDIT_KEYS[edit] - if set(entry) != wanted: - raise AllowListInvalid( - f"{where}: carries {sorted(map(str, entry))}, and a {edit} entry " - f"carries exactly {sorted(wanted)}") - for key in wanted - {"old", "new"}: - _text(entry[key], f"{where}: {key}") - if not _SUITE.fullmatch(entry["suite"]): - raise AllowListInvalid(f"{where}: suite {entry['suite']!r} is not tests/test_.py") - if not _TEST.fullmatch(entry["test"]): - raise AllowListInvalid(f"{where}: test {entry['test']!r} is not a test's name") - if not _LANDING.fullmatch(entry["landing"]): - raise AllowListInvalid( - f"{where}: landing {entry['landing']!r} is not opensoft/openXdox-code#") - for key in ("before_blob", "after_blob"): - if not _BLOB.fullmatch(entry[key]): - raise AllowListInvalid(f"{where}: {key} is not a full object id") - old = _whole_lines(entry["old"], f"{where}: old") - new = _whole_lines(entry["new"], f"{where}: new") - if old == new: - raise AllowListInvalid(f"{where}: old and new are the same text") + _check_keys(entry, where) + _check_spellings(entry, where) + _check_texts(entry, where) suite = entry["suite"] if suite in last_after and entry["before_blob"] != last_after[suite]: raise AllowListInvalid( @@ -251,6 +281,21 @@ class Finding: why: str +def _admitting(repo: Path, landing: str, path: str, + entries: list[dict]) -> tuple[int | None, list[str]]: + """The entry (1-based) for `path` that holds at `landing`, or None and + every entry's reason for not holding.""" + reasons = [] + for n, entry in enumerate(entries, 1): + if entry["suite"] != path: + continue + why = entry_holds(repo, landing, entry) + if why is None: + return n, [] + reasons.append(f"entry {n}: {why}") + return None, reasons + + def check(repo: Path, landings: list[str], protected: set[str], entries: list[dict]) -> list[Finding]: """One finding per protected path each landing touched: admitted by the @@ -261,41 +306,51 @@ def check(repo: Path, landings: list[str], protected: set[str], _git(repo, "diff", "--name-only", f"{landing}^1", landing).splitlines() if line.strip()} for path in sorted(touched & protected): - reasons = [] - admitted = None - for n, entry in enumerate(entries, 1): - if entry["suite"] != path: - continue - why = entry_holds(repo, landing, entry) - if why is None: - admitted = n - break - reasons.append(f"entry {n}: {why}") + admitted, reasons = _admitting(repo, landing, path, entries) findings.append(Finding( landing, path, admitted, "" if admitted else ("; ".join(reasons) or "no entry names this suite"))) return findings -def _lines(path: str) -> list[str]: - return [line.strip() for line in Path(path).read_text(encoding="utf-8").splitlines() - if line.strip()] +class InputInvalid(ValueError): + """An item of a list the caller passed is neither a landing nor a suite.""" + + +def _items(text: str, shape: re.Pattern[str], what: str) -> list[str]: + """The non-empty lines of `text`, each of `shape`, in order.""" + items = [line.strip() for line in text.splitlines() if line.strip()] + for item in items: + if not shape.fullmatch(item): + raise InputInvalid(f"{item!r} is not {what}") + return items def main(argv: list[str] | None = None) -> int: - argv = sys.argv[1:] if argv is None else argv - if len(argv) != 2: - print("usage: protected_suites.py ", - file=sys.stderr) - return 2 + parser = argparse.ArgumentParser( + description="The unedited-by-the-arc check, with the reviewed allow-list subtracted.") + parser.add_argument("--landings", required=True, + help="the arc's landings, one full commit id per line") + parser.add_argument("--suites", required=True, + help="the protected suites, one tests/test_.py per line") + try: + args = parser.parse_args(sys.argv[1:] if argv is None else argv) + except SystemExit as exc: + return 2 if exc.code else 0 repo = Path.cwd() + try: + landings = _items(args.landings, _COMMIT, "a full commit id") + suites = set(_items(args.suites, _SUITE, "a tests/test_.py path")) + except InputInvalid as exc: + print(f"FAIL: {exc}, so nothing is checked", file=sys.stderr) + return 2 try: entries = load_allow_list(repo / ALLOW_LIST) except AllowListInvalid as exc: print(f"FAIL: {ALLOW_LIST} breaks its own rules, so nothing is subtracted: {exc}", file=sys.stderr) return 2 - findings = check(repo, _lines(argv[0]), set(_lines(argv[1])), entries) + findings = check(repo, landings, suites, entries) refused = [] for f in findings: if f.admitted_by is not None: diff --git a/tests/test_governed_registry_and_writer.py b/tests/test_governed_registry_and_writer.py index b358276..9ec9a4a 100644 --- a/tests/test_governed_registry_and_writer.py +++ b/tests/test_governed_registry_and_writer.py @@ -99,7 +99,8 @@ def test_after_the_active_entry_is_dropped_the_next_one_becomes_active() -> None registry.register(_entry("alpha")) registry.drop("alpha") registry.register(_entry("beta")) - assert registry.active is not None and registry.active.repository == "beta" + assert registry.active is not None + assert registry.active.repository == "beta" def test_dropping_another_entry_keeps_the_active_one() -> None: @@ -122,9 +123,9 @@ def _boundary(root: Path, name: str = "snapshot.json") -> OutputBoundary: ids=["nan", "infinity", "minus-infinity"]) def test_a_value_json_cannot_carry_is_refused_and_nothing_is_written(tmp_path, value) -> None: target = tmp_path / "snapshot.json" + boundary = _boundary(tmp_path) with pytest.raises(snapshot_mod.SnapshotNotWritable): - snapshot_mod.write_snapshot({"kind": "k", "score": value}, target, - _boundary(tmp_path)) + snapshot_mod.write_snapshot({"kind": "k", "score": value}, target, boundary) assert list(tmp_path.iterdir()) == [] @@ -156,7 +157,8 @@ def replace(source, destination): written = snapshot_mod.write_snapshot({"kind": "k"}, target, _boundary(tmp_path)) assert written == target.resolve() assert json.loads(target.read_text(encoding="utf-8")) == {"kind": "k"} - assert len(moves) == 1 and moves[0].startswith(".snapshot.json.") + assert len(moves) == 1 + assert moves[0].startswith(".snapshot.json.") assert sorted(p.name for p in tmp_path.iterdir()) == ["snapshot.json"] @@ -168,8 +170,9 @@ def refuse(source, destination): raise OSError("the move failed") monkeypatch.setattr(os, "replace", refuse) + boundary = _boundary(tmp_path) with pytest.raises(OSError, match="the move failed"): - snapshot_mod.write_snapshot({"kind": "k"}, target, _boundary(tmp_path)) + snapshot_mod.write_snapshot({"kind": "k"}, target, boundary) assert target.read_text(encoding="utf-8") == "old\n" assert sorted(p.name for p in tmp_path.iterdir()) == ["snapshot.json"] @@ -185,9 +188,9 @@ def test_a_rewrite_keeps_the_snapshots_permissions(tmp_path) -> None: def test_the_boundary_still_decides_the_destination(tmp_path) -> None: from opendox.boundary import BoundaryViolation + boundary = _boundary(tmp_path) with pytest.raises(BoundaryViolation): - snapshot_mod.write_snapshot({"kind": "k"}, tmp_path / "elsewhere.json", - _boundary(tmp_path)) + snapshot_mod.write_snapshot({"kind": "k"}, tmp_path / "elsewhere.json", boundary) assert list(tmp_path.iterdir()) == [] diff --git a/tests/test_projection_contributions.py b/tests/test_projection_contributions.py index 3a80355..afeee68 100644 --- a/tests/test_projection_contributions.py +++ b/tests/test_projection_contributions.py @@ -253,8 +253,9 @@ def write_snapshot(snapshot, path, boundary): # pragma: no cover domain_profile.unregister() projection_seams.writer.register(OtherWriter()) try: + profile = domain_profile.load(PROFILE_FIXTURE) with pytest.raises(projection_seams.SeamAlreadyRegistered): - domain_profile.register(domain_profile.load(PROFILE_FIXTURE)) + domain_profile.register(profile) assert not domain_profile.is_registered() finally: projection_seams.writer.unregister() @@ -340,9 +341,12 @@ def SCANNED_ROOTS(self): # noqa: N802 - the governed module's own name monkeypatch.setattr(pc, "_governed", lambda name: CorpusRoot()) roots = pc.CORPUS_ROOT.SCANNED_ROOTS - assert "read when used" in repr(roots) and reads == [] + assert "read when used" in repr(roots) + assert reads == [] assert tuple(roots) == ("docs", "openspec") - assert len(roots) == 2 and roots[1] == "openspec" and "docs" in roots + assert len(roots) == 2 + assert roots[1] == "openspec" + assert "docs" in roots def test_the_change_rows_are_the_governed_enumeration_with_each_origin(monkeypatch) -> None: @@ -393,6 +397,7 @@ def test_no_validator_from_any_root_is_unavailable_naming_the_script(monkeypatch result = pc.VALIDATOR.validate(tmp_path / "s.json", search_from=(tmp_path / "a", tmp_path / "b")) assert result.outcome == projection_seams.VALIDATOR_UNAVAILABLE - assert not result.available and result.validator is None + assert not result.available + assert result.validator is None assert str(snapshot_mod.VALIDATOR_RELPATH) in result.unavailable_reason assert pc.VALIDATOR.dependency_remedy == snapshot_mod.DEPENDENCY_REMEDY diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py index abafde4..120c899 100644 --- a/tests/test_protected_suite_check.py +++ b/tests/test_protected_suite_check.py @@ -114,7 +114,8 @@ def _check(repo: Repo, entries: list[dict]) -> list[ps.Finding]: def test_an_unentered_edit_to_a_protected_suite_is_refused(repo) -> None: repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") [finding] = _check(repo, []) - assert finding.admitted_by is None and finding.path == SUITE + assert finding.admitted_by is None + assert finding.path == SUITE assert "no entry names this suite" in finding.why @@ -181,24 +182,51 @@ def test_two_landings_admitted_by_two_chained_entries(repo) -> None: assert sorted(f.admitted_by for f in findings) == [1, 2] -def test_the_command_exits_one_on_a_refusal_and_zero_when_every_edit_holds(repo, tmp_path, +def _command(repo: Repo, *, landings: str | None = None, suites: str = SUITE + "\n") -> list[str]: + """The falsifier's call: each option carries its list, one item per line.""" + if landings is None: + landings = repo.git("log", "--first-parent", "--format=%H", f"--grep=^{ARC}$", + "HEAD") + "\n" + return [f"--landings={landings}", f"--suites={suites}"] + + +def test_the_command_exits_one_on_a_refusal_and_zero_when_every_edit_holds(repo, monkeypatch) -> None: repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") - landings = tmp_path / "x-arc.txt" - landings.write_text(repo.git("log", "--first-parent", "--format=%H", - f"--grep=^{ARC}$", "HEAD") + "\n", encoding="utf-8") - suites = tmp_path / "suites.txt" - suites.write_text(SUITE + "\n", encoding="utf-8") allow = repo.root / ps.ALLOW_LIST monkeypatch.chdir(repo.root) allow.write_text(yaml.safe_dump({"schema_version": 1, "kind": ps.KIND, "entries": []}), encoding="utf-8") - assert ps.main([str(landings), str(suites)]) == 1 + assert ps.main(_command(repo)) == 1 allow.write_text(yaml.safe_dump({"schema_version": 1, "kind": ps.KIND, "entries": [_entry(repo)]}), encoding="utf-8") - assert ps.main([str(landings), str(suites)]) == 0 + assert ps.main(_command(repo)) == 0 allow.write_text("schema_version: 1\nschema_version: 1\n", encoding="utf-8") - assert ps.main([str(landings), str(suites)]) == 2 + assert ps.main(_command(repo)) == 2 + + +@pytest.mark.parametrize("landings, suites", [ + ("HEAD\n", SUITE + "\n"), # a revision, not a commit id + ("--output=/tmp/x\n", SUITE + "\n"), # an option git would take + (None, "../outside.py\n"), # not a tests/test_.py + (None, "tests/test_é.py\n"), # outside ASCII +], ids=["revision", "option", "outside-path", "non-ascii"]) +def test_an_item_of_neither_shape_is_refused_before_anything_is_checked( + repo, monkeypatch, capsys, landings, suites) -> None: + """The lists are values, never paths, and each item is held to its shape + before any of it reaches git.""" + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + monkeypatch.chdir(repo.root) + (repo.root / ps.ALLOW_LIST).write_text(yaml.safe_dump( + {"schema_version": 1, "kind": ps.KIND, "entries": [_entry(repo)]}), encoding="utf-8") + command = _command(repo, landings=landings, suites=suites) + assert ps.main(command) == 2 + assert "so nothing is checked" in capsys.readouterr().err + + +def test_a_missing_option_is_a_usage_refusal(repo, monkeypatch) -> None: + monkeypatch.chdir(repo.root) + assert ps.main([f"--suites={SUITE}"]) == 2 # -------------------------------------------------------------------------- @@ -240,8 +268,9 @@ def _valid_entry(**changes) -> dict: ("schema_version: 1\nschema_version: 1\n", "given twice"), ], ids=lambda value: value if isinstance(value, str) else None) def test_a_list_that_breaks_its_own_rules_subtracts_nothing(tmp_path, broken, why) -> None: + written = _write(tmp_path, broken) with pytest.raises(ps.AllowListInvalid, match=why): - ps.load_allow_list(_write(tmp_path, broken)) + ps.load_allow_list(written) def test_a_respelling_names_what_it_respelled(tmp_path) -> None: From 7815c2da834d1dab2ce90e4be0fef64aea629b14 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:13:52 +0000 Subject: [PATCH 11/25] Hold each half of the arc check's blob and named-test rules by a case of its own (plan 034 T059) Two mutants of scripts/protected_suites.py survived the suite: skipping the before-blob check, and skipping the check that the replaced text lies inside the named test. Each was masked by its twin (the after-blob check, and the replacement's check). New cases hold each side alone: an entry naming another blob on either side; an edit whose replaced text is module code and whose replacement becomes the test's last line; and the reverse, an assertion moved out of the test. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_protected_suite_check.py | 36 ++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py index 120c899..8d9b96f 100644 --- a/tests/test_protected_suite_check.py +++ b/tests/test_protected_suite_check.py @@ -10,7 +10,8 @@ the test the entry names, is admitted; * an entry is refused as soon as its landing's diff differs from its recorded text in any way: another edit beside it, a text that occurs twice, an edit in - another test, or blobs that are not the entry's; + another test or reaching out of the named one, or blobs that are not the + entry's; * a commit without the `Arc:` line is not a landing, whatever it touches; * entries for one suite chain, and a file that breaks its own rules refuses the whole check rather than subtracting less. @@ -161,6 +162,39 @@ def test_an_edit_outside_the_named_test_is_refused(repo) -> None: assert "not inside test_first" in finding.why +@pytest.mark.parametrize("side", ["before_blob", "after_blob"]) +def test_an_entry_whose_recorded_blob_is_not_the_landings_is_refused(repo, side) -> None: + """The text would apply, but the entry names another blob on one side, so + it records some other edit than this landing's.""" + repo.commit({SUITE: AFTER}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, **{side: repo.blob("some other text\n")})]) + assert finding.admitted_by is None + assert f"not the entry's {repo.blob('some other text' + chr(10))}" in finding.why + + +def test_an_edit_that_turns_module_code_into_test_code_is_refused(repo) -> None: + """What the entry replaces lies outside the named test, although what it + leaves lies inside it: a module-level line becomes the test's last line.""" + before = BEFORE + "X = 1\n" + after = BEFORE + " assert 1\n" + repo.commit({SUITE: before}, "a module-level line after the test") + repo.commit({SUITE: after}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, before=before, after=after, + old="X = 1\n", new=" assert 1\n")]) + assert finding.admitted_by is None + assert "old text is not inside test_second" in finding.why + + +def test_an_edit_that_moves_test_code_out_of_the_test_is_refused(repo) -> None: + """The reverse: what the entry replaces lies inside the named test, and + what it leaves lies outside it, so the test loses an assertion.""" + after = BEFORE.replace(OLD, 'X = {"a": 1}\n') + repo.commit({SUITE: after}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, after=after, new='X = {"a": 1}\n')]) + assert finding.admitted_by is None + assert "new text is not inside test_second" in finding.why + + def test_a_commit_without_the_trailer_is_not_a_landing(repo) -> None: repo.commit({SUITE: AFTER}, "an edit that is no arc landing") assert _check(repo, []) == [] From c34c0de6845182472e8646ad4614168d20842f77 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:26:10 +0000 Subject: [PATCH 12/25] Take Copilot's round-1 findings on #35: one-window registry reads, renames, the schema integer, unsigned fixtures (plan 034 T059) - r4139816732: SnapshotRegistry.index_document read the entries, the aggregates and the active key in three windows, so a writer between them could leave the index naming an active key its entries did not carry; compose_aggregate read _aggregates unlocked and looked each member up in a window of its own. Each is now one hold of the lock, with the document composed and the snapshots read after it. Two cases hold it, each running a writer on another thread between the reads; both are red before this commit. - r4139816490: the arc check's git diff detected renames, so a protected suite renamed to an unprotected path showed only the destination and was never checked. It now diffs with --no-renames, and a case holds it. - r4139816690: schema_version 1.0 passed, since 1.0 == 1. The check is now for the integer itself, and the rules' table gains the case. - r4139816759: the scratch-repository fixture inherited the developer's git configuration, so commit.gpgsign=true failed every case. It passes -c commit.gpgsign=false, as tests/test_trust_gaps.py does. Under a signing configuration the file read 16 passed, 19 errors before, and 37 passed after. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/protected_suites.py | 8 ++- src/openxdox/snapshot_registry.py | 37 +++++++++---- tests/test_governed_registry_and_writer.py | 63 +++++++++++++++++++++- tests/test_protected_suite_check.py | 18 ++++++- 4 files changed, 110 insertions(+), 16 deletions(-) diff --git a/scripts/protected_suites.py b/scripts/protected_suites.py index 3671f0e..80843c9 100644 --- a/scripts/protected_suites.py +++ b/scripts/protected_suites.py @@ -145,7 +145,8 @@ def _document(path: Path) -> dict: raise AllowListInvalid( f"{path} carries {sorted(map(str, raw))}, not schema_version, kind and entries") version = raw["schema_version"] - if isinstance(version, bool) or version != SCHEMA_VERSION: + # The integer itself: not a bool, and not `1.0`, which equals 1 in Python. + if type(version) is not int or version != SCHEMA_VERSION: raise AllowListInvalid(f"schema_version is {version!r}, not {SCHEMA_VERSION}") if raw["kind"] != KIND: raise AllowListInvalid(f"kind is {raw['kind']!r}, not {KIND!r}") @@ -302,8 +303,11 @@ def check(repo: Path, landings: list[str], protected: set[str], entry (1-based) that holds there, or refused with every entry's reason.""" findings: list[Finding] = [] for landing in landings: + # --no-renames: a protected suite renamed away is a deletion at its + # own path, never only the destination's addition. touched = {line.strip() for line in - _git(repo, "diff", "--name-only", f"{landing}^1", landing).splitlines() + _git(repo, "diff", "--no-renames", "--name-only", + f"{landing}^1", landing).splitlines() if line.strip()} for path in sorted(touched & protected): admitted, reasons = _admitting(repo, landing, path, entries) diff --git a/src/openxdox/snapshot_registry.py b/src/openxdox/snapshot_registry.py index 6897939..648580f 100644 --- a/src/openxdox/snapshot_registry.py +++ b/src/openxdox/snapshot_registry.py @@ -623,12 +623,22 @@ def index_document(self, *, published: bool = False) -> dict: second place this document carries `(repository, ref)` pairs and nothing checked it, so a published index could name the branch of unmerged work. The check is here, over BOTH collections, so the premise is true of the - document rather than of one field.""" - entries = self.entries() + document rather than of one field. + + READ IN ONE WINDOW (plan 034 T059, Copilot on openXdox-code#35, + r4139816732). The entries, the aggregates and the active key are taken + under one hold of the lock, so the document never names an active key + its own entries do not carry, as it could when a writer ran between + three separate reads. The document is composed after the lock is let + go, from that one reading.""" + with self._lock: + entries = self.entries() + aggregates = self.aggregates() + active = self._active if published: for entry in entries: assert_publishable(entry.repository, entry.ref) - for aggregate in self.aggregates(): + for aggregate in aggregates: for repository, ref in aggregate.members: assert_publishable(repository, ref) doc: dict[str, Any] = { @@ -639,18 +649,17 @@ def index_document(self, *, published: bool = False) -> dict: newest = [e.generated_at for e in entries if e.generated_at] if newest: doc["generated_at"] = max(newest) - aggregates = self.aggregates() if aggregates: doc["aggregates"] = [{ "id": a.id, **({"display_name": a.display_name} if a.display_name else {}), "members": [{"repository": r, "ref": f} for r, f in a.members], } for a in aggregates] - if not published and self._active is not None: + if not published and active is not None: # Serving-side only: which entry the server considers ACTIVE. Additive, # ignored by any consumer that does not know it (and absent from a # published index, which has no notion of "active"). - doc["active"] = {"repository": self._active[0], "ref": self._active[1]} + doc["active"] = {"repository": active[0], "ref": active[1]} return doc # ---- aggregate composition (from the index, never a repository scan) ---- @@ -663,11 +672,17 @@ def compose_aggregate(self, aggregate_id: str, `repository` so a renderer can badge it. Members with no available snapshot are skipped (degrade, never refuse). `aggregate` lets a caller compose one it resolved itself (a register-DERIVED project aggregate, - add-project-merged-projection D11) without registering it.""" - aggregate = aggregate or self._aggregates.get(aggregate_id) - if aggregate is None: - return None - members = [self.get(repo, ref) for repo, ref in aggregate.members] + add-project-merged-projection D11) without registering it. + + The aggregate and its members are looked up in one hold of the lock + (plan 034 T059, r4139816732), so a writer cannot drop or replace a + member between two lookups. The members' snapshots are read after it + is let go.""" + with self._lock: + aggregate = aggregate or self._aggregates.get(aggregate_id) + if aggregate is None: + return None + members = [self.get(repo, ref) for repo, ref in aggregate.members] snapshots = [(m, m.read_json()) for m in members if m is not None] loaded = [(m, doc) for m, doc in snapshots if isinstance(doc, dict)] return compose_snapshots(aggregate, loaded) diff --git a/tests/test_governed_registry_and_writer.py b/tests/test_governed_registry_and_writer.py index 9ec9a4a..71f90dd 100644 --- a/tests/test_governed_registry_and_writer.py +++ b/tests/test_governed_registry_and_writer.py @@ -13,7 +13,8 @@ truncated snapshot (r4126138808), and `canonical_json` wrote NaN and Infinity, which no JSON reader parses (r4125900060). 4. `SnapshotRegistry` read without its lock, so a reader could answer from the - middle of a block `atomically()` holds (r4136863481). + middle of a block `atomically()` holds (r4136863481). The index and an + aggregate's composition are also one reading each (r4139816732, on #35). Each case below is red against the code before T059. @@ -216,6 +217,66 @@ def test_the_bytes_are_the_canonical_render(tmp_path) -> None: } +def _hold_between(registry, method: str, writer) -> threading.Thread: + """Run `writer` on another thread between `method`'s return and whatever + its caller reads next, as a writer interleaving there would.""" + between = threading.Event() + real = getattr(registry, method) + + def paused(*args, **kwargs): + out = real(*args, **kwargs) + between.set() + time.sleep(0.2) + return out + + setattr(registry, method, paused) + + def run() -> None: + between.wait(5) + writer() + + thread = threading.Thread(target=run) + thread.start() + return thread + + +def test_the_index_is_one_reading_of_the_registry(monkeypatch) -> None: + """A writer that runs between the index's reads cannot give it an active + key its own entries do not carry (Copilot, r4139816732).""" + monkeypatch.setattr(reg.SnapshotEntry, "index_entry", + lambda self: {"repository": self.repository, "ref": self.ref}) + registry = reg.SnapshotRegistry() + registry.register(_entry("alpha")) + + def writer() -> None: + registry.drop("alpha") + registry.register(_entry("beta"), active=True) + + thread = _hold_between(registry, "entries", writer) + document = registry.index_document() + thread.join(timeout=5) + carried = {(e["repository"], e["ref"]) for e in document["entries"]} + assert (document["active"]["repository"], document["active"]["ref"]) in carried + + +def test_an_aggregate_is_composed_from_one_reading_of_its_members(monkeypatch) -> None: + """A writer that drops a member between two member lookups cannot leave + the aggregate composed from half of them.""" + composed = [] + monkeypatch.setattr(reg.SnapshotEntry, "read_json", lambda self: {}) + monkeypatch.setattr(reg, "compose_snapshots", + lambda aggregate, loaded: composed.extend(m.repository for m, _ in loaded)) + registry = reg.SnapshotRegistry() + registry.register(_entry("alpha")) + registry.register(_entry("beta")) + registry.register_aggregate(reg.Aggregate(id="agg", members=[("alpha", "main"), + ("beta", "main")])) + thread = _hold_between(registry, "get", lambda: registry.drop("beta")) + registry.compose_aggregate("agg") + thread.join(timeout=5) + assert composed == ["alpha", "beta"] + + @pytest.mark.parametrize("read", sorted(READS)) def test_a_read_waits_for_a_held_read_modify_write(read) -> None: """A block holds the registry, registers a session and promotes it, and diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py index 8d9b96f..3fe4a8a 100644 --- a/tests/test_protected_suite_check.py +++ b/tests/test_protected_suite_check.py @@ -65,8 +65,11 @@ def __init__(self, root: Path) -> None: self.git("init", "-q", "-b", "main") def git(self, *args: str) -> str: - return subprocess.run(("git", "-C", str(self.root), *args), check=True, - capture_output=True, text=True, env=self.env).stdout.strip() + # Signing off, whatever the developer's own configuration says, as this + # repository's other git fixtures do (tests/test_trust_gaps.py). + return subprocess.run(("git", "-C", str(self.root), "-c", "commit.gpgsign=false", + *args), check=True, capture_output=True, text=True, + env=self.env).stdout.strip() def commit(self, files: dict[str, str], message: str) -> str: for rel, text in files.items(): @@ -195,6 +198,16 @@ def test_an_edit_that_moves_test_code_out_of_the_test_is_refused(repo) -> None: assert "new text is not inside test_second" in finding.why +def test_a_protected_suite_renamed_away_is_refused(repo) -> None: + """A rename is the suite's deletion at its own path, whatever the + destination: rename detection must not hide it.""" + repo.git("mv", SUITE, "tests/renamed_away.py") + repo.git("commit", "-q", "-m", f"rename\n\n{ARC}") + [finding] = _check(repo, []) + assert finding.admitted_by is None + assert finding.path == SUITE + + def test_a_commit_without_the_trailer_is_not_a_landing(repo) -> None: repo.commit({SUITE: AFTER}, "an edit that is no arc landing") assert _check(repo, []) == [] @@ -286,6 +299,7 @@ def _valid_entry(**changes) -> dict: @pytest.mark.parametrize("broken, why", [ ({"schema_version": 2, "kind": ps.KIND, "entries": []}, "schema_version"), ({"schema_version": True, "kind": ps.KIND, "entries": []}, "schema_version"), + ({"schema_version": 1.0, "kind": ps.KIND, "entries": []}, "schema_version"), ({"schema_version": 1, "kind": "other", "entries": []}, "kind"), ({"schema_version": 1, "kind": ps.KIND, "entries": [], "extra": 1}, "carries"), ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(edit="rewrite")]}, "edit is"), From 1885aee1fa981a06c7354cb5bb4997fcf3cabca4 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:27:54 +0000 Subject: [PATCH 13/25] Raise the triple's floors to CI's reading, 984 and 980, with the reason (plan 034 T059) Run 36655105352 measured c34c0dea, the head before this change, at openDox-code 814516b7: selected 984, passed 980, skipped 4, failures 0, errors 0. The +99 on both floors is T060's 11 (openXdox-code#34 named the raise as the next writer's) and T059's 88: three new test files of 37, 26 and 20 cases, and the six seam-assembly cases that run here now that their file left the declaration, less the declaration check's case for that file. EXPECT_SKIPPED stays 4, the same four skips. The floors sit on the reading, margin zero, as #25 set them (Copilot, r4139816631). Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 14b998e..b2426ec 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -274,6 +274,26 @@ jobs: # in this pin carries the gap, and none now does: the four skips left # are named below, and none of them waits for `doc_health`. # + # RAISED BY plan 034 T059 (openXdox-code#35), the same way: run + # 36655105352 (2026-09-30) measured `c34c0dea`, the head before this + # change, at openDox-code `814516b7`: selected 984, passed 980, skipped + # 4, failures 0, errors 0. The +99 on both floors has two reasons: + # * +11 is T060's (openXdox-code#34), the `values` probes it added to + # `tests/test_gate_loop_probes.py`. #34 left the floors where they + # were, since the phase-2 draft rule kept it out of this file, and + # named the raise as the next writer's. That is this one. + # * +88 is T059's: it adds three test files, of 37 + # (`tests/test_protected_suite_check.py`), 26 + # (`tests/test_governed_registry_and_writer.py`) and 20 + # (`tests/test_projection_contributions.py`) cases, and + # `tests/test_seam_assembly_beside_gate_and_projection.py` leaves the + # declaration, so its six cases run here now and pass. The + # declaration's own check loses that file's case (-1). + # Nothing that was collected before stopped being collected, no outcome + # changed, and the four skips are the same four (the reading compared + # with `main` `c41063d6` test by test). The floors sit ON the reading, + # as #25 set them and T044 kept them, so the margin is zero again. + # # THE SKIPS ARE NAMED, which is why the exact pin is safe to take. All # FOUR are `tests/test_aggregation_register_instance.py`'s, which the # sixteen-file list never ran: each skips because the "aggregation @@ -303,8 +323,8 @@ jobs: # on a ruling, which lowers them with its reason (above). Set to # the measured values, which this suite has actually met on CI # rather than an aspiration. - MIN_SELECTED: "885" - MIN_PASSED: "881" + MIN_SELECTED: "984" + MIN_PASSED: "980" # EXACT - the load-bearing number. EXPECT_SKIPPED: "4" run: | From 11c02cf402b1e873e84bd6898b9aec627c8a3cad Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:02:13 +0000 Subject: [PATCH 14/25] Take Copilot's later findings on #35: an entry admits one landing, malformed keys and edits refuse, the teardown rule stated (plan 034 T059) Copilot's reviews at c34c0de6 and 1885aee1 listed three findings in their overviews: - Replay: an entry matched by suite, blobs and text could admit a second landing that repeated its edit after the suite came back to its before_blob. The check now takes the landings oldest first, whatever order they arrive in, and an entry that has admitted one is spent. Two cases hold it: X -> Y -> X -> Y with two entries refuses the third landing, and either input order gives the same answer. - Malformed input: edit: [] reached a dict membership test and raised TypeError, and a mapping key that is itself a sequence did the same, instead of exit 2 with nothing subtracted. Both are AllowListInvalid now, and the rules' table gains both cases. - Teardown: domain_profile.unregister() leaves the governed contributions registered. That is kept, and its docstring now says why: they are the process's, a host registers them without this module too (T064), and openDox's seams refuse a governed registration once a default has been read, so a teardown that took them back could not be undone. projection_contributions.unregister() is the teardown for them, and a case pins both halves. The four checker cases are red before this commit and green after. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/protected_suites.py | 37 +++++++++++++++++++++----- src/openxdox/domain_profile.py | 15 ++++++++++- tests/test_projection_contributions.py | 22 +++++++++++++++ tests/test_protected_suite_check.py | 33 +++++++++++++++++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/scripts/protected_suites.py b/scripts/protected_suites.py index 80843c9..0f1f6fe 100644 --- a/scripts/protected_suites.py +++ b/scripts/protected_suites.py @@ -48,6 +48,13 @@ A landing that touches a protected suite is admitted for that suite only if one entry holds at it. Every other protected path it touches is refused. +AN ENTRY ADMITS ONE LANDING (Copilot on openXdox-code#35). The landings are +taken oldest first, and an entry that has admitted one is spent: a later +landing that repeats the same edit, after the suite came back to the entry's +`before_blob`, is refused unless an entry of its own holds. An entry names its +landing by pull request, not by commit (T019's rule), so the commit it admits +is found here, once. + THE ALLOW-LIST'S OWN RULES are checked before anything is subtracted, and a file that breaks one refuses the whole check (exit 2) rather than subtracting less: `schema_version` 1, `kind` `protected-suite-respellings`, no key given @@ -107,6 +114,10 @@ def _mapping(loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = Fals seen: set[Any] = set() for key_node, _value in node.value: key = loader.construct_object(key_node, deep=deep) + if not isinstance(key, (str, int, float, bool)) and key is not None: + raise AllowListInvalid( + f"a key is a {type(key).__name__} (line {key_node.start_mark.line + 1}), " + "and every key is a plain scalar") if key in seen: raise AllowListInvalid( f"the key {key!r} is given twice (line {key_node.start_mark.line + 1})") @@ -160,7 +171,7 @@ def _check_keys(entry: Any, where: str) -> None: if not isinstance(entry, dict): raise AllowListInvalid(f"{where} is not a mapping") edit = entry.get("edit") - if edit not in EDIT_KEYS: + if not isinstance(edit, str) or edit not in EDIT_KEYS: raise AllowListInvalid(f"{where}: edit is {edit!r}, not one of {sorted(EDIT_KEYS)}") wanted = COMMON_KEYS | EDIT_KEYS[edit] if set(entry) != wanted: @@ -282,14 +293,19 @@ class Finding: why: str -def _admitting(repo: Path, landing: str, path: str, - entries: list[dict]) -> tuple[int | None, list[str]]: +def _admitting(repo: Path, landing: str, path: str, entries: list[dict], + spent: dict[int, str]) -> tuple[int | None, list[str]]: """The entry (1-based) for `path` that holds at `landing`, or None and - every entry's reason for not holding.""" + every entry's reason for not holding. An entry in `spent` has admitted + another landing already and admits no second one.""" reasons = [] for n, entry in enumerate(entries, 1): if entry["suite"] != path: continue + if n in spent: + reasons.append(f"entry {n}: it admitted {spent[n][:12]} already, " + "and an entry admits one landing") + continue why = entry_holds(repo, landing, entry) if why is None: return n, [] @@ -300,9 +316,14 @@ def _admitting(repo: Path, landing: str, path: str, def check(repo: Path, landings: list[str], protected: set[str], entries: list[dict]) -> list[Finding]: """One finding per protected path each landing touched: admitted by the - entry (1-based) that holds there, or refused with every entry's reason.""" + entry (1-based) that holds there, or refused with every entry's reason. + The landings are taken oldest first, whatever order they come in, and + each entry admits one of them at most.""" findings: list[Finding] = [] - for landing in landings: + spent: dict[int, str] = {} + ancestors = {landing: int(_git(repo, "rev-list", "--count", landing).strip()) + for landing in landings} + for landing in sorted(landings, key=ancestors.__getitem__): # --no-renames: a protected suite renamed away is a deletion at its # own path, never only the destination's addition. touched = {line.strip() for line in @@ -310,7 +331,9 @@ def check(repo: Path, landings: list[str], protected: set[str], f"{landing}^1", landing).splitlines() if line.strip()} for path in sorted(touched & protected): - admitted, reasons = _admitting(repo, landing, path, entries) + admitted, reasons = _admitting(repo, landing, path, entries, spent) + if admitted is not None: + spent[admitted] = landing findings.append(Finding( landing, path, admitted, "" if admitted else ("; ".join(reasons) or "no entry names this suite"))) diff --git a/src/openxdox/domain_profile.py b/src/openxdox/domain_profile.py index 1f7c518..544b6fe 100644 --- a/src/openxdox/domain_profile.py +++ b/src/openxdox/domain_profile.py @@ -1136,7 +1136,20 @@ def register(profile: DomainProfile) -> DomainProfile: def unregister() -> None: - """Drop the registration. For test isolation and for a host tearing down.""" + """Drop the registration. For test isolation and for a host tearing down. + + THE GOVERNED CONTRIBUTIONS STAY (plan 034 T059). `register()` also + registers openXdox's governed projection at openDox's seams + (`projection_contributions.register()`), and this leaves it there. It is + the process's, not the profile's: a host registers it without this module + too (openxFactory does, plan 034 T064), and openDox's seams refuse a + governed registration once one of their defaults has been read. A + teardown that took it back would therefore make registering the same + profile again fail wherever anything was served from a default in + between, so a test that takes the profile away for one case could not put + it back. A host that tears the governed projection down as well calls + `projection_contributions.unregister()`, which empties only what it holds. + """ global _registered _registered = None diff --git a/tests/test_projection_contributions.py b/tests/test_projection_contributions.py index afeee68..363b06e 100644 --- a/tests/test_projection_contributions.py +++ b/tests/test_projection_contributions.py @@ -243,6 +243,28 @@ def test_registering_openxdoxs_profile_registers_the_contributions(isolated_seam domain_profile.register(held) +def test_unregistering_the_profile_leaves_the_contributions(isolated_seams) -> None: + """The profile's teardown drops the profile only. The contributions are the + process's, and a teardown that took them back could not be undone once a + default had been read (the docstring of `domain_profile.unregister`).""" + held = _registered_profile() + domain_profile.unregister() + try: + profile = domain_profile.load(PROFILE_FIXTURE) + domain_profile.register(profile) + domain_profile.unregister() + assert not domain_profile.is_registered() + assert pc.is_registered() + domain_profile.register(profile) # and back, as a fixture does + assert domain_profile.is_registered() + pc.unregister() + assert not pc.is_registered() + finally: + domain_profile.unregister() + if held is not None: + domain_profile.register(held) + + def test_a_refused_contribution_leaves_no_profile_registered(isolated_seams) -> None: class OtherWriter: @staticmethod diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py index 3fe4a8a..32b200d 100644 --- a/tests/test_protected_suite_check.py +++ b/tests/test_protected_suite_check.py @@ -208,6 +208,36 @@ def test_a_protected_suite_renamed_away_is_refused(repo) -> None: assert finding.path == SUITE +def test_an_entry_admits_one_landing_and_a_replay_is_refused(repo) -> None: + """Two reviewed landings take the suite X -> Y and back to X. A third, + unreviewed, repeats the first edit: the suite is at entry 1's + `before_blob` again and its diff is entry 1's text, but entry 1 has + admitted its landing and admits no second one (Copilot on #35).""" + first = repo.commit({SUITE: AFTER}, f"reviewed edit\n\n{ARC}") + second = repo.commit({SUITE: BEFORE}, f"reviewed revert\n\n{ARC}") + replay = repo.commit({SUITE: AFTER}, f"the same edit, unreviewed\n\n{ARC}") + entries = [_entry(repo), + _entry(repo, before=AFTER, after=BEFORE, old=NEW, new=OLD)] + findings = {f.landing: f for f in _check(repo, entries)} + assert findings[first].admitted_by == 1 + assert findings[second].admitted_by == 2 + assert findings[replay].admitted_by is None + assert f"it admitted {first[:12]} already" in findings[replay].why + + +def test_the_landings_are_taken_oldest_first_in_any_order(repo) -> None: + """The falsifier lists landings newest first; the check does not depend + on it.""" + first = repo.commit({SUITE: AFTER}, f"reviewed edit\n\n{ARC}") + repo.commit({SUITE: BEFORE}, "an unentered revert, no landing") + replay = repo.commit({SUITE: AFTER}, f"the same edit again\n\n{ARC}") + for order in ([first, replay], [replay, first]): + findings = {f.landing: f for f in + ps.check(repo.root, order, {SUITE}, [_entry(repo)])} + assert findings[first].admitted_by == 1 + assert findings[replay].admitted_by is None + + def test_a_commit_without_the_trailer_is_not_a_landing(repo) -> None: repo.commit({SUITE: AFTER}, "an edit that is no arc landing") assert _check(repo, []) == [] @@ -303,6 +333,9 @@ def _valid_entry(**changes) -> dict: ({"schema_version": 1, "kind": "other", "entries": []}, "kind"), ({"schema_version": 1, "kind": ps.KIND, "entries": [], "extra": 1}, "carries"), ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(edit="rewrite")]}, "edit is"), + ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(edit=[])]}, "edit is"), + ("schema_version: 1\nkind: protected-suite-respellings\nentries: []\n? [a]\n: 1\n", + "plain scalar"), ({"schema_version": 1, "kind": ps.KIND, "entries": [_valid_entry(respelled="x")]}, "carries"), ({"schema_version": 1, "kind": ps.KIND, "entries": [{k: v for k, v in _valid_entry().items() if k != "review"}]}, "carries"), From 092bc8eb7f00b3137b23fc3de9e787cb8424f2e9 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:04:21 +0000 Subject: [PATCH 15/25] Raise the triple's floors to CI's reading again, 989 and 985 (plan 034 T059) Run 36733668958 measured 11c02cf4, the head before this change, at openDox-code 814516b7: selected 989, passed 985, skipped 4, failures 0, errors 0. The five cases 11c02cf4 added (four in tests/test_protected_suite_check.py, one in tests/test_projection_contributions.py) take the raise from +99 to +104: T060's 11 and T059's 93. The paragraph is restated with the new counts, and the floors sit on the reading again, margin zero. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b2426ec..7ce859e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -275,16 +275,16 @@ jobs: # are named below, and none of them waits for `doc_health`. # # RAISED BY plan 034 T059 (openXdox-code#35), the same way: run - # 36655105352 (2026-09-30) measured `c34c0dea`, the head before this - # change, at openDox-code `814516b7`: selected 984, passed 980, skipped - # 4, failures 0, errors 0. The +99 on both floors has two reasons: + # 36733668958 (2026-09-30) measured `11c02cf4`, the head before this + # change, at openDox-code `814516b7`: selected 989, passed 985, skipped + # 4, failures 0, errors 0. The +104 on both floors has two reasons: # * +11 is T060's (openXdox-code#34), the `values` probes it added to # `tests/test_gate_loop_probes.py`. #34 left the floors where they # were, since the phase-2 draft rule kept it out of this file, and # named the raise as the next writer's. That is this one. - # * +88 is T059's: it adds three test files, of 37 + # * +93 is T059's: it adds three test files, of 41 # (`tests/test_protected_suite_check.py`), 26 - # (`tests/test_governed_registry_and_writer.py`) and 20 + # (`tests/test_governed_registry_and_writer.py`) and 21 # (`tests/test_projection_contributions.py`) cases, and # `tests/test_seam_assembly_beside_gate_and_projection.py` leaves the # declaration, so its six cases run here now and pass. The @@ -323,8 +323,8 @@ jobs: # on a ruling, which lowers them with its reason (above). Set to # the measured values, which this suite has actually met on CI # rather than an aspiration. - MIN_SELECTED: "984" - MIN_PASSED: "980" + MIN_SELECTED: "989" + MIN_PASSED: "985" # EXACT - the load-bearing number. EXPECT_SKIPPED: "4" run: | From 62a1b0da50089d387e6d52ec6e650e677b3e99bf Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:18:18 +0000 Subject: [PATCH 16/25] Resolve the source root under resolve_within's guard, and bring the seam-assembly file's docstring to its included state (plan 034 T059) Copilot's review at 092bc8eb listed two findings in its overview: - resolve_within resolved the source root before its exception guard, so a root that is itself a symlink loop raised out of the /source route instead of refusing. The root is now resolved inside the guard, and a case holds it (red before this commit). - tests/test_seam_assembly_beside_gate_and_projection.py still said, in two passages, that serve_projection reaches doc_health and that the module stops at its import. Both now say that was so from T044 to T059, and that the module imports and runs in the required check since T059. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- src/openxdox/snapshot_registry.py | 5 +++- tests/test_governed_registry_and_writer.py | 8 ++++++ ...eam_assembly_beside_gate_and_projection.py | 27 ++++++++++--------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/openxdox/snapshot_registry.py b/src/openxdox/snapshot_registry.py index 648580f..6a7e132 100644 --- a/src/openxdox/snapshot_registry.py +++ b/src/openxdox/snapshot_registry.py @@ -430,8 +430,11 @@ def resolve_within(root: Path, url_tail: str) -> Path | None: parts = [p for p in rel.replace("\\", "/").split("/") if p not in ("", ".")] if _names_something_hidden(parts): return None - root = Path(root).resolve() + # The ROOT is resolved inside the guard too (Copilot on openXdox-code#35): + # a source root that is itself a symlink loop refuses, as a path through + # one does, rather than raising out of the `/source` route. try: + root = Path(root).resolve() resolved = (root / rel).resolve() except (OSError, RuntimeError, ValueError): return None diff --git a/tests/test_governed_registry_and_writer.py b/tests/test_governed_registry_and_writer.py index 71f90dd..8748b33 100644 --- a/tests/test_governed_registry_and_writer.py +++ b/tests/test_governed_registry_and_writer.py @@ -71,6 +71,14 @@ def test_a_symlink_to_a_document_is_still_served(served_root: Path) -> None: served_root / "docs" / "note.md").resolve() +def test_a_source_root_that_is_a_symlink_loop_serves_nothing(tmp_path: Path) -> None: + """The root is resolved under the same guard as the path, so a malformed + root refuses instead of raising out of the route.""" + loop = tmp_path / "loop" + loop.symlink_to(tmp_path / "loop") + assert reg.resolve_within(loop, "docs/note.md") is None + + def test_the_spelled_rule_still_refuses_first(served_root: Path) -> None: assert reg.resolve_within(served_root, ".git/config") is None assert reg.resolve_within(served_root, "%2egit/config") is None diff --git a/tests/test_seam_assembly_beside_gate_and_projection.py b/tests/test_seam_assembly_beside_gate_and_projection.py index 3ef98ca..c83026f 100644 --- a/tests/test_seam_assembly_beside_gate_and_projection.py +++ b/tests/test_seam_assembly_beside_gate_and_projection.py @@ -8,15 +8,15 @@ states first. The fourth, (d), is that the extension assembles cleanly beside the OTHER existing contribution columns, in either declaration order. Two of those columns are `serve_gate.GateRoutesExtension` and -`serve_projection.ProjectionRoutesExtension`, and `serve_projection` reaches -openxFactory's `doc_health` when it is imported, through `snapshot_registry`. -So each suite imported the two behind a guard that SKIPPED when `doc_health` -was absent, saying "doc_health reachability is BUILD-arc work (§ 3.5/3.6) ... -this test will assert for real once that lands". A lone checkout never has -`doc_health`, so the six cases behind that guard skipped on every run of this -leg's required check. 9.4 names them: "every one of openXdox's six skips -carries the same reason ... The whole-product assertions are precisely the -ones that skip". +`serve_projection.ProjectionRoutesExtension`, and until plan 034 T059 +`serve_projection` reached openxFactory's `doc_health` when it was imported, +through `snapshot_registry`. So each suite imported the two behind a guard +that SKIPPED when `doc_health` was absent, saying "doc_health reachability is +BUILD-arc work (§ 3.5/3.6) ... this test will assert for real once that +lands". A lone checkout never has `doc_health`, so the six cases behind that +guard skipped on every run of this leg's required check until T044. 9.4 named +them: "every one of openXdox's six skips carries the same reason ... The +whole-product assertions are precisely the ones that skip". WHAT T044 DID. It removed the guard, so each case asserts for real, and it moved the six here. Each keeps its body and its comments, except that the @@ -68,10 +68,11 @@ ) # The two ALREADY-LANDED contribution columns every case below assembles -# beside. `serve_projection` reaches `doc_health` when it is imported -# (through `snapshot_registry`), so in a lone checkout this module stops at -# its import, at collection, on `doc_health` alone. That is the failure the -# declaration holds this file to. No guard turns it into a skip any more. +# beside. From T044 to T059 `serve_projection` reached `doc_health` when it +# was imported (through `snapshot_registry`), so in a lone checkout this +# module stopped at its import, on `doc_health` alone, and the declaration +# held it to that failure. Since T059 both import in a lone checkout, and +# this module runs in the required check. No guard turns a case into a skip. from openxdox.serve_gate import GateRoutesExtension from openxdox.serve_projection import ProjectionRoutesExtension From e78600569bc4c742691dcc1c6b89d80f04f1a084 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:25:51 +0000 Subject: [PATCH 17/25] A landing the checkout does not hold is an input refusal, exit 2, not a protected-suite refusal (plan 034 T059) Copilot's review at 62a1b0d4 listed one finding in its overview: a well-formed commit id with no object behind it reached git rev-list, and the CalledProcessError escaped as a traceback with exit 1, the status that means the arc edited a protected suite. The check now catches a failed git read and exits 2, naming the command, with nothing checked. A case holds it (red before this commit). Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/protected_suites.py | 14 ++++++++++++-- tests/test_protected_suite_check.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/protected_suites.py b/scripts/protected_suites.py index 0f1f6fe..525255a 100644 --- a/scripts/protected_suites.py +++ b/scripts/protected_suites.py @@ -31,7 +31,8 @@ refuses an item of neither shape (exit 2) rather than handing it to git. The one file it reads is the allow-list, at its fixed path under the checkout it runs in. It exits 0 when no landing touched a protected suite outside an entry that holds, 1 when one did (naming each landing and -path), and 2 when its input or the allow-list itself breaks its rules. +path), and 2 when its input or the allow-list itself breaks its rules, or when +the checkout does not hold the history a landing needs. WHEN AN ENTRY HOLDS (the file's own header states the rule, and T060 wrote it). For a landing L that touches a protected `suite`, an entry for that suite holds @@ -377,7 +378,16 @@ def main(argv: list[str] | None = None) -> int: print(f"FAIL: {ALLOW_LIST} breaks its own rules, so nothing is subtracted: {exc}", file=sys.stderr) return 2 - findings = check(repo, landings, suites, entries) + try: + findings = check(repo, landings, suites, entries) + except subprocess.CalledProcessError as exc: + # A landing this checkout does not hold, or one with no parent: the + # history the check needs is not here, which is not a refusal. + detail = (exc.stderr or "").strip().splitlines() + print(f"FAIL: git could not read the history the check needs " + f"({' '.join(map(str, exc.cmd[3:]))}: {detail[0] if detail else exc.returncode}), " + "so nothing is checked", file=sys.stderr) + return 2 refused = [] for f in findings: if f.admitted_by is not None: diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py index 32b200d..e45478b 100644 --- a/tests/test_protected_suite_check.py +++ b/tests/test_protected_suite_check.py @@ -301,6 +301,17 @@ def test_an_item_of_neither_shape_is_refused_before_anything_is_checked( assert "so nothing is checked" in capsys.readouterr().err +def test_a_landing_this_checkout_does_not_hold_is_an_input_refusal(repo, monkeypatch, + capsys) -> None: + """A well-formed commit id with no object behind it is missing history, + not an edit the arc made: exit 2, never 1 (Copilot on #35).""" + monkeypatch.chdir(repo.root) + (repo.root / ps.ALLOW_LIST).write_text(yaml.safe_dump( + {"schema_version": 1, "kind": ps.KIND, "entries": []}), encoding="utf-8") + assert ps.main(_command(repo, landings="0" * 40 + "\n")) == 2 + assert "git could not read the history" in capsys.readouterr().err + + def test_a_missing_option_is_a_usage_refusal(repo, monkeypatch) -> None: monkeypatch.chdir(repo.root) assert ps.main([f"--suites={SUITE}"]) == 2 From f22c73b00bede352d11884e947aa55b3e0966b90 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:31:55 +0000 Subject: [PATCH 18/25] Raise the triple's floors to CI's reading, 991 and 987 (plan 034 T059) Run 36736683387 measured e7860056, the head before this change, at openDox-code 814516b7: selected 991, passed 987, skipped 4, failures 0, errors 0. The two cases 62a1b0d4 and e7860056 added (the symlink-loop root in tests/test_governed_registry_and_writer.py, the missing-history landing in tests/test_protected_suite_check.py) take the raise to +106: T060's 11 and T059's 95. The floors sit on the reading again, margin zero. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7ce859e..fdf7e41 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -275,15 +275,15 @@ jobs: # are named below, and none of them waits for `doc_health`. # # RAISED BY plan 034 T059 (openXdox-code#35), the same way: run - # 36733668958 (2026-09-30) measured `11c02cf4`, the head before this - # change, at openDox-code `814516b7`: selected 989, passed 985, skipped - # 4, failures 0, errors 0. The +104 on both floors has two reasons: + # 36736683387 (2026-09-30) measured `e7860056`, the head before this + # change, at openDox-code `814516b7`: selected 991, passed 987, skipped + # 4, failures 0, errors 0. The +106 on both floors has two reasons: # * +11 is T060's (openXdox-code#34), the `values` probes it added to # `tests/test_gate_loop_probes.py`. #34 left the floors where they # were, since the phase-2 draft rule kept it out of this file, and # named the raise as the next writer's. That is this one. - # * +93 is T059's: it adds three test files, of 41 - # (`tests/test_protected_suite_check.py`), 26 + # * +95 is T059's: it adds three test files, of 42 + # (`tests/test_protected_suite_check.py`), 27 # (`tests/test_governed_registry_and_writer.py`) and 21 # (`tests/test_projection_contributions.py`) cases, and # `tests/test_seam_assembly_beside_gate_and_projection.py` leaves the @@ -323,8 +323,8 @@ jobs: # on a ruling, which lowers them with its reason (above). Set to # the measured values, which this suite has actually met on CI # rather than an aspiration. - MIN_SELECTED: "989" - MIN_PASSED: "985" + MIN_SELECTED: "991" + MIN_PASSED: "987" # EXACT - the load-bearing number. EXPECT_SKIPPED: "4" run: | From f70ed5d1e08d4c339a27acc93b16121887ca4b46 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:31:55 +0000 Subject: [PATCH 19/25] State write_snapshot's refusal order as the code has it: the rendering, then the boundary (plan 034 T059) Copilot's review at e7860056 listed one finding in its overview: the docstring said permit_output runs first, and in the next sentence that the rendering runs before it, which the code does. It now says the rendering is refused first, as SnapshotNotWritable, with nothing asked of the boundary, and that the boundary's check runs before anything is written. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- src/openxdox/snapshot.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/openxdox/snapshot.py b/src/openxdox/snapshot.py index 78f2c18..5eb651a 100644 --- a/src/openxdox/snapshot.py +++ b/src/openxdox/snapshot.py @@ -105,9 +105,11 @@ def write_snapshot(snapshot: dict[str, Any], path: Path | str, boundary) -> Path THE BOUNDARY STILL DECIDES THE DESTINATION. `permit_output` is the check `write_output` makes, root and allowlist, with its refusal and its ledger, - and it runs first, so a refused target leaves nothing behind. The - rendering runs before it, so a snapshot JSON cannot carry is refused with - nothing written either. The sibling is created exclusively beside the + and it runs before anything is written, so a refused target leaves + nothing behind. THE RENDERING RUNS BEFORE IT: a snapshot JSON cannot carry + is refused first, as `SnapshotNotWritable`, with nothing written and + nothing asked of the boundary, so a write that is wrong on both counts is + refused for its content. The sibling is created exclusively beside the permitted target, with the mode an ordinary write would give it, or with the target's own permission bits where the target exists, so a refresh never widens a restricted snapshot. Its name is a dot-file with no From 9d3155371b4086357000c71d9808dfd6b2d725ef Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:38:16 +0000 Subject: [PATCH 20/25] An entry's old text must start a line, as it ends one (plan 034 T059) Copilot at f70ed5d1 (r4146436523): the whole-lines rule checked only the trailing newline, so old: "1 == 1\n" could admit a change to the tail of "assert 1 == 1", an edit never shown whole. The occurrence must now start at the text's start or just after a newline. A case holds it (red before this commit), and entries 1 to 3 still hold at a simulated squash landing. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/protected_suites.py | 8 ++++++-- tests/test_protected_suite_check.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/protected_suites.py b/scripts/protected_suites.py index 525255a..bfa1e99 100644 --- a/scripts/protected_suites.py +++ b/scripts/protected_suites.py @@ -40,8 +40,9 @@ * `git rev-parse L^1:` is its `before_blob`, and `git rev-parse L:` is its `after_blob`; -* its `old` text occurs exactly once in the `before_blob` text, and replacing - it with `new` gives the `after_blob` text byte for byte; +* its `old` text occurs exactly once in the `before_blob` text, starting a + line (it ends one, by the file's rules), and replacing it with `new` gives + the `after_blob` text byte for byte; * the replaced text lies inside the one test the entry names, in the before text, and its replacement lies inside that test in the after text. So an entry cannot admit an edit to any other test of the suite. @@ -277,6 +278,9 @@ def entry_holds(repo: Path, landing: str, entry: dict) -> str | None: if before_text.count(old) != 1: return f"the entry's old text occurs {before_text.count(old)} times before the landing, not once" at = before_text.index(old) + if at and before_text[at - 1] != "\n": + # Whole lines: the occurrence starts a line, as it ends one. + return "the entry's old text does not start a line before the landing, so it is not whole lines" if before_text.replace(old, new, 1) != after_text: return "replacing the entry's old text with its new text does not give the suite at the landing" if not _inside_the_test(before_text, at, old, entry["test"]): diff --git a/tests/test_protected_suite_check.py b/tests/test_protected_suite_check.py index e45478b..1bf0b64 100644 --- a/tests/test_protected_suite_check.py +++ b/tests/test_protected_suite_check.py @@ -156,6 +156,18 @@ def test_an_old_text_that_occurs_twice_is_refused(repo) -> None: assert "occurs 2 times" in finding.why +def test_an_old_text_that_is_the_tail_of_a_line_is_refused(repo) -> None: + """`old` ends in a newline, but it is only the end of a line: the entry + would admit a change to part of an assertion, never shown whole + (Copilot on #35).""" + after = BEFORE.replace("assert 1 == 1", "assert 2 == 2") + repo.commit({SUITE: after}, f"edit\n\n{ARC}") + [finding] = _check(repo, [_entry(repo, after=after, test="test_first", + old="1 == 1\n", new="2 == 2\n")]) + assert finding.admitted_by is None + assert "does not start a line" in finding.why + + def test_an_edit_outside_the_named_test_is_refused(repo) -> None: """The text is exact, but it lies in `test_second`, and the entry names `test_first`.""" From 0fdd65c19f67f6ee075d175f0568e40d0fd2dd6d Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:44:50 +0000 Subject: [PATCH 21/25] Raise the triple's floors to CI's reading, 992 and 988 (plan 034 T059) Run 36738271008 measured 9d315537, the head before this change, at openDox-code 814516b7: selected 992, passed 988, skipped 4, failures 0, errors 0. The partial-line case 9d315537 added takes the raise to +107: T060's 11 and T059's 96. Margin zero again (Copilot's overview at 9d315537). Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fdf7e41..c1e2901 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -275,14 +275,14 @@ jobs: # are named below, and none of them waits for `doc_health`. # # RAISED BY plan 034 T059 (openXdox-code#35), the same way: run - # 36736683387 (2026-09-30) measured `e7860056`, the head before this - # change, at openDox-code `814516b7`: selected 991, passed 987, skipped - # 4, failures 0, errors 0. The +106 on both floors has two reasons: + # 36738271008 (2026-09-30) measured `9d315537`, the head before this + # change, at openDox-code `814516b7`: selected 992, passed 988, skipped + # 4, failures 0, errors 0. The +107 on both floors has two reasons: # * +11 is T060's (openXdox-code#34), the `values` probes it added to # `tests/test_gate_loop_probes.py`. #34 left the floors where they # were, since the phase-2 draft rule kept it out of this file, and # named the raise as the next writer's. That is this one. - # * +95 is T059's: it adds three test files, of 42 + # * +96 is T059's: it adds three test files, of 43 # (`tests/test_protected_suite_check.py`), 27 # (`tests/test_governed_registry_and_writer.py`) and 21 # (`tests/test_projection_contributions.py`) cases, and @@ -323,8 +323,8 @@ jobs: # on a ruling, which lowers them with its reason (above). Set to # the measured values, which this suite has actually met on CI # rather than an aspiration. - MIN_SELECTED: "991" - MIN_PASSED: "987" + MIN_SELECTED: "992" + MIN_PASSED: "988" # EXACT - the load-bearing number. EXPECT_SKIPPED: "4" run: | From a1b75f3a26d8c1d12c1d877642d608b773a3d986 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:54:58 +0000 Subject: [PATCH 22/25] Read doc_health only for the sentinel an entry with no revision gets (plan 034 T059) Copilot at 0fdd65c1 (r4146580826): index_entry imported pin_sentinels on every call, so an index of entries that carry their own source_revision failed where doc_health is absent, although the sentinel was never needed. The import now happens only for an entry with no revision. A blocked-module subprocess case holds both halves: a versioned entry is indexed without doc_health, and a revisionless one still fails on doc_health (red before this commit). DOC_HEALTH_SURFACE still reads one deferred import here. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- src/openxdox/snapshot_registry.py | 17 ++++++++++++----- tests/test_projection_contributions.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/openxdox/snapshot_registry.py b/src/openxdox/snapshot_registry.py index 6a7e132..2daa2a1 100644 --- a/src/openxdox/snapshot_registry.py +++ b/src/openxdox/snapshot_registry.py @@ -83,8 +83,8 @@ # comes from the declaration that a verification guarding on the exact string # reads. # -# IT IS IMPORTED WHERE IT IS READ, in `SnapshotEntry.index_entry` (plan 034 -# T059). This module is openXdox's contribution at openDox's snapshot registry +# IT IS IMPORTED WHERE IT IS READ, in `SnapshotEntry.index_entry`, for an +# entry with no revision of its own (plan 034 T059). This module is openXdox's contribution at openDox's snapshot registry # seam (`openxdox.projection_contributions`), and openDox probes a # registration's names when it is made. `pin_sentinels` is this module's one # reach into openxFactory's `doc_health`, which a lone openXdox-code checkout @@ -301,15 +301,22 @@ def index_entry(self) -> dict: only that the revision was not established: the repository is readable, and whether the snapshot's own generation lacked a revision, could not fetch one, or never recorded one is not knowable from here. Writing a - stronger member would assert a condition nobody established.""" - from doc_health import pin_sentinels + stronger member would assert a condition nobody established. + `doc_health` is read only for the sentinel (plan 034 T059, Copilot on + openXdox-code#35, r4146580826), so an entry that carries its revision + is indexed where `doc_health` is absent too.""" + source_revision = self.source_revision + if not source_revision: + from doc_health import pin_sentinels + + source_revision = pin_sentinels.UNKNOWN out: dict[str, Any] = { "repository": self.repository, "ref": self.ref, "snapshot": self.location or ( self.snapshot_path.name if self.snapshot_path else f"{self.repository}-snapshot.json"), - "source_revision": self.source_revision or pin_sentinels.UNKNOWN, + "source_revision": source_revision, } if self.generated_at: out["generated_at"] = self.generated_at diff --git a/tests/test_projection_contributions.py b/tests/test_projection_contributions.py index 363b06e..6e7b5dd 100644 --- a/tests/test_projection_contributions.py +++ b/tests/test_projection_contributions.py @@ -352,6 +352,24 @@ def test_without_doc_health_a_governed_generation_fails_on_doc_health() -> None: assert done.stdout.strip().splitlines()[-1] == "refused: doc_health" +def test_without_doc_health_an_entry_that_carries_its_revision_is_indexed() -> None: + """`doc_health` is read only for the sentinel an entry with no revision + gets, so an index of entries that carry theirs is served without it, and + the sentinel alone still fails on `doc_health` (Copilot on #35).""" + done = _run_blocked(''' + import json + from openxdox import snapshot_registry as reg + versioned = reg.SnapshotEntry(repository="alpha", ref="main", source_revision="a" * 40) + print(json.dumps(versioned.index_entry()["source_revision"])) + try: + reg.SnapshotEntry(repository="beta", ref="main").index_entry() + except ModuleNotFoundError as exc: + print("refused:", exc.name) + ''') + assert done.returncode == 0, done.stderr + assert done.stdout.strip().splitlines()[-2:] == ['"' + "a" * 40 + '"', "refused: doc_health"] + + def test_the_scanned_roots_are_read_when_used(monkeypatch) -> None: reads = [] From 4feb80092a3ba81de09ebd4adb04d10a88c6450c Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:01:31 +0000 Subject: [PATCH 23/25] Raise the triple's floors to CI's reading, 993 and 989 (plan 034 T059) Run 36740358207 measured a1b75f3a, the head before this change, at openDox-code 814516b7: selected 993, passed 989, skipped 4, failures 0, errors 0. The versioned-entry case a1b75f3a added takes the raise to +108: T060's 11 and T059's 97. Margin zero again. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c1e2901..34efbaa 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -275,16 +275,16 @@ jobs: # are named below, and none of them waits for `doc_health`. # # RAISED BY plan 034 T059 (openXdox-code#35), the same way: run - # 36738271008 (2026-09-30) measured `9d315537`, the head before this - # change, at openDox-code `814516b7`: selected 992, passed 988, skipped - # 4, failures 0, errors 0. The +107 on both floors has two reasons: + # 36740358207 (2026-09-30) measured `a1b75f3a`, the head before this + # change, at openDox-code `814516b7`: selected 993, passed 989, skipped + # 4, failures 0, errors 0. The +108 on both floors has two reasons: # * +11 is T060's (openXdox-code#34), the `values` probes it added to # `tests/test_gate_loop_probes.py`. #34 left the floors where they # were, since the phase-2 draft rule kept it out of this file, and # named the raise as the next writer's. That is this one. - # * +96 is T059's: it adds three test files, of 43 + # * +97 is T059's: it adds three test files, of 43 # (`tests/test_protected_suite_check.py`), 27 - # (`tests/test_governed_registry_and_writer.py`) and 21 + # (`tests/test_governed_registry_and_writer.py`) and 22 # (`tests/test_projection_contributions.py`) cases, and # `tests/test_seam_assembly_beside_gate_and_projection.py` leaves the # declaration, so its six cases run here now and pass. The @@ -323,8 +323,8 @@ jobs: # on a ruling, which lowers them with its reason (above). Set to # the measured values, which this suite has actually met on CI # rather than an aspiration. - MIN_SELECTED: "992" - MIN_PASSED: "988" + MIN_SELECTED: "993" + MIN_PASSED: "989" # EXACT - the load-bearing number. EXPECT_SKIPPED: "4" run: | From 25414afc48ea40d52055522d2ad7819d80150d2d Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:46:55 +0000 Subject: [PATCH 24/25] Re-pin openDox-code 047bb4fa, the commit T062 pins at the openDox root (plan 034 T059) T062 (opensoft/openDox#16) landed as d5098297. It pins openDox-code 047bb4fa394f3e1bf42466062a67ef18e99f8d6a, the head of openDox-code main after T058 (#68). So this leg's `opendox @` pin moves from 814516b7 (the head of openDox-code#59) to that commit. The pin crosses, first-parent: T057 (#58, 8ec08e9), T055 (#59, fa14087) and its follow-up (#70, 75bd870), T056 (#66, a23e422) and T058 (#68, 047bb4f). The repository has no lock or constraints file, so pyproject.toml is the one place the pin lives. One seam moved. T058 replaced default_projection.VALIDATOR, one stand-in for all of openDox's own kinds, with VALIDATORS, one validator per own kind. So tests/test_projection_contributions.py reads VALIDATORS[kind] in its two cases. That file is T059's own and not a protected suite. The pin comments were re-measured at 047bb4fa, and each measurement holds: * ViewBinding still takes exports and styles (dataclasses.fields); * _back_import_census() returns the same three rows as the ratchet table; * openDox's NEUTRAL_MODULES is still thirteen, with STILL_REACHING empty; * the vendored stylesheet helpers and GATE_EXCLUSIVE are byte-unchanged in tests/test_binding_stylesheets.py. The whole suite, alone at the new pin, gives 989 passed, 4 skipped and 1 deselected (CI's command), the triple unchanged. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 2 +- pyproject.toml | 2 +- src/openxdox/view_extensions.py | 14 ++++++++------ tests/integration/test_assembled_bundle.py | 2 +- tests/test_dependency_direction.py | 5 ++++- tests/test_gate_loop_probes.py | 2 +- tests/test_projection_contributions.py | 7 +++++-- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 34efbaa..22a914e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -132,7 +132,7 @@ jobs: # imported carved session fixtures that reached `ideation_dashboard` and # `doc_health`, two packages neither leg carries. At the openDox this # leg pinned from plan 034 T040 (openDox-code `2d116415`), and at the one - # it pins since plan 034 T059 (`814516b7`), the chain loads in a lone + # it pins since plan 034 T059 (`047bb4fa`), the chain loads in a lone # checkout, and what still reaches openxFactory is what the declaration # and `LEFT_OUT` hold. Each of the sixteen files' # account of itself (why it joined the list, its counts, its review diff --git a/pyproject.toml b/pyproject.toml index 14d480d..c444e07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ requires-python = ">=3.12" # PR) — and not an arbitrary choice: `Draft202012Validator`, the name # `gate_console.py:563` imports, was added in jsonschema 4.18.0. dependencies = [ - "opendox @ git+https://github.com/opensoft/openDox-code@814516b778b04d4d5022e486c657774b61c86f15", + "opendox @ git+https://github.com/opensoft/openDox-code@047bb4fa394f3e1bf42466062a67ef18e99f8d6a", "PyYAML>=6.0", "jsonschema>=4.18", ] diff --git a/src/openxdox/view_extensions.py b/src/openxdox/view_extensions.py index 3c1b64b..7a47813 100644 --- a/src/openxdox/view_extensions.py +++ b/src/openxdox/view_extensions.py @@ -24,23 +24,25 @@ registry itself — `opendox.view_extension` does not exist there, and the `exports` field RULED Q2 adds is newer still". THAT IS NO LONGER TRUE, and the old wording is quoted here as provenance rather than deleted: the pin named -openDox-code#55 (`2d116415`, plan 034 T037's landing) from plan 034 T040, and -names openDox-code `814516b7` (the head of openDox-code#59, T055) since plan 034 -T059; at both `view_extension` is importable and `ViewBinding` takes `exports` — +openDox-code#55 (`2d116415`, plan 034 T037's landing) from plan 034 T040, +named openDox-code `814516b7` (the head of openDox-code#59, T055) in plan 034 +T059's drafts, and names `047bb4fa` (phase 2's openDox-code, after T058, which +T062 pins at the openDox root) since then; at each `view_extension` is +importable and `ViewBinding` takes `exports` — measured, and the three materialization assertions in `tests/test_gate_loop_views.py` run and pass against it instead of skipping. It first became true at `0b4e8bbf` (openDox-code#23, § 3.4 slice S8 leg B), which is where that wording was corrected; the pin then crossed `0e65b5f8` (#24) to `5c137a90` (openDox-code#27, § 3.4 RULED Q7), whose `ViewBinding` first carried a `styles` field — absent at `0b4e8bbf`, present at `5c137a90` -and still at `2d116415` and at `814516b7`, measured by `dataclasses.fields()` +and still at `2d116415`, at `814516b7` and at `047bb4fa`, measured by `dataclasses.fields()` in a venv at each pin. THE `5c137a90` BUMP ITSELF READ NOTHING, and the review of `ea6991b` was right to check that: it materialized `VIEW_BINDING_SPECS` unchanged and asked nothing about the installed `ViewBinding`. THE READING IS THIS ACT'S, and this act is the pull request that bump named as waiting on it: `specs_for()` below reads `dataclasses.fields(binding_cls)` and drops `styles` where the installed dataclass has no such field. MEASURED IN A VENV AT THAT PIN, and again at -`2d116415` and at `814516b7`: it has one, so nothing is dropped, every binding that owns +`2d116415`, at `814516b7` and at `047bb4fa`: it has one, so nothing is dropped, every binding that owns selectors declares its sheet, and the four contributed stylesheets are LIVE rather than inert — which is the one thing they waited on that bump for. @@ -519,7 +521,7 @@ def specs_for(binding_cls: Any) -> tuple[dict[str, Any], ...]: So the field is DROPPED where the installed class does not take it and the column mounts unstyled. AT THE PIN THIS LEG DECLARES TODAY THE DETECTION IS THE PLAIN PATH, not a fallback: `dataclasses.fields()` finds `styles` on - `814516b7`'s `ViewBinding`, as on `2d116415`'s and on `5c137a90`'s where the field first + `047bb4fa`'s `ViewBinding`, as on `814516b7`'s, `2d116415`'s and on `5c137a90`'s where the field first reached the pin, every spec crosses whole, and the four contributed sheets are LIVE — same code, same behaviour, one branch not taken. This paragraph read "the pin bump that follows openDox-code's Q7 leg turns the sheets on with no edit here"; that bump landed, and that is what diff --git a/tests/integration/test_assembled_bundle.py b/tests/integration/test_assembled_bundle.py index 703029b..859c176 100644 --- a/tests/integration/test_assembled_bundle.py +++ b/tests/integration/test_assembled_bundle.py @@ -31,7 +31,7 @@ `_ST_DECLARATION` and `_declared_st_tokens`) and the `GATE_EXCLUSIVE` tuple are openDox-code's text at `55194335`, the last commit that carried all five, byte for byte. The tuple and the three helpers openDox-code kept are unchanged at -`2d116415` and at `814516b7`. Their comments are kept too, so "Copilot review, round N" in them +`2d116415`, at `814516b7` and at `047bb4fa`. Their comments are kept too, so "Copilot review, round N" in them is a round on openDox-code#27, where that file was written. Four things changed, each because this is the composition and not a lone leg: 1. A missing assembly FAILS here, where it skipped there. The composition is diff --git a/tests/test_dependency_direction.py b/tests/test_dependency_direction.py index 49de0b4..63743bf 100644 --- a/tests/test_dependency_direction.py +++ b/tests/test_dependency_direction.py @@ -422,7 +422,10 @@ def test_the_doc_health_implementation_surface_is_exactly_declared() -> None: #: `domain_profile`, `profile_proxy`, `view_extension`, `cli` and `serve`. At #: `814516b7` it holds thirteen, T055's four new modules with them: #: `projection_seams`, `default_registry`, `default_projection` and `rfc3339`. -#: Its `STILL_REACHING` is empty at both. +#: Its `STILL_REACHING` is empty at both. At `047bb4fa`, where the pin moved +#: next (phase 2's openDox-code after T058, which T062 pins at the openDox +#: root), both are unchanged, and `_back_import_census()` returns the same +#: three rows the table below holds, so the ratchet does not move. #: #: THE TWO MODULES THAT DID NOT IMPORT WITHOUT A CONSUMER NOW DO. This note #: said `opendox.serve` and `opendox.cli` were blocked by `ideation_dashboard` diff --git a/tests/test_gate_loop_probes.py b/tests/test_gate_loop_probes.py index a908799..7a1ef41 100644 --- a/tests/test_gate_loop_probes.py +++ b/tests/test_gate_loop_probes.py @@ -194,7 +194,7 @@ def bundle(tmp_path) -> Path: # Its four-row table is there, not restated here. Round 1 of the review on # #21 found this file claiming the install "came from somewhere older than # the declared pin" on a check that only tested for a marker. UNDER THAT - # CHECK, had the DECLARED leg (`0b4e8bbf` then, `814516b7` now) itself ever + # CHECK, had the DECLARED leg (`0b4e8bbf` then, `047bb4fa` now) itself ever # stopped shipping `web/**`, all thirteen # probes below would have skipped and this required check would have stayed # green over the regression. UNDER THE TABLE THEY OBEY NOW THEY FAIL: the diff --git a/tests/test_projection_contributions.py b/tests/test_projection_contributions.py index 6e7b5dd..9dd7d43 100644 --- a/tests/test_projection_contributions.py +++ b/tests/test_projection_contributions.py @@ -120,8 +120,11 @@ def test_openDoxs_own_kinds_keep_openDoxs_validator(isolated_seams) -> None: projection_seams.register_defaults() from opendox import default_projection + # openDox-code 047bb4fa (plan 034 T058) registers one validator per own + # kind, `default_projection.VALIDATORS[kind]`, where 814516b7 had one + # stand-in, `VALIDATOR`, for all of them. for kind in default_projection.OWN_KINDS: - assert projection_seams.validators.for_kind(kind) is default_projection.VALIDATOR + assert projection_seams.validators.for_kind(kind) is default_projection.VALIDATORS[kind] for kind in pc.GOVERNED_KINDS: assert projection_seams.validators.for_kind(kind) is pc.VALIDATOR assert projection_seams.registry.current() is snapshot_registry @@ -220,7 +223,7 @@ def test_unregister_empties_only_what_it_holds(isolated_seams) -> None: from opendox import default_projection projection_seams.validators.register_default( - "opendox-snapshot", default_projection.VALIDATOR) + "opendox-snapshot", default_projection.VALIDATORS["opendox-snapshot"]) pc.unregister() assert not generator_seam.is_registered() assert not projection_seams.writer.is_registered() From aa0a2c5d0c3193e2507e400cc1a29dd3430f1df1 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:57:15 +0000 Subject: [PATCH 25/25] Give back a replaced unread default when a registration is refused (plan 034 T059) Copilot on #35 at 25414afc (r4149710491). When openDox's entry points had installed their unread defaults, register() replaced them seam by seam. If a later seam then refused, the rollback emptied the earlier seams, so a caller that caught the refusal lost its neutral generator, registry and corpus root, despite the all-or-none contract. Before each write, register() now records the unread default the seam holds. It reads the default where the seam keeps it, as _holds does, because current() would close the default's window. A refusal then empties what the call wrote and gives the displaced default back with the seam's own register_default, so each seam holds what it held before, and a default stays an unread default. The case that pinned the old emptying, test_a_replaced_unread_default_is_given_back_as_a_default, asserts the restoration now, with the seams' private names pinned. It also shows the default is still replaceable: once the writer's reader lets go, register() lands. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Co-Authored-By: Claude Opus 5.5 (1M context) --- src/openxdox/projection_contributions.py | 37 ++++++++++++++++++++---- tests/test_projection_contributions.py | 26 ++++++++++++----- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/openxdox/projection_contributions.py b/src/openxdox/projection_contributions.py index a3d5a79..3787069 100644 --- a/src/openxdox/projection_contributions.py +++ b/src/openxdox/projection_contributions.py @@ -328,24 +328,51 @@ def _take_back(seam: Any, kind: str | None) -> None: seam.unregister(kind) +def _unread_default(seam: Any, kind: str | None) -> Any: + """The unread default `seam` holds (for `kind`), or None. It is the one + registration `register()` can replace: a host's refuses, and so does a + default that has been read. Read where the seam keeps it, like `_holds`, + because `current()` would close the default's window.""" + if seam is generator_seam: + return generator_seam._registered if generator_seam._is_default else None + if kind is None: + return seam._registered if seam._is_default else None + held = seam._registered.get(kind) + return held[0] if held is not None and held[1] else None + + +def _put_back(seam: Any, kind: str | None, displaced: Any) -> None: + """Empty what this call wrote, and give back the unread default it + replaced, as the default it was (Copilot on openXdox-code#35).""" + _take_back(seam, kind) + if displaced is None: + return + if kind is None: + seam.register_default(displaced) + else: + seam.register_default(kind, displaced) + + def register() -> tuple[str, ...]: """Register every contribution at its seam, all or none. Returns the seams' names. A refusal is openDox's own (`GeneratorAlreadyRegistered`, `SeamAlreadyRegistered`), raised unchanged once every seam this call wrote - has been emptied again.""" + holds again what it held before: nothing, or the unread default this call + replaced, given back as a default.""" with _lock: - written: list[tuple[Any, str | None]] = [] + written: list[tuple[Any, str | None, Any]] = [] try: for _name, seam, kind, contribution in _contributions(): if _holds(seam, kind, contribution): continue + displaced = _unread_default(seam, kind) _register_one(seam, kind, contribution) - written.append((seam, kind)) + written.append((seam, kind, displaced)) except BaseException: - for seam, kind in reversed(written): - _take_back(seam, kind) + for seam, kind, displaced in reversed(written): + _put_back(seam, kind, displaced) raise return tuple(name for name, *_ in _contributions()) diff --git a/tests/test_projection_contributions.py b/tests/test_projection_contributions.py index 9dd7d43..ef2c317 100644 --- a/tests/test_projection_contributions.py +++ b/tests/test_projection_contributions.py @@ -170,22 +170,34 @@ def write_snapshot(snapshot, path, boundary): # pragma: no cover assert projection_seams.validators.kinds() == () -def test_a_replaced_unread_default_is_taken_back_too(isolated_seams) -> None: +def test_a_replaced_unread_default_is_given_back_as_a_default(isolated_seams) -> None: """The entry points registered their defaults, and something read the writer's. This call replaces the unread defaults before the writer - refuses, and empties them again, so no seam is left governed while the - writer stays neutral.""" - from opendox import default_generator, default_projection + refuses. It gives each one back as the unread default it was (Copilot on + openXdox-code#35), so a caller that catches the refusal keeps its neutral + generator, registry and corpus root. No seam is left governed while the + writer stays neutral, and none is left empty either.""" + from opendox import default_generator, default_projection, default_registry generator_seam.register_default(default_generator.GENERATOR) projection_seams.register_defaults() + registry_default = projection_seams.registry._registered + corpus_root_default = projection_seams.corpus_root._registered assert projection_seams.writer.current() is default_projection.WRITER # read with pytest.raises(projection_seams.SeamAlreadyRegistered): pc.register() - assert not generator_seam.is_registered() - assert not projection_seams.registry.is_registered() - assert not projection_seams.corpus_root.is_registered() + # each is back, and still an unread default: the names are pinned here + assert generator_seam._registered is default_generator.GENERATOR + assert generator_seam._is_default + assert projection_seams.registry._registered is registry_default is default_registry + assert projection_seams.registry._is_default + assert projection_seams.corpus_root._registered is corpus_root_default + assert projection_seams.corpus_root._is_default assert projection_seams.writer.current() is default_projection.WRITER + # and replaceable still: once the writer's reader lets go, this call lands + projection_seams.writer.unregister() + pc.register() + assert pc.is_registered() def test_a_seam_already_holding_the_contribution_is_not_taken_back(isolated_seams) -> None: