From e0298cf4b8fd5e9162265aa1c3a7c51263117f56 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:22:04 +0000 Subject: [PATCH 1/2] T085: openDox's own defaults for the two doxBench seams, and the workbench rules move into opendox.validator (plan 034) The standalone defaults for T027's two seams (R1Q10 (a), R1Q12 (a); openxFactory#656 comment 5850003126), in R1Q3 (a)'s pattern: - New src/opendox/doxbench_defaults.py. - The validators default is opendox.validator.doxbench_validators(): openDox's own validator over its packaged copies of xfactory-workbench-chat-turn and xfactory-workbench-model-catalog (T057), one validator per doxBench wire kind (DOXBENCH_KINDS). - The rail default is NO_STATUS_EXEMPTION: no source is exempt. It reports a document's own Status: line, raw, so the packet's label stays true. - register_defaults() registers both. - serve_wire and doxbench_packet gain register_default_*() and the projection seams' rules: - with nothing registered, each still refuses, naming the seam (4.2); - a host replaces a default until the default is read, and is refused after (R1Q3 (ii); RN-1 (a)); - a host registered first is kept. - The four entry points call doxbench_defaults.register_defaults(), beside T055's projection_seams.register_defaults(): cli.build_parser(), cli.main(), serve.build_server() and serve.main(). On the holder's ruling, these lines come BEFORE T084's in cli.py's and serve.py's single-writer order. T058's two workbench-manifest rules move from default_projection's WORKBENCH_RULES into opendox.validator (RULED 5920216845, item 2, "Move into openDox's validator (Recommended)"): - They are OWNED_RULES of the ideation-workbench copy, which carries no x-rules catalog, named pinned-keywords-are-checked and new-candidates-are-disjoint. - There are no aliases. default_projection checks neither. - A copy that carries a catalog cannot also own rules. The served model catalog now answers a standalone process. Measured at main 047bb4fa, GET /workbench/model-catalog answered 500 catalog_unavailable, and it now answers 200 with an envelope openDox's validator accepts. What the catalog offers (omp-local, declared available) is 16.4's, and T081's to change. Tests: - tests/test_doxbench_defaults.py is new, with 42 cases. - tests/test_post_render_validator.py has the new ids, 4 new cases, and the old ids only as RETIRED_RULE_IDS. - tests/test_doxbench_seams.py's fixture now restores a seam's records exactly. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/cli.py | 10 + src/opendox/contracts/__init__.py | 3 +- src/opendox/default_projection.py | 97 +----- src/opendox/doxbench_defaults.py | 120 +++++++ src/opendox/doxbench_packet.py | 168 +++++++--- src/opendox/serve.py | 11 + src/opendox/serve_wire.py | 170 +++++++--- src/opendox/validator.py | 170 +++++++++- tests/test_doxbench_defaults.py | 487 ++++++++++++++++++++++++++++ tests/test_doxbench_seams.py | 41 ++- tests/test_post_render_validator.py | 105 +++++- 11 files changed, 1186 insertions(+), 196 deletions(-) create mode 100644 src/opendox/doxbench_defaults.py create mode 100644 tests/test_doxbench_defaults.py diff --git a/src/opendox/cli.py b/src/opendox/cli.py index 8bd0fe08..b16e2beb 100644 --- a/src/opendox/cli.py +++ b/src/opendox/cli.py @@ -97,6 +97,10 @@ # register the defaults where no host has. `is_rfc3339_datetime` is not a seam: # it is the neutral contract's own date-time rule, and openDox owns it. from opendox import projection_seams # noqa: E402 +# openDox's own defaults for the two doxBench seams (plan 034 T085), which +# the entry points below register the same way. Importing it registers +# nothing. +from opendox import doxbench_defaults # noqa: E402 from opendox.rfc3339 import is_rfc3339_datetime # noqa: E402 # THE COMPOSITION POINT, BOUND AT LAST (§ 4.3; RULED ASK-2 option (2), @@ -1113,6 +1117,10 @@ def build_parser(*, subcommand_extensions: tuple = ()) -> argparse.ArgumentParse # only where no host has registered its own. Registering reads nothing, so # a host that registers after this parser is built still replaces them. projection_seams.register_defaults() + # AND openDox's OWN doxBench validators and status-exemption rail (4.3, + # T085; R1Q10 (a) and R1Q12 (a), the same pattern), each only where no + # host has registered its own, and replaceable by a host until read. + doxbench_defaults.register_defaults() parser = argparse.ArgumentParser(prog="ideation-dashboard", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = parser.add_subparsers(dest="command", required=True) @@ -1220,6 +1228,8 @@ def main(argv: list[str] | None = None, *, generator_seam.register_default(default_generator.GENERATOR) # AND openDox's own projection defaults (5.5, T055), the same way. projection_seams.register_defaults() + # AND openDox's own doxBench defaults (4.3, T085), the same way. + doxbench_defaults.register_defaults() args = build_parser( subcommand_extensions=subcommand_extensions).parse_args(argv) try: diff --git a/src/opendox/contracts/__init__.py b/src/opendox/contracts/__init__.py index 823e7a30..e114eb50 100644 --- a/src/opendox/contracts/__init__.py +++ b/src/opendox/contracts/__init__.py @@ -9,7 +9,8 @@ puts them on disk beside the validator and the assembly root remains their source of truth for editing"*. So a code-leg checkout with no assembly root around it still has them, and so does an install. `opendox.validator` reads -them from here, and T085's doxBench validators will read the same copies. +them from here, and so do its doxBench validators +(`opendox.validator.doxbench_validators`, the default T085 registers). WHAT IS HERE. diff --git a/src/opendox/default_projection.py b/src/opendox/default_projection.py index 0849baa6..47864995 100644 --- a/src/opendox/default_projection.py +++ b/src/opendox/default_projection.py @@ -63,14 +63,15 @@ is searched for, and no path can make the validator reachable or not. No subprocess runs, so there is no dependency remedy (`dependency_remedy` is None). -* THE WORKBENCH MANIFEST'S TWO VALIDATOR RULES. Its schema says of two rules - that it cannot state them, and leaves them to the validator. The consumer's - script checked them in single-file mode, and they are carried here, under - its identifiers, so routing `validate_manifest` to openDox's validator drops - neither (the holder's decision, 2026-09-28). `workbench-pinned-not-checked`: - every `recipe.pinned` keyword is also in `recipe.checked`. - `workbench-candidate-overlap`: no `recipe.new_candidates` document is - already a member or excluded. +* THE WORKBENCH MANIFEST'S TWO VALIDATOR RULES ARE THE VALIDATOR'S. Its + schema says of two rules that it cannot state them, and leaves them to the + validator. T058 carried them here, under the consumer script's + identifiers. Since plan 034's T085 (RULED `openxFactory#656` comment + `5920216845`, item 2, *"Move into openDox's validator (Recommended)"*), + `opendox.validator` owns them, as `pinned-keywords-are-checked` and + `new-candidates-are-disjoint`, and they reach this adapter's report + through `validator_for(kind).violations()` like every other rule of the + kind. This module checks neither. IMPORT WEIGHT. `opendox.generator_seam`, `opendox.projection_seams` and the standard library. So this module imports with no extra installed and no @@ -97,7 +98,7 @@ from opendox import generator_seam, projection_seams __all__ = ["CORPUS_ROOT", "CorpusRoot", "OWN_KINDS", "OwnValidator", - "NUMBER_RULE", "SYNTAX_RULE", "SnapshotNotWritable", "VALIDATORS", "WORKBENCH_RULES", + "NUMBER_RULE", "SYNTAX_RULE", "SnapshotNotWritable", "VALIDATORS", "WRITER", "Writer"] #: The workbench manifest's kind, `opendox.workbench.KIND`, restated because @@ -110,7 +111,8 @@ #: generate verb writes with openDox's own generator, and the workbench #: manifest `workbench.save()` validates. `opendox.validator` validates the #: doxBench wire kinds too, and they reach it through their own seam -#: (`serve_wire.register_doxbench_validators`, T085), not through this one. +#: (`serve_wire`'s doxBench-validators seam, where the entry points register +#: `opendox.doxbench_defaults`'s default, T085), not through this one. OWN_KINDS: tuple[str, ...] = (generator_seam.NEUTRAL_SNAPSHOT_KIND, WORKBENCH_KIND) #: How each own kind is written, and so how its document is read. @@ -129,11 +131,6 @@ #: (Copilot at openDox-code#68 c7768ed5, r4146428769). NUMBER_RULE = "document-number" -#: The workbench manifest's two validator rules, which its schema leaves to -#: the validator, under the identifiers the consumer's script gave them. -WORKBENCH_RULES: tuple[str, ...] = ("workbench-pinned-not-checked", - "workbench-candidate-overlap") - class CorpusRoot: """openDox's own corpus-root predicate: a git repository's root.""" @@ -346,48 +343,6 @@ def _refuse_repeated_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: return document -def _names(value: Any) -> list[str]: - """A list's string entries, once each, in order. Anything else answers - none: its shape is the schema's to judge, and these rules only compare. - - Linear: the schema bounds none of the three lists, so membership is a - set's, and the list only keeps the order (Copilot at openDox-code#68 - 09cd1e8a, r4139734444).""" - if not isinstance(value, list): - return [] - names: list[str] = [] - seen: set[str] = set() - for item in value: - if isinstance(item, str) and item not in seen: - seen.add(item) - names.append(item) - return names - - -#: How many names a rule's detail quotes before it says how many more, and -#: how much of each name it quotes. -_QUOTED = 10 -_NAME_CHARS = 80 - - -def _quoted(names: list[str]) -> str: - """`names` as a detail quotes them: the first few, each cut to a readable - length, and a count of the rest, so one violation stays one readable line. - The schema bounds neither the lists nor their strings (Copilot at - openDox-code#68 21e4723f, r4139769791).""" - shown = repr([name if len(name) <= _NAME_CHARS else name[:_NAME_CHARS - 1] + "…" - for name in names[:_QUOTED]]) - return shown if len(names) <= _QUOTED else f"{shown[:-1]}, and {len(names) - _QUOTED} more]" - - -def _documents(entries: Any) -> set[str]: - """The `document` of each entry of a members or excluded list.""" - if not isinstance(entries, list): - return set() - return {entry["document"] for entry in entries - if isinstance(entry, dict) and isinstance(entry.get("document"), str)} - - class OwnValidator: """openDox's own validator for ONE of its kinds, behind the validator lookup's protocol (plan 034's T058; this module's docstring).""" @@ -434,32 +389,6 @@ def construct_float(loader, node): except yaml.YAMLError as exc: raise ValueError(" ".join(str(exc).split())) from exc - @staticmethod - def _workbench_rules(document: Any) -> list: - """The manifest's two validator rules (this module's docstring).""" - from opendox.validator import Violation - - if not isinstance(document, dict) or not isinstance(document.get("recipe"), dict): - return [] - recipe = document["recipe"] - found = [] - checked = set(_names(recipe.get("checked"))) - stray = [name for name in _names(recipe.get("pinned")) if name not in checked] - if stray: - found.append(Violation( - WORKBENCH_RULES[0], ("recipe", "pinned"), "workbench-rule", - f"pinned keyword(s) {_quoted(stray)} are not in checked: every pinned " - "keyword MUST also be checked")) - placed = _documents(document.get("members")) | _documents(document.get("excluded")) - overlap = [name for name in _names(recipe.get("new_candidates")) if name in placed] - if overlap: - found.append(Violation( - WORKBENCH_RULES[1], ("recipe", "new_candidates"), "workbench-rule", - f"new_candidates {_quoted(overlap)} already appear in members or " - "excluded: a new candidate is a document the set has not " - "placed yet")) - return found - def validate(self, path: Path | str, *, strict: bool = False, search_from: tuple = ()) -> projection_seams.ValidationResult: """Validate the document at `path` as this validator's kind. `strict` @@ -505,8 +434,6 @@ def validate(self, path: Path | str, *, strict: bool = False, f"{' '.join(str(exc).split()) or type(exc).__name__}")] else: violations = kind_validator.violations(document) - if self.kind == WORKBENCH_KIND: - violations += self._workbench_rules(document) if not violations: return projection_seams.ValidationResult( True, 0, f"{self.kind}: 0 violations, by {ran}\n", "", ran) diff --git a/src/opendox/doxbench_defaults.py b/src/opendox/doxbench_defaults.py new file mode 100644 index 00000000..36546a61 --- /dev/null +++ b/src/opendox/doxbench_defaults.py @@ -0,0 +1,120 @@ +"""openDox's OWN defaults for the two doxBench seams: the schema validators and +the status-exemption rail (plan 034's T085; #1144's 4.3 as T007's batch G +amends it, and 16.4 in part; R1Q10 (a) and R1Q12 (a), `openxFactory#656` +comment `5850003126`). + +WHY THIS FILE EXISTS. T027 made two of the eight reaches into openxFactory +seams: `serve_wire.py:1369`, which imported openxFactory's doxBench contract +validators, and `doxbench_packet.py:177`, which imported its `Status:` reader. +Each refuses, naming itself, when nothing is registered, and openxFactory +registers its own (T046). Standalone, nothing did, so the served model catalog +answered `catalog_unavailable` and no context packet could be assembled. Batch +G's addendum to 4.3 names the standalone defaults: *"openDox's default +validators run over its spec leg's two chat schemas, +`xfactory-workbench-chat-turn` and `xfactory-workbench-model-catalog`, and +there is no status exemption by default."* This module holds them. + +THE ENTRY POINTS REGISTER THEM WHERE NO HOST HAS (R1Q3 (a), comment +`5817152735`). `cli.build_parser()`, `cli.main()`, `serve.build_server()` and +`serve.main()` call `register_defaults()`, beside their registrations of the +default profile, home corpus, generator and projection defaults. Each seam +keeps the projection seams' rules: + +* a process that runs none of them still refuses, naming the seam and its + call (4.2's discipline). A default is a registration an entry point makes, + never a fallback inside a seam; +* a host's registration made before the default has been read replaces it; +* after the default has been read, a host's registration is refused (R1Q3 + (ii); RN-1 (a)), so one process never verifies its model routes against two + factories, or marks its packets by two readers; +* the same registration again is a no-op. + +THE TWO DEFAULTS. + +* THE VALIDATORS: `opendox.validator.doxbench_validators`, openDox's own + validator over its packaged copies of the two doxBench schemas (T057, + R1Q12 (a)), one validator per wire kind. The seam calls it once per request, + and each call proves every copy against its recorded digest before it is + read, so a changed byte is refused on the request that reads it. +* THE RAIL: `NO_STATUS_EXEMPTION`. It marks NO source exempt from aggressive + compression, whatever its status says. openDox has no lifecycle vocabulary + of its own, and "exempt" is a word only such a vocabulary gives a status. + It still REPORTS a document's own `Status:` line, the raw value, as the + packet labels each source with it. Reporting none would label a document + that carries one as having "no Status: header", which would be false. It + reads the line the way openxFactory's reader does: the first `Status:` line + among the first `STATUS_SCAN_LINES` real lines, where only CR, LF and CRLF + end a line. + +IMPORT WEIGHT. The standard library only. `register_defaults()` imports the +two seam modules and `opendox.validator` when it is CALLED, so importing this +module registers nothing and names no sibling. + +A CREATED FILE: it has no row in openxFactory's +`docs/opendox-carve-manifest.yaml`, because the manifest declares what LEAVES +openxFactory and never what a destination assembles (RULED OQ-C). +""" + +from __future__ import annotations + +import re + +__all__ = ["NO_STATUS_EXEMPTION", "NoStatusExemption", "register_defaults"] + +#: The real line separators: CR, LF and CRLF, and nothing else, so an exotic +#: separator (a form feed, U+2028) cannot move a `Status:` line into the +#: window or out of it. +_REAL_LINE_END = re.compile(r"\r\n|\r|\n") + + +class NoStatusExemption: + """openDox's own status-exemption rail: no source is exempt. + + It carries `doxbench_packet.STATUS_EXEMPTION_REQUIRED`, and the two + constants a reader of the rail's names may ask for on + `opendox.doxbench_packet`.""" + + #: The statuses whose content is exempt from aggressive compression: none. + EXEMPT_STATUSES: frozenset[str] = frozenset() + + #: How many of a document's first real lines are read for its `Status:`. + STATUS_SCAN_LINES = 15 + + _STATUS_RE = re.compile(r"^Status:\s*(.+?)\s*$") + + def lifecycle_status(self, text: str) -> str | None: + """The document's own declared `Status:`, raw, or None when it + declares none in its first `STATUS_SCAN_LINES` real lines.""" + if not isinstance(text, str): + return None + for line in _REAL_LINE_END.split(text)[:self.STATUS_SCAN_LINES]: + match = self._STATUS_RE.match(line) + if match: + return match.group(1) + return None + + def is_compression_exempt(self, text: str) -> bool: + """Never: there is no status exemption by default (batch G's addendum + to #1144's 4.3).""" + return False + + def __repr__(self) -> str: + return "" + + +#: The one rail an entry point registers. +NO_STATUS_EXEMPTION = NoStatusExemption() + + +def register_defaults() -> None: + """Register openDox's OWN default at each of the two doxBench seams, where + no host has registered one (R1Q10 (a), in R1Q3 (a)'s pattern). + + The entry points call this beside their other default registrations. It + registers nothing over a host, and reads nothing, so a host registration + made afterwards, and before any request reads a default, still replaces + it. Importing this module registers nothing.""" + from opendox import doxbench_packet, serve_wire, validator + + serve_wire.register_default_doxbench_validators(validator.doxbench_validators) + doxbench_packet.register_default_status_exemption(NO_STATUS_EXEMPTION) diff --git a/src/opendox/doxbench_packet.py b/src/opendox/doxbench_packet.py index a896dd58..94a25ced 100644 --- a/src/opendox/doxbench_packet.py +++ b/src/opendox/doxbench_packet.py @@ -36,8 +36,10 @@ ``Status:`` vocabulary is that corpus's, and it is what made this file un-carveable; the MARKING, which is generic, stayed here. The read is now the one a HOST registers at the status-exemption seam - (``register_status_exemption``), and with none registered the assembler - refuses rather than marking nothing. Nothing about the rail's position + (``register_status_exemption``), or openDox's own, which marks nothing + exempt and which the entry points register where no host has (plan + 034's T085). With none registered the assembler refuses rather than + marking nothing. Nothing about the rail's position in the pipeline changed: it still runs inside the assembler, before the packet exists. 4. **THE BOUNDS CHECK** — refuse with the MEASURED DIMENSION. No truncation, @@ -72,6 +74,7 @@ from __future__ import annotations import dataclasses +import threading import time import unicodedata from collections.abc import Callable, Mapping, Sequence @@ -156,6 +159,18 @@ class PacketExpired(PacketRejected): # (`StatusExemptionNotRegistered`, 4.2's discipline): a packet assembled with # its sources unmarked would carry the exemption silently switched off. # +# openDox's OWN DEFAULT IS NO EXEMPTION, REGISTERED BY AN ENTRY POINT (plan +# 034's T085; R1Q10 (a), `openxFactory#656` comment `5850003126`, in R1Q3 (a)'s +# pattern). `opendox.doxbench_defaults.NO_STATUS_EXEMPTION` marks no source +# exempt, and `cli.build_parser()`, `cli.main()`, `serve.build_server()` and +# `serve.main()` register it where no host has, through +# `opendox.doxbench_defaults.register_defaults()`, with +# `register_default_status_exemption(rail)`. A process that runs none of them +# still refuses, as above: the default is a registration and never a fallback +# here. A host's rail registered BEFORE the default has been read replaces it, +# and one registered AFTER is refused (R1Q3 (ii); RN-1 (a)), so one process +# never marks its packets by two readers. +# # WHY A MODULE `__getattr__` AND NOT A TOP-LEVEL RE-EXPORT. The names are read # off this module by existing callers, and a registration happens after this # module is imported, so nothing can bind them at import time. Resolved lazily, @@ -196,7 +211,11 @@ class PacketExpired(PacketRejected): STATUS_EXEMPTION_NOT_REGISTERED = ( "no status-exemption rail is registered at openDox's status-exemption " "seam (opendox.doxbench_packet), so no source can be marked and no packet " - "is assembled. A host registers its rail at process start with " + "is assembled. openDox's own rail, no exemption, is a registration an " + "ENTRY POINT makes where no host has " + "(opendox.doxbench_defaults.register_defaults(), which cli.build_parser(), " + "cli.main(), serve.build_server() and serve.main() call), and never a " + "fallback here. A host registers its rail at process start with " + STATUS_EXEMPTION_REGISTRATION_CALL + ".") @@ -217,11 +236,44 @@ class StatusExemptionAlreadyRegistered(RuntimeError): ONE registration: a process whose packets are marked by two readers, depending on which registration an assembly happened to reach, is the failure one registration exists to prevent. Registering the SAME rail - again is not refused. `unregister_status_exemption()` makes a deliberate - swap explicit.""" + again is not refused, and neither is a host's rail over openDox's own + default before anything has read it. `unregister_status_exemption()` + makes a deliberate swap explicit.""" _status_exemption_rail: object | None = None +#: Whether the rail is openDox's own default (an entry point's), and whether +#: an assembly has read that default since it was registered. The second +#: closes the window in which a host's rail replaces it. +_status_exemption_is_default = False +_status_exemption_default_read = False +_STATUS_EXEMPTION_LOCK = threading.Lock() + + +def _require_a_rail(rail: object, call: str, whose: str) -> None: + """Refuse, with `TypeError`, a rail that does not carry + `STATUS_EXEMPTION_REQUIRED` as callables, or cannot hand one over.""" + missing: list[str] = [] + probe_failure: Exception | None = None + for name in STATUS_EXEMPTION_REQUIRED: + try: + member = getattr(rail, name) + except Exception as exc: # noqa: BLE001 - a name it cannot hand over is a name it lacks + probe_failure = probe_failure or exc + member = None + if not callable(member): + missing.append(name) + if rail is None or missing: + refusal = TypeError( + f"{call} takes {whose} status-exemption " + "rail, which must carry callable " + f"{', '.join(STATUS_EXEMPTION_REQUIRED)}; " + f"{type(rail).__name__} lacks {', '.join(missing) or 'them'}. A " + "host with no rail does not register one: it leaves the seam " + "empty, and the assembler refuses, naming this call.") + if probe_failure is None: + raise refusal + raise refusal from probe_failure def register_status_exemption(rail: object) -> object: @@ -243,50 +295,82 @@ def register_status_exemption(rail: object) -> object: The SAME rail again is a no-op, and it is not probed a second time: it was validated when it was registered, and a lazy rail whose later lookup fails must not turn an idempotent host start into a refusal. Only a NEW - registration is validated.""" - global _status_exemption_rail + registration is validated. + + A different rail over a host's is refused. Over openDox's own default it + REPLACES the default until an assembly has read the default, and it is + refused once one has (R1Q3 (ii)).""" + global _status_exemption_rail, _status_exemption_is_default + global _status_exemption_default_read if rail is not None and rail is _status_exemption_rail: return rail - missing: list[str] = [] - probe_failure: Exception | None = None - for name in STATUS_EXEMPTION_REQUIRED: - try: - member = getattr(rail, name) - except Exception as exc: # noqa: BLE001 - a name it cannot hand over is a name it lacks - probe_failure = probe_failure or exc - member = None - if not callable(member): - missing.append(name) - if rail is None or missing: - refusal = TypeError( - "register_status_exemption() takes the host's status-exemption " - "rail, which must carry callable " - f"{', '.join(STATUS_EXEMPTION_REQUIRED)}; " - f"{type(rail).__name__} lacks {', '.join(missing) or 'them'}. A " - "host with no rail does not register one: it leaves the seam " - "empty, and the assembler refuses, naming this call.") - if probe_failure is None: - raise refusal - raise refusal from probe_failure - if _status_exemption_rail is not None and _status_exemption_rail is not rail: + _require_a_rail(rail, "register_status_exemption()", "the host's") + with _STATUS_EXEMPTION_LOCK: + held = _status_exemption_rail + if held is rail: + return rail + if held is None or (_status_exemption_is_default + and not _status_exemption_default_read): + _status_exemption_rail = rail + _status_exemption_is_default = False + _status_exemption_default_read = False + return rail + over_a_host = not _status_exemption_is_default + if over_a_host: raise StatusExemptionAlreadyRegistered( "a status-exemption rail is already registered at openDox's " "status-exemption seam, and a different one would replace it. " "Registration happens once, at process start. Call " "opendox.doxbench_packet.unregister_status_exemption() first if " "the swap is deliberate.") - _status_exemption_rail = rail - return rail + raise StatusExemptionAlreadyRegistered( + "openDox's own status-exemption rail (no exemption) is registered at " + "openDox's status-exemption seam, because an entry point registered it " + "where no host had, and an assembly has already read it, so the " + "host's rail cannot replace it now. A swap would leave one process " + "marking its packets by two readers, as a host's profile after a " + "build would (R1Q3 (ii), openxFactory#656 comment 5817152735; RN-1 " + "(a), comment 5850003126). Register the host's own at process start, " + "ahead of cli.build_parser(), cli.main(), serve.build_server() and " + "serve.main(). Call opendox.doxbench_packet.unregister_status_exemption() " + "first if the swap is deliberate.") + + +def register_default_status_exemption(rail: object) -> object: + """AN ENTRY POINT's registration of openDox's own default rail (R1Q10 (a)). + + Registers `rail` ONLY where nothing is registered, and leaves a host's + rail, or a default already registered, exactly as it is. Returns whatever + is registered afterwards. It is NOT for hosts. A default that could not + mark a source is refused whether or not anything is registered, because + it is openDox's own defect.""" + global _status_exemption_rail, _status_exemption_is_default + global _status_exemption_default_read + if rail is not None and rail is _status_exemption_rail: + return rail + _require_a_rail(rail, "register_default_status_exemption()", "openDox's own") + with _STATUS_EXEMPTION_LOCK: + if _status_exemption_rail is None: + _status_exemption_rail = rail + _status_exemption_is_default = True + _status_exemption_default_read = False + return _status_exemption_rail def unregister_status_exemption() -> None: - """Drop the registration. For test isolation and for a host tearing down.""" - global _status_exemption_rail - _status_exemption_rail = None + """Drop the registration, a host's or the default, and its records. For + test isolation and for a host tearing down.""" + global _status_exemption_rail, _status_exemption_is_default + global _status_exemption_default_read + with _STATUS_EXEMPTION_LOCK: + _status_exemption_rail = None + _status_exemption_is_default = False + _status_exemption_default_read = False def status_exemption_registered() -> bool: - """Is a rail registered, without resolving anything or refusing?""" + """Is a rail registered, a host's or the entry point's default, without + resolving anything or refusing?""" return _status_exemption_rail is not None @@ -298,8 +382,15 @@ def _status_exemption(): call, `__getattr__` once per name — so the dependence on the host's rail sits at one readable point, as it did when this function imported the publisher's module by name. + + Reading openDox's own default here is what closes its window: from this + call on, a host's rail over it is refused. """ - rail = _status_exemption_rail + global _status_exemption_default_read + with _STATUS_EXEMPTION_LOCK: + rail = _status_exemption_rail + if rail is not None and _status_exemption_is_default: + _status_exemption_default_read = True if rail is None: raise StatusExemptionNotRegistered(STATUS_EXEMPTION_NOT_REGISTERED) return rail @@ -1529,8 +1620,9 @@ def __getattr__(name: str): With no rail registered these names raise `AttributeError` naming the missing attribute and the seam — a loud failure at the first assembly rather than a packet quietly assembled with every source unmarked. - openDox's own neutral default is a later act (plan 034's T085), and until - then there is none to fall back to. + openDox's own neutral default, no exemption, is not a fallback here either: + it is a registration the entry points make where no host has (plan 034's + T085), and these names then answer as that rail's own objects. THE SEAM'S REFUSAL IS TRANSLATED, NOT LET THROUGH RAW. `_status_exemption()` refuses with `StatusExemptionNotRegistered`, a `PacketError`, which diff --git a/src/opendox/serve.py b/src/opendox/serve.py index c6420917..378244ad 100644 --- a/src/opendox/serve.py +++ b/src/opendox/serve.py @@ -151,6 +151,10 @@ from opendox import consumer_reach # noqa: E402 from opendox import defaults # noqa: E402 from opendox import projection_seams # noqa: E402 +# openDox's own defaults for the two doxBench seams (plan 034 T085), which +# `build_server()` and `main()` register where no host has. Importing it +# registers nothing. +from opendox import doxbench_defaults # noqa: E402 # § 3.4 slice S5: `build_server()` publishes the VIEW MANIFEST on # `/capabilities`, the one line slice S3 built both ends of and left for the # slice at which a contribution first exists to deliver. @@ -1801,6 +1805,11 @@ def build_server( # from the registered registry, which is what lets a server be BUILT with # nothing else installed (plan 034, research R7). projection_seams.register_defaults() + # AND openDox's OWN doxBench validators and status-exemption rail (4.3, + # T085; R1Q10 (a) and R1Q12 (a), the same pattern), each only where no host + # has registered its own. So the served model catalog answers standalone, + # validated by openDox's own validator over its packaged copies. + doxbench_defaults.register_defaults() from opendox import doxbench_turns # Imported HERE rather than at module scope, for the reason that is @@ -2313,6 +2322,8 @@ def main(argv: list[str] | None = None) -> int: # BEFORE the parser: its option defaults below read the registered # registry (`registry_mod.DEFAULT_REF` and the data source's defaults). projection_seams.register_defaults() + # AND openDox's own doxBench defaults (4.3, T085), the same way. + doxbench_defaults.register_defaults() parser = argparse.ArgumentParser(prog="ideation-dashboard-serve", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--web-dir", default=str(Path(__file__).resolve().parent / "web"), diff --git a/src/opendox/serve_wire.py b/src/opendox/serve_wire.py index b062a81f..4674de67 100644 --- a/src/opendox/serve_wire.py +++ b/src/opendox/serve_wire.py @@ -85,6 +85,7 @@ from __future__ import annotations import json +import threading from opendox import doxbench_knowledge from opendox import doxbench_packet @@ -1368,13 +1369,30 @@ def doxbench_turn_v2_success_body(*, client_turn_id: str, assistant_turn_id: str # (`opendox.domain_profile.register`). openDox names no implementation, so # nothing here imports the host's package. # -# REFUSAL, NOT A DEFAULT. With nothing registered the factory refuses, naming -# this seam and the registration call (`DoxbenchValidatorsNotRegistered`, -# 4.2's discipline), and the two model routes refuse with their fixed codes, -# because `_doxbench_validators` turns any factory failure into `None`. -# openDox's own validators for its own copies of the doxBench schemas are a -# later act (plan 034's T085); until then there is nothing for this seam to -# fall back to, and "no validator" is never an implicit pass. +# openDox's OWN DEFAULT, REGISTERED BY AN ENTRY POINT (plan 034's T085; R1Q10 +# (a), `openxFactory#656` comment `5850003126`, in R1Q3 (a)'s pattern, +# `5817152735`). openDox's own validators over its packaged copies of the two +# doxBench schemas (`opendox.validator.doxbench_validators`, R1Q12 (a)) are the +# standalone default. `cli.build_parser()`, `cli.main()`, +# `serve.build_server()` and `serve.main()` register it where no host has, +# through `opendox.doxbench_defaults.register_defaults()`, with +# `register_default_doxbench_validators(factory)`. The rules are the +# projection seams' (`opendox.projection_seams`) and the profile's: +# +# * a process that runs none of them still REFUSES, naming this seam and its +# registration call (`DoxbenchValidatorsNotRegistered`, 4.2's discipline): +# the default is a registration an entry point makes, never a fallback +# inside this seam; +# * a host's registration made BEFORE the default has been read replaces it; +# * AFTER a request has read the default, a host's registration is refused, +# as a host's profile after a build is (R1Q3 (ii); RN-1 (a)): one process +# would otherwise verify its model routes against two factories; +# * the same registration again is a no-op, and +# `unregister_doxbench_validators()` makes a deliberate swap explicit. +# +# With nothing registered the two model routes refuse with their fixed codes, +# because `_doxbench_validators` turns any factory failure into `None`, and +# "no validator" is never an implicit pass. #: The ONE call a host makes, quoted verbatim in the refusal so the refusal #: names its remedy rather than its symptom. @@ -1387,19 +1405,23 @@ def doxbench_turn_v2_success_body(*, client_turn_id: str, assistant_turn_id: str DOXBENCH_VALIDATORS_NOT_REGISTERED = ( "no doxBench schema validators are registered at openDox's " "doxBench-validators seam (opendox.serve_wire), so no model route can " - "verify what it would answer, and each refuses. A host registers its " - "validators factory at process start with " - + DOXBENCH_VALIDATORS_REGISTRATION_CALL + ".") + "verify what it would answer, and each refuses. openDox's own validators " + "are a registration an ENTRY POINT makes where no host has " + "(opendox.doxbench_defaults.register_defaults(), which cli.build_parser(), " + "cli.main(), serve.build_server() and serve.main() call), and never a " + "fallback here. A host registers its validators factory at process start " + "with " + DOXBENCH_VALIDATORS_REGISTRATION_CALL + ".") class DoxbenchValidatorsNotRegistered(RuntimeError): """The doxBench-validators seam has no registered factory. Raised by `default_doxbench_validators`, and so by every request that - reaches `build_server`'s default seam in a process no host registered. - `_doxbench_validators` swallows it into `None`, like every other factory - failure, so its text never reaches the wire: the routes answer their fixed - codes, and a direct caller reads the seam and the call it names.""" + reaches `build_server`'s default seam in a process where neither a host nor + an entry point registered one. `_doxbench_validators` swallows it into + `None`, like every other factory failure, so its text never reaches the + wire: the routes answer their fixed codes, and a direct caller reads the + seam and the call it names.""" class DoxbenchValidatorsAlreadyRegistered(RuntimeError): @@ -1408,11 +1430,27 @@ class DoxbenchValidatorsAlreadyRegistered(RuntimeError): ONE registration: a process whose model routes verify against two factories, depending on which registration a request happened to reach, is the failure one registration exists to prevent. Registering the SAME - factory again is not refused. `unregister_doxbench_validators()` makes a - deliberate swap explicit.""" + factory again is not refused, and neither is a host's factory over + openDox's own default before anything has read it. + `unregister_doxbench_validators()` makes a deliberate swap explicit.""" _doxbench_validators_factory = None +#: Whether the registration is openDox's own default (an entry point's), and +#: whether a request has read that default since it was registered. The +#: second closes the window in which a host's registration replaces it. +_doxbench_validators_is_default = False +_doxbench_validators_default_read = False +_DOXBENCH_VALIDATORS_LOCK = threading.Lock() + + +def _require_a_factory(factory, call: str, whose: str) -> None: + if not callable(factory): + raise TypeError( + f"{call} takes {whose} validators factory, a callable, not " + f"{type(factory).__name__}. A host with no validators does not " + "register one: it leaves the seam empty, and the model routes " + "refuse, naming this call.") def register_doxbench_validators(factory): @@ -1422,34 +1460,80 @@ def register_doxbench_validators(factory): validators, a `dict` of wire kind to validator, as `ideation_dashboard.doxbench_contracts.validators` does for openxFactory. It is called PER REQUEST, never cached here, so a factory that re-verifies - its pin on every call keeps a mid-run repin observable.""" - global _doxbench_validators_factory - if not callable(factory): - raise TypeError( - "register_doxbench_validators() takes the host's validators " - f"factory, a callable, not {type(factory).__name__}. A host with no " - "validators does not register one: it leaves the seam empty, and " - "the model routes refuse, naming this call.") - if (_doxbench_validators_factory is not None - and _doxbench_validators_factory is not factory): + its pin on every call keeps a mid-run repin observable. + + The SAME factory again is a no-op. A different one over a host's is + refused. Over openDox's own default it REPLACES the default until a + request has read the default, and it is refused once one has.""" + global _doxbench_validators_factory, _doxbench_validators_is_default + global _doxbench_validators_default_read + _require_a_factory(factory, "register_doxbench_validators()", "the host's") + with _DOXBENCH_VALIDATORS_LOCK: + held = _doxbench_validators_factory + if held is factory: + return factory + if held is None or (_doxbench_validators_is_default + and not _doxbench_validators_default_read): + _doxbench_validators_factory = factory + _doxbench_validators_is_default = False + _doxbench_validators_default_read = False + return factory + over_a_host = not _doxbench_validators_is_default + if over_a_host: raise DoxbenchValidatorsAlreadyRegistered( "a validators factory is already registered at openDox's " "doxBench-validators seam, and a different one would replace it. " "Registration happens once, at process start. Call " "opendox.serve_wire.unregister_doxbench_validators() first if the " "swap is deliberate.") - _doxbench_validators_factory = factory - return factory + raise DoxbenchValidatorsAlreadyRegistered( + "openDox's own validators are registered at openDox's " + "doxBench-validators seam, because an entry point registered them " + "where no host had, and a request has already read them, so the " + "host's factory cannot replace them now. A swap would leave one " + "process verifying its model routes against two factories, as a " + "host's profile after a build would (R1Q3 (ii), openxFactory#656 " + "comment 5817152735; RN-1 (a), comment 5850003126). Register the " + "host's own at process start, ahead of cli.build_parser(), " + "cli.main(), serve.build_server() and serve.main(). Call " + "opendox.serve_wire.unregister_doxbench_validators() first if the " + "swap is deliberate.") + + +def register_default_doxbench_validators(factory): + """AN ENTRY POINT's registration of openDox's own default (R1Q10 (a)). + + Registers `factory` ONLY where nothing is registered, and leaves a host's + registration, or a default already registered, exactly as it is. Returns + whatever is registered afterwards. It is NOT for hosts. A default that is + not callable is refused whether or not anything is registered, because it + is openDox's own defect.""" + global _doxbench_validators_factory, _doxbench_validators_is_default + global _doxbench_validators_default_read + _require_a_factory(factory, "register_default_doxbench_validators()", + "openDox's own") + with _DOXBENCH_VALIDATORS_LOCK: + if _doxbench_validators_factory is None: + _doxbench_validators_factory = factory + _doxbench_validators_is_default = True + _doxbench_validators_default_read = False + return _doxbench_validators_factory def unregister_doxbench_validators() -> None: - """Drop the registration. For test isolation and for a host tearing down.""" - global _doxbench_validators_factory - _doxbench_validators_factory = None + """Drop the registration, a host's or the default, and its records. For + test isolation and for a host tearing down.""" + global _doxbench_validators_factory, _doxbench_validators_is_default + global _doxbench_validators_default_read + with _DOXBENCH_VALIDATORS_LOCK: + _doxbench_validators_factory = None + _doxbench_validators_is_default = False + _doxbench_validators_default_read = False def doxbench_validators_registered() -> bool: - """Is a validators factory registered, without calling it or refusing?""" + """Is a validators factory registered, a host's or the entry point's + default, without calling it or refusing?""" return _doxbench_validators_factory is not None @@ -1460,18 +1544,26 @@ def default_doxbench_validators() -> dict: This is `build_server`'s default for the `schema_validator_factory` seam. It is a function, not an eager module-level load, so a server can be built on a plane with no registered factory and simply refuse the two model - routes rather than failing to start — and so the host's factory runs per - request rather than being cached at import, which is what makes a mid-run - repin observable. + routes rather than failing to start — and so the registered factory runs + per request rather than being cached at import, which is what makes a + mid-run repin observable. Refuses with `DoxbenchValidatorsNotRegistered`, naming the seam and its registration call, when nothing is registered. Otherwise raises whatever the factory raises (openxFactory's `ContractPinError` on an unreachable checkout, an absent schema, a digest mismatch, a manifest disagreement, or - a drifted `stack.yaml` ref). The caller (`_doxbench_validators`) turns any - of those into a fail-closed route refusal: "I could not read the contract" - is never an implicit pass.""" - factory = _doxbench_validators_factory + a drifted `stack.yaml` ref; openDox's own `ValidatorUnavailable` for a + packaged copy that fails its proof). The caller (`_doxbench_validators`) + turns any of those into a fail-closed route refusal: "I could not read the + contract" is never an implicit pass. + + Reading openDox's own default here is what closes its window: from this + call on, a host's registration over it is refused.""" + global _doxbench_validators_default_read + with _DOXBENCH_VALIDATORS_LOCK: + factory = _doxbench_validators_factory + if factory is not None and _doxbench_validators_is_default: + _doxbench_validators_default_read = True if factory is None: raise DoxbenchValidatorsNotRegistered(DOXBENCH_VALIDATORS_NOT_REGISTERED) return factory() diff --git a/src/opendox/validator.py b/src/opendox/validator.py index 16c97c56..a542b705 100644 --- a/src/opendox/validator.py +++ b/src/opendox/validator.py @@ -107,16 +107,33 @@ class in `x-rules`. A snapshot validator is REFUSED when that catalog names a never under a name or a time. So a changed byte is refused on the very call that sees it, as openxFactory's `doxbench_contracts.validators()` refuses one. +THE WORKBENCH MANIFEST'S TWO VALIDATOR RULES (plan 034's T085; RULED +`openxFactory#656` comment `5920216845`, item 2, *"Move into openDox's +validator (Recommended)"*). The `ideation-workbench` copy states two rules in +its own text and leaves them to its validator, because no JSON Schema keyword +states either: `recipe.pinned` says *"every pinned keyword MUST also appear in +`checked` -- this schema does not encode the subset constraint"*, and a +`recipe.new_candidates` entry is a document the set has not placed yet. The +copy carries no `x-rules` catalog, so this module OWNS the two rules +(`OWNED_RULES`), under ids it gives them, stating what must hold as its other +rules do: `pinned-keywords-are-checked` and `new-candidates-are-disjoint`. +T058 carried them in `default_projection`, under the consumer script's +identifiers. They live here now, and nothing answers to the old identifiers. +A copy that DOES carry a catalog has its rules from the catalog alone, so a +copy that carries one and is also owned rules here is refused when its +validator is built: one rule is enforced from one place. + jsonschema's SHAPE, FOR THE doxBench SEAM. `KindValidator.iter_errors()` yields violations whose `validator` (the failed keyword) and `absolute_path` read as a `jsonschema` error's do. Those are the two fields `serve_workbench`'s readers of the doxBench-validators seam read. -`validators()` answers one validator per wire kind: the model catalog's whole -document, and each chat-turn envelope's own `$defs` entry, as openxFactory's -`doxbench_contracts` builds them. So T085 can register it -(`serve_wire.register_doxbench_validators`). The doxBench kinds' semantic -rules are T085's. Here they are validated structurally, as openxFactory's -`validators()` validates them. +`doxbench_validators()` answers one validator per doxBench wire kind +(`DOXBENCH_KINDS`): the model catalog's whole document, and each chat-turn +envelope's own `$defs` entry, as openxFactory's `doxbench_contracts` builds +them. It is openDox's own default at that seam, which the entry points +register where no host has (plan 034's T085, R1Q10 (a); +`opendox.doxbench_defaults`). The doxBench kinds are validated structurally +here, as openxFactory's `validators()` validates them. IMPORT WEIGHT. The standard library, and `opendox.contracts`, whose record and copies are read with PyYAML only when a validator is built. It names no @@ -139,16 +156,19 @@ class in `x-rules`. A snapshot validator is REFUSED when that catalog names a __all__ = [ "DEPTH_RULE", "DIALECT", + "DOXBENCH_KINDS", "FORMATS", "KEYWORDS", "KINDS", "KIND_ENTRIES", "KindValidator", + "OWNED_RULES", "REFERENCE_RULES", "SchemaNotEvaluable", "UnknownKind", "ValidatorUnavailable", "Violation", + "doxbench_validators", "report", "validate", "validator_for", @@ -185,6 +205,16 @@ class in `x-rules`. A snapshot validator is REFUSED when that catalog names a #: The instance kinds, sorted. KINDS: tuple[str, ...] = tuple(sorted(KIND_ENTRIES)) +#: The doxBench WIRE kinds, sorted: the model catalog and the three chat-turn +#: envelopes, which the two model routes validate through the +#: doxBench-validators seam (`serve_wire`), as openxFactory's +#: `doxbench_contracts.WIRE_KINDS` names them. Each is held in openDox's +#: packaged copy of `xfactory-workbench-model-catalog` or +#: `xfactory-workbench-chat-turn` (R1Q12 (a)). +DOXBENCH_KINDS: tuple[str, ...] = tuple(sorted( + kind for kind, (copy_id, _pointer) in KIND_ENTRIES.items() + if copy_id in ("xfactory-workbench-chat-turn", "xfactory-workbench-model-catalog"))) + #: The keywords that can FAIL, and that this module evaluates. _ASSERTING = frozenset({ "const", "dependentRequired", "enum", "format", "maxItems", "maxLength", @@ -639,6 +669,98 @@ def _keyword_index_matches_topics(snap: Any) -> Iterator[Violation]: } +# --------------------------------------------------------------------------- +# the workbench manifest's two validator rules (plan 034's T085) +# --------------------------------------------------------------------------- + +def _names(value: Any) -> list[str]: + """A list's string entries, once each, in order. Anything else answers + none: its shape is the schema's to judge, and these rules only compare. + + Linear: the schema bounds none of the three lists, so membership is a + set's, and the list only keeps the order (Copilot at openDox-code#68 + 09cd1e8a, r4139734444).""" + if not isinstance(value, list): + return [] + names: list[str] = [] + seen: set[str] = set() + for item in value: + if isinstance(item, str) and item not in seen: + seen.add(item) + names.append(item) + return names + + +#: How many names a rule's detail quotes before it says how many more, and +#: how much of each name it quotes. +_QUOTED = 10 +_NAME_CHARS = 80 + + +def _quoted(names: list[str]) -> str: + """`names` as a detail quotes them: the first few, each cut to a readable + length, and a count of the rest, so one violation stays one readable line. + The schema bounds neither the lists nor their strings (Copilot at + openDox-code#68 21e4723f, r4139769791).""" + shown = repr([name if len(name) <= _NAME_CHARS else name[:_NAME_CHARS - 1] + "…" + for name in names[:_QUOTED]]) + return shown if len(names) <= _QUOTED else f"{shown[:-1]}, and {len(names) - _QUOTED} more]" + + +def _placed(entries: Any) -> set[str]: + """The `document` of each entry of a members or excluded list.""" + if not isinstance(entries, list): + return set() + return {entry["document"] for entry in entries + if isinstance(entry, dict) and isinstance(entry.get("document"), str)} + + +def _recipe(manifest: Any) -> dict[str, Any] | None: + recipe = manifest.get("recipe") if isinstance(manifest, dict) else None + return recipe if isinstance(recipe, dict) else None + + +def _pinned_keywords_are_checked(manifest: Any) -> Iterator[Violation]: + """Every `recipe.pinned` keyword is also in `recipe.checked`: a keyword + is pinned (required) only among the keywords that stratify the set.""" + recipe = _recipe(manifest) + if recipe is None: + return + checked = set(_names(recipe.get("checked"))) + stray = [name for name in _names(recipe.get("pinned")) if name not in checked] + if stray: + yield _broken("pinned-keywords-are-checked", ("recipe", "pinned"), + f"pinned keyword(s) {_quoted(stray)} are not in checked: " + "every pinned keyword MUST also be checked") + + +def _new_candidates_are_disjoint(manifest: Any) -> Iterator[Violation]: + """No `recipe.new_candidates` document is already a member or excluded: a + new candidate is a document the set has not placed yet.""" + recipe = _recipe(manifest) + if recipe is None: + return + placed = _placed(manifest.get("members")) | _placed(manifest.get("excluded")) + overlap = [name for name in _names(recipe.get("new_candidates")) if name in placed] + if overlap: + yield _broken("new-candidates-are-disjoint", ("recipe", "new_candidates"), + f"new_candidates {_quoted(overlap)} already appear in members " + "or excluded: a new candidate is a document the set has not " + "placed yet") + + +#: The rules this module OWNS, per packaged copy: rules a copy states in its own +#: text and leaves to its validator, because no JSON Schema keyword states them, +#: for a copy that carries no `x-rules` catalog (this module's docstring). Only +#: the workbench manifest's copy has any (RULED `5920216845`, item 2). +OWNED_RULES: Mapping[str, Mapping[str, Callable[[Any], Iterator[Violation]]]] = { + "ideation-workbench": { + "pinned-keywords-are-checked": _pinned_keywords_are_checked, + "new-candidates-are-disjoint": _new_candidates_are_disjoint, + }, +} + + # --------------------------------------------------------------------------- # a copy, compiled # --------------------------------------------------------------------------- @@ -772,9 +894,10 @@ class KindValidator: """The validator of one kind, built over one proved copy. `iter_errors(instance)` yields every `Violation`: the schema's, in the - order the schema is walked, and then the reference rules', in the - contract's catalog order. `violations()` lists them and `is_valid()` asks - whether there are none.""" + order the schema is walked, then the reference rules', in the contract's + catalog order, and then the rules this module owns for the copy + (`OWNED_RULES`), in their order. `violations()` lists them and + `is_valid()` asks whether there are none.""" def __init__(self, kind: str, copy_id: str, pointer: str, document: Any, digest: str) -> None: @@ -787,6 +910,7 @@ def __init__(self, kind: str, copy_id: str, pointer: str, document: Any, self._refuse_what_is_not_evaluated(document, pointer) self._entry = _at_pointer(document, pointer) # resolved, and a schema self._reference = self._reference_rules(document) + self._owned = self._owned_rules(document) # -- building ----------------------------------------------------------- @@ -929,6 +1053,20 @@ def _reference_rules(self, document: dict[str, Any] "is enforced only when both name it") return tuple(implemented[rule] for rule in declared) + def _owned_rules(self, document: dict[str, Any] + ) -> tuple[Callable[[Any], Iterator[Violation]], ...]: + """The rules this module owns for the copy (`OWNED_RULES`), in their + order. Refused for a copy that carries an `x-rules` catalog: the + catalog then states every rule the copy has, and a rule this module + also owned would be enforced from two places.""" + owned = OWNED_RULES.get(self.copy_id, {}) + if owned and "x-rules" in document: + raise self._not_evaluable( + f"it carries an x-rules catalog, and this module also owns the " + f"rules {sorted(owned)} for it; a copy with a catalog has its " + "rules from the catalog alone") + return tuple(owned.values()) + # -- evaluating --------------------------------------------------------- def iter_errors(self, instance: Any) -> Iterator[Violation]: @@ -948,6 +1086,8 @@ def iter_errors(self, instance: Any) -> Iterator[Violation]: "it is not judged valid") for check in self._reference: yield from check(instance) + for check in self._owned: + yield from check(instance) def violations(self, instance: Any) -> list[Violation]: return list(self.iter_errors(instance)) @@ -1217,6 +1357,18 @@ def validators() -> dict[str, KindValidator]: return {kind: validator_for(kind) for kind in KINDS} +def doxbench_validators() -> dict[str, KindValidator]: + """openDox's own validators for the doxBench wire kinds (`DOXBENCH_KINDS`), + every copy proved on this call: the factory openDox registers, as its + default, at the doxBench-validators seam (`serve_wire`; plan 034's T085, + R1Q10 (a) and R1Q12 (a)). The seam calls it once per request, so a copy + that changes is refused on the very request that reads it. A copy that + fails its proof raises `ValidatorUnavailable`, and the seam's reader turns + that into no validators, so both model routes refuse: no verdict is never + a pass. A fresh dict on every call.""" + return {kind: validator_for(kind) for kind in DOXBENCH_KINDS} + + def validate(instance: Any, *, kind: str | None = None) -> list[Violation]: """Every rule of `kind`'s contract that `instance` breaks. diff --git a/tests/test_doxbench_defaults.py b/tests/test_doxbench_defaults.py new file mode 100644 index 00000000..f492ceec --- /dev/null +++ b/tests/test_doxbench_defaults.py @@ -0,0 +1,487 @@ +"""openDox's own defaults for the two doxBench seams: plan 034's T085 (#1144's +4.3 as T007's batch G amends it, and 16.4 in part; R1Q10 (a) and R1Q12 (a), +`openxFactory#656` comment `5850003126`). + +T027 made two reaches into openxFactory seams that refuse, naming themselves, +when nothing is registered: the doxBench schema validators +(`serve_wire.register_doxbench_validators`) and the status-exemption rail +(`doxbench_packet.register_status_exemption`). Standalone, nothing registered +either, so a standalone server's `GET /workbench/model-catalog` answered `500 +catalog_unavailable` (measured at openDox-code `047bb4fa`). T085 gives each seam +an openDox default, `opendox.doxbench_defaults`, which the four entry points +register where no host has. This file holds: + +1. THE DEFAULTS THEMSELVES. The validators are `opendox.validator`'s own over + its packaged copies of the two doxBench schemas, one per wire kind, and they + meet the jsonschema-shaped contract the seam's readers in `serve_workbench` + read: openDox-spec's own examples conform, its negative ones do not, and + the buffers-floor reader tells the floor from any other fault. The rail + exempts nothing, and reports a document's own `Status:` line, raw. +2. THE SEAM RULES, for both seams. Nothing registered still refuses, naming + the seam and both calls (4.2). A default registers only where nothing is + registered. A host registered first is kept. A host registered after the + default and before it is read replaces it. After a read, a host is + refused, and the default stays. A malformed default is refused anyway. +3. THE ENTRY POINTS. `cli.build_parser()`, `cli.main()`, + `serve.build_server()` and `serve.main()` each register both defaults, and + each keeps a host registered first. Each runs in a fresh interpreter, so + no other case's registrations reach it. +4. THE FALSIFIER: THE SERVED CATALOG ROUTE ANSWERS STANDALONE. A + `python -m opendox.cli generate-and-open` child and a `python -m + opendox.serve` child, with neither sibling importable + (`tests/standalone_child.py`), each answer `GET /workbench/model-catalog` + 200 with an envelope openDox's own validator accepts. What that catalog + OFFERS is 16.4's and T081's: at this task's head it is the harness + declaration, `omp-local`, declared available, and T081 asserts that no + entry is available with no model configured. Plan 034's T085 box is ticked + when both hold, at T081's landing (the holder's ruling, 2026-10-02). + +Every in-process case starts with nothing registered at either seam, and puts +back exactly what it found, records included. Against a tree WITHOUT T085, +`opendox.doxbench_defaults` does not import and each case fails on its own +assertion rather than the file failing to collect, which keeps the red run +legible. + +A CREATED FILE: no carve-manifest row (RULED OQ-C). +""" + +from __future__ import annotations + +import http.client +import json +import re +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest +import yaml + +from opendox import contracts +from opendox import doxbench_model +from opendox import doxbench_packet as pk +from opendox import serve_wire +from opendox import validator as own +from opendox.serve_workbench import WorkbenchRoutes +from session_fixtures import GATE_TEST_PRINCIPALS +from standalone_child import Child, fresh_repository + +try: + from opendox import doxbench_defaults as defaults +except ImportError: # a tree without T085: each case fails on its own line + defaults = None + +ROOT = Path(__file__).resolve().parent.parent +EXAMPLES = ROOT / "tests" / "fixtures" / "spec-examples" +PLAIN = ROOT / "tests" / "fixtures" / "plain-documents" + +#: The doxBench wire kinds, as `serve_wire` spells them. +WIRE_KINDS = (serve_wire.DOXBENCH_MODEL_CATALOG_KIND, + serve_wire.DOXBENCH_CHAT_TURN_V2_KIND, + serve_wire.DOXBENCH_CHAT_TURN_V2_SUCCESS_KIND, + serve_wire.DOXBENCH_CHAT_TURN_V2_FAILURE_KIND) + +#: The packaged copies the four kinds are read from (R1Q12 (a)). +CHAT_COPIES = ("xfactory-workbench-chat-turn", "xfactory-workbench-model-catalog") + +#: Each seam's slots and calls, read through `getattr` so that against a tree +#: without T085 each case fails on its own assertion. +SEAMS = { + "validators": dict( + module=serve_wire, + slots=("_doxbench_validators_factory", "_doxbench_validators_is_default", + "_doxbench_validators_default_read"), + register="register_doxbench_validators", + register_default="register_default_doxbench_validators", + unregister="unregister_doxbench_validators", + registered="doxbench_validators_registered", + read=lambda: serve_wire.default_doxbench_validators(), + not_registered=serve_wire.DoxbenchValidatorsNotRegistered, + already=serve_wire.DoxbenchValidatorsAlreadyRegistered, + default=lambda: own.doxbench_validators, + host=lambda: (lambda: {"stand-in": True}), + malformed="not callable"), + "rail": dict( + module=pk, + slots=("_status_exemption_rail", "_status_exemption_is_default", + "_status_exemption_default_read"), + register="register_status_exemption", + register_default="register_default_status_exemption", + unregister="unregister_status_exemption", + registered="status_exemption_registered", + read=lambda: pk._status_exemption(), + not_registered=pk.StatusExemptionNotRegistered, + already=pk.StatusExemptionAlreadyRegistered, + default=lambda: defaults.NO_STATUS_EXEMPTION, + host=lambda: _HostRail(), + malformed=object()), +} + + +class _HostRail: + """A host's stand-in rail, exempting everything.""" + + def lifecycle_status(self, text): + return "host" + + def is_compression_exempt(self, text): + return True + + +@pytest.fixture(autouse=True) +def _empty_seams(): + """Nothing registered at either seam, and each PUT BACK exactly, records + included, so an entry point's default is never handed back as a host's.""" + held = {name: [getattr(seam["module"], slot, None) for slot in seam["slots"]] + for name, seam in SEAMS.items()} + for seam in SEAMS.values(): + getattr(seam["module"], seam["unregister"])() + yield + for name, seam in SEAMS.items(): + for slot, value in zip(seam["slots"], held[name]): + if hasattr(seam["module"], slot): + setattr(seam["module"], slot, value) + + +def _example(name: str): + return yaml.safe_load((EXAMPLES / name).read_text(encoding="utf-8")) + + +# --------------------------------------------------------------------------- +# 1 — the defaults themselves +# --------------------------------------------------------------------------- + +def test_the_validators_are_openDoxs_own_one_per_wire_kind() -> None: + assert own.DOXBENCH_KINDS == tuple(sorted(WIRE_KINDS)) + built = own.doxbench_validators() + assert sorted(built) == sorted(WIRE_KINDS) + pins = contracts.record() + for kind, validator in built.items(): + assert isinstance(validator, own.KindValidator), kind + assert validator.copy_id in CHAT_COPIES, kind + assert validator.digest == pins.copy(validator.copy_id).sha256, kind + assert own.doxbench_validators() is not built, "a fresh dict per call" + + +@pytest.mark.parametrize("name", sorted( + path.name for path in EXAMPLES.glob("workbench-*.example.yaml"))) +def test_openDox_specs_own_examples_conform_through_the_seams_reader(name) -> None: + """The reader the model routes call (`_doxbench_wire_conforms`) accepts + every doxBench example openDox-spec ships, judged by openDox's own + validators.""" + instance = _example(name) + assert WorkbenchRoutes._doxbench_wire_conforms( + own.doxbench_validators(), instance["kind"], instance), name + + +def test_a_malformed_envelope_does_not_conform_and_an_unknown_kind_has_no_verdict() -> None: + validators = own.doxbench_validators() + catalog = doxbench_model.catalog_wire_envelope(doxbench_model.EMPTY_CATALOG) + assert WorkbenchRoutes._doxbench_wire_conforms( + validators, serve_wire.DOXBENCH_MODEL_CATALOG_KIND, catalog) + assert not WorkbenchRoutes._doxbench_wire_conforms( + validators, serve_wire.DOXBENCH_MODEL_CATALOG_KIND, {**catalog, "models": "none"}) + assert not WorkbenchRoutes._doxbench_wire_conforms( + validators, "ideation-workbench", {"kind": "ideation-workbench"}) + + +def test_the_buffers_floor_reader_reads_openDoxs_violations() -> None: + """`_doxbench_violation_beside_the_buffers_floor` reads a violation's + `validator` keyword and `absolute_path`. Over openDox's validators, a + request whose only fault is an empty `buffers` reads as the floor alone, + and one with a second fault does not.""" + validators = own.doxbench_validators() + kind = serve_wire.DOXBENCH_CHAT_TURN_V2_KIND + request = _example("workbench-chat-turn-v2-loaded-set.example.yaml") + floor_only = {**request, "buffers": []} + assert not WorkbenchRoutes._doxbench_wire_conforms(validators, kind, floor_only) + assert WorkbenchRoutes._doxbench_violation_beside_the_buffers_floor( + validators, kind, floor_only) is False + assert WorkbenchRoutes._doxbench_violation_beside_the_buffers_floor( + validators, kind, {**floor_only, "client_turn_id": 7}) is True + + +def test_the_default_rail_exempts_nothing() -> None: + """There is no status exemption by default (batch G's addendum to 4.3), + whatever the document's status says.""" + rail = defaults.NO_STATUS_EXEMPTION + for status in ("ratified", "approved", "standard", "draft"): + assert rail.is_compression_exempt(f"# A doc\n\nStatus: {status}\n") is False + assert rail.EXEMPT_STATUSES == frozenset() + + +def test_the_default_rail_reports_a_documents_own_status_raw() -> None: + """So the packet's label (`Status: `, or `no Status: header`) stays + true of the document.""" + rail = defaults.NO_STATUS_EXEMPTION + assert rail.lifecycle_status("# T\n\nStatus: ratified (by a change) \n") == \ + "ratified (by a change)" + assert rail.lifecycle_status("# T\r\nStatus: draft\r\n") == "draft" + assert rail.lifecycle_status("# T\rStatus: draft\r") == "draft" + assert rail.lifecycle_status("# A doc with no status\n\nbody\n") is None + assert rail.lifecycle_status("x\n" * 15 + "Status: late\n") is None + assert rail.lifecycle_status("x\n" * 14 + "Status: last\n") == "last" + # only CR, LF and CRLF end a line, so a form feed or U+2028 does not + # start one + assert rail.lifecycle_status("# T\fStatus: hidden\n") is None + assert rail.lifecycle_status("# T
Status: hidden\n") is None + assert rail.lifecycle_status(None) is None + + +def test_the_assembler_marks_every_source_with_the_default_rail() -> None: + pk.register_default_status_exemption(defaults.NO_STATUS_EXEMPTION) + sources = [ + pk.PacketSource(ref="notes/a.md", kind=pk.SOURCE_EVIDENCE, + text="Status: ratified\n\nbody\n", status=None, + compression_exempt=True), + pk.PacketSource(ref="notes/b.md", kind=pk.SOURCE_EVIDENCE, + text="no header\n", status="stale", compression_exempt=True), + ] + marked = pk.exemption_rail(sources) + assert [(s.status, s.compression_exempt) for s in marked] == [ + ("ratified", False), (None, False)] + assert pk.EXEMPT_STATUSES == frozenset() + + +# --------------------------------------------------------------------------- +# 2 — the seam rules, for both seams +# --------------------------------------------------------------------------- + +def _call(seam, name, *args): + return getattr(seam["module"], seam[name])(*args) + + +def _held(seam): + return [getattr(seam["module"], slot) for slot in seam["slots"]] + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_nothing_registered_refuses_naming_the_seam_and_both_calls(name) -> None: + seam = SEAMS[name] + assert _call(seam, "registered") is False + with pytest.raises(seam["not_registered"]) as refused: + seam["read"]() + text = str(refused.value) + assert "opendox.doxbench_defaults.register_defaults()" in text + assert f"{seam['module'].__name__}.{seam['register']}(" in text + for entry_point in ("cli.build_parser()", "cli.main()", "serve.build_server()", + "serve.main()"): + assert entry_point in text, entry_point + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_register_defaults_registers_each_default_only_where_nothing_is(name) -> None: + seam = SEAMS[name] + defaults.register_defaults() + assert _held(seam) == [seam["default"](), True, False] + defaults.register_defaults() + assert _held(seam) == [seam["default"](), True, False], "a second call is a no-op" + assert _call(seam, "registered") is True + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_a_host_registered_first_is_kept(name) -> None: + seam = SEAMS[name] + host = seam["host"]() + _call(seam, "register", host) + defaults.register_defaults() + assert _held(seam) == [host, False, False] + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_a_host_replaces_the_default_until_it_is_read(name) -> None: + seam = SEAMS[name] + defaults.register_defaults() + host = seam["host"]() + assert _call(seam, "register", host) is host + assert _held(seam) == [host, False, False] + assert _call(seam, "register", host) is host, "the same host again is a no-op" + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_after_the_default_is_read_a_host_is_refused_and_the_default_stays(name) -> None: + seam = SEAMS[name] + defaults.register_defaults() + seam["read"]() + assert _held(seam) == [seam["default"](), True, True] + with pytest.raises(seam["already"], match=r"R1Q3 \(ii\)"): + _call(seam, "register", seam["host"]()) + assert _held(seam) == [seam["default"](), True, True] + _call(seam, "unregister") + assert _held(seam) == [None, False, False] + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_a_second_host_over_a_host_is_refused(name) -> None: + seam = SEAMS[name] + first = seam["host"]() + _call(seam, "register", first) + with pytest.raises(seam["already"]): + _call(seam, "register", seam["host"]()) + assert _held(seam)[0] is first + + +@pytest.mark.parametrize("name", sorted(SEAMS)) +def test_a_malformed_default_is_refused_whether_or_not_anything_is_registered(name) -> None: + seam = SEAMS[name] + with pytest.raises(TypeError, match="openDox's own"): + _call(seam, "register_default", seam["malformed"]) + _call(seam, "register", seam["host"]()) + with pytest.raises(TypeError, match="openDox's own"): + _call(seam, "register_default", seam["malformed"]) + + +def test_the_registered_default_validators_answer_through_the_seam() -> None: + defaults.register_defaults() + answered = serve_wire.default_doxbench_validators() + assert sorted(answered) == sorted(WIRE_KINDS) + assert all(validator.copy_id in CHAT_COPIES for validator in answered.values()) + + +# --------------------------------------------------------------------------- +# 3 — the entry points register both, and keep a host's +# --------------------------------------------------------------------------- + +_ENTRY_POINT = textwrap.dedent(""" + import json, sys + from pathlib import Path + from opendox import doxbench_defaults, doxbench_packet, serve_wire, validator + entry, host_first = sys.argv[1], sys.argv[2] == "host" + host_factory = lambda: {} + class HostRail: + def lifecycle_status(self, text): return None + def is_compression_exempt(self, text): return True + host_rail = HostRail() + if host_first: + serve_wire.register_doxbench_validators(host_factory) + doxbench_packet.register_status_exemption(host_rail) + from opendox import cli, serve + if entry == "cli.build_parser": + cli.build_parser() + elif entry in ("cli.main", "serve.main"): + try: + (cli.main if entry == "cli.main" else serve.main)(["--help"]) + except SystemExit: + pass + elif entry == "serve.build_server": + tmp = Path(sys.argv[3]) + (tmp / "web").mkdir() + (tmp / "snapshot.json").write_text("{}") + serve.build_server(tmp / "web", tmp / "snapshot.json", tmp, port=0).server_close() + factory = serve_wire._doxbench_validators_factory + rail = doxbench_packet._status_exemption_rail + print(json.dumps({ + "validators": ("host" if factory is host_factory else + "default" if factory is validator.doxbench_validators else repr(factory)), + "validators_is_default": serve_wire._doxbench_validators_is_default, + "rail": ("host" if rail is host_rail else + "default" if rail is doxbench_defaults.NO_STATUS_EXEMPTION else repr(rail)), + "rail_is_default": doxbench_packet._status_exemption_is_default, + })) + """) + +ENTRY_POINTS = ("cli.build_parser", "cli.main", "serve.build_server", "serve.main") + + +def _entry_point(tmp_path: Path, entry: str, host_first: bool) -> dict: + done = subprocess.run( + [sys.executable, "-c", _ENTRY_POINT, entry, "host" if host_first else "none", + str(tmp_path)], + capture_output=True, text=True, cwd=ROOT, timeout=120) + assert done.returncode == 0, done.stderr + return json.loads(done.stdout.strip().splitlines()[-1]) + + +@pytest.mark.parametrize("entry", ENTRY_POINTS) +def test_each_entry_point_registers_both_defaults_where_no_host_has(tmp_path, entry) -> None: + assert _entry_point(tmp_path, entry, host_first=False) == { + "validators": "default", "validators_is_default": True, + "rail": "default", "rail_is_default": True} + + +@pytest.mark.parametrize("entry", ENTRY_POINTS) +def test_each_entry_point_keeps_a_host_registered_first(tmp_path, entry) -> None: + assert _entry_point(tmp_path, entry, host_first=True) == { + "validators": "host", "validators_is_default": False, + "rail": "host", "rail_is_default": False} + + +# --------------------------------------------------------------------------- +# 4 — THE FALSIFIER: the served catalog route answers standalone +# --------------------------------------------------------------------------- + +_URL = re.compile(r"(http://([0-9.]+):([0-9]+))/index\.html$") + + +#: The console's human: one of the principals this suite declares +#: (`session_fixtures.GATE_TEST_PRINCIPALS`, the `XF_GATE_PRINCIPALS` roster the +#: child inherits), claimed with `--actor`, so the served console resolves it +#: and offers its session routes. +ACTOR = "tester" + + +def _repository(tmp_path: Path) -> Path: + """T050's fixture in a fresh repository (#1144's preamble).""" + return fresh_repository(PLAIN, tmp_path) + + +def _get(base: tuple[str, int], path: str, headers: dict | None = None): + connection = http.client.HTTPConnection(*base, timeout=30) + try: + connection.request("GET", path, headers=headers or {}) + response = connection.getresponse() + return response.status, response.read() + finally: + connection.close() + + +def served_catalog(child: Child) -> dict: + """The served model catalog, read as the chat rail reads it: the console + token from `/capabilities`, then `GET /workbench/model-catalog`. Asserts + the route ANSWERS, with an envelope openDox's own validator accepts.""" + assert ACTOR in GATE_TEST_PRINCIPALS + match = child.wait_for_line(_URL) + base = (match.group(2), int(match.group(3))) + status, body = _get(base, "/capabilities") + assert status == 200, status + capabilities = json.loads(body) + assert capabilities["actions"]["session"] is True, capabilities + status, body = _get(base, "/workbench/model-catalog", + {"X-XF-Console-Token": capabilities["console_token"]}) + assert status == 200, (status, body) + envelope = json.loads(body) + assert envelope["kind"] == serve_wire.DOXBENCH_MODEL_CATALOG_KIND + assert own.validate(envelope) == [], own.report(own.validate(envelope)) + assert child.interrupt() == 0, child.stderr_text() + assert child.refused() == [], child.refused() + return envelope + + +def test_the_served_catalog_route_answers_from_generate_and_open(tmp_path) -> None: + """`python -m opendox.cli generate-and-open`, the documented command, with + neither sibling importable. At `047bb4fa` it answered `500 + catalog_unavailable`.""" + child = Child(tmp_path, "opendox.cli", "generate-and-open", + "--repo-root", str(_repository(tmp_path)), "--repository", "fixture", + "--no-open", "--port", "0", "--run-dir", str(tmp_path / "run"), + "--actor", ACTOR) + try: + served_catalog(child) + finally: + child.kill() + + +def test_the_served_catalog_route_answers_from_serve_main(tmp_path) -> None: + """The server's own entry point, over a snapshot `generate` wrote.""" + repo = _repository(tmp_path) + out = tmp_path / "out" / "snapshot.json" + generated = Child(tmp_path, "opendox.cli", "generate", "--repo-root", str(repo), + "--repository", "fixture", "--output", str(out)) + assert generated.wait() == 0, generated.stderr_text() + child = Child(tmp_path, "opendox.serve", "--snapshot", str(out), + "--checkout-root", str(repo), "--port", "0", "--actor", ACTOR) + try: + served_catalog(child) + finally: + child.kill() diff --git a/tests/test_doxbench_seams.py b/tests/test_doxbench_seams.py index b715ad35..e0228d2c 100644 --- a/tests/test_doxbench_seams.py +++ b/tests/test_doxbench_seams.py @@ -72,10 +72,12 @@ #: Each seam's own functions, and the readers that resolve through it. VALIDATOR_SEAM_FUNCTIONS = ( - "register_doxbench_validators", "unregister_doxbench_validators", - "doxbench_validators_registered", "default_doxbench_validators") + "register_doxbench_validators", "register_default_doxbench_validators", + "unregister_doxbench_validators", "doxbench_validators_registered", + "default_doxbench_validators") RAIL_SEAM_FUNCTIONS = ( - "register_status_exemption", "unregister_status_exemption", + "register_status_exemption", "register_default_status_exemption", + "_require_a_rail", "unregister_status_exemption", "status_exemption_registered", "_status_exemption", "exemption_rail", "__getattr__") @@ -93,31 +95,38 @@ @pytest.fixture(autouse=True) def _empty_seams(): """Every test starts with NOTHING registered at either seam, and puts back - what it found. + EXACTLY what it found, records included. Both seams are process-global by design (ONE registration each, for the process), so a teardown that only unregistered would strip a registration - some other part of the process made at its start. + some other part of the process made at its start. And since plan 034's + T085 a seam's registration may be openDox's own DEFAULT, which an entry + point registered and a request may already have read, so the records go + back with it (`setattr`, never a host's `register()`): a default restored + as a host's would no longer give way to one. Read through `getattr` with a default, so that against a tree WITHOUT the seams each test fails on its own assertion rather than all of them erroring here, which is what makes this file's red run legible.""" found = { - "validators": (serve_wire, "_doxbench_validators_factory", - "unregister_doxbench_validators", - "register_doxbench_validators"), - "rail": (pk, "_status_exemption_rail", "unregister_status_exemption", - "register_status_exemption"), + "validators": (serve_wire, ("_doxbench_validators_factory", + "_doxbench_validators_is_default", + "_doxbench_validators_default_read"), + "unregister_doxbench_validators"), + "rail": (pk, ("_status_exemption_rail", "_status_exemption_is_default", + "_status_exemption_default_read"), + "unregister_status_exemption"), } - previous = {key: getattr(module, slot, None) - for key, (module, slot, _u, _r) in found.items()} - for module, _slot, unregister, _register in found.values(): + previous = {key: [(slot, getattr(module, slot)) for slot in slots + if hasattr(module, slot)] + for key, (module, slots, _u) in found.items()} + for module, _slots, unregister in found.values(): getattr(module, unregister, lambda: None)() yield - for key, (module, _slot, unregister, register) in found.items(): + for key, (module, _slots, unregister) in found.items(): getattr(module, unregister, lambda: None)() - if previous[key] is not None: - getattr(module, register)(previous[key]) + for slot, value in previous[key]: + setattr(module, slot, value) class _TrapModule(types.ModuleType): diff --git a/tests/test_post_render_validator.py b/tests/test_post_render_validator.py index f2772edf..3f8e2265 100644 --- a/tests/test_post_render_validator.py +++ b/tests/test_post_render_validator.py @@ -22,7 +22,12 @@ `search_from` change nothing. 4. The workbench manifest, read as YAML, with the two validator rules its schema leaves to the validator, and `workbench.save(validate=True)` over - them. + them. Since plan 034's T085 (RULED `openxFactory#656` comment + `5920216845`, item 2), the rules are `opendox.validator`'s own, as + `pinned-keywords-are-checked` and `new-candidates-are-disjoint`, and + `default_projection` checks neither. The identifiers T058 gave them appear + here only as the BEFORE-STATE (`RETIRED_RULE_IDS`), which nothing may + report. Every case starts with nothing registered at the four projection seams and puts back what it found. @@ -55,6 +60,17 @@ NEUTRAL = "opendox-snapshot" NOW = "2026-09-27T12:00:00Z" +#: The workbench manifest's two validator rules, as `opendox.validator` owns +#: them (T085). +PINNED_RULE = "pinned-keywords-are-checked" +CANDIDATE_RULE = "new-candidates-are-disjoint" + +#: THE BEFORE-STATE: the identifiers T058 carried the same two rules under, in +#: `default_projection.WORKBENCH_RULES` at openDox-code `047bb4fa`, as the +#: consumer's script named them. T085 keeps neither as an alias, so no report +#: may name either. +RETIRED_RULE_IDS = ("workbench-pinned-not-checked", "workbench-candidate-overlap") + _SINGLE_SEAMS = (ps.registry, ps.corpus_root, ps.writer) @@ -368,7 +384,7 @@ def test_the_rules_read_a_long_list_in_linear_time() -> None: names = [f"keyword-{index}" for index in range(50_000)] started = time.monotonic() - read = default_projection._names(names + names + [7, None]) + read = own._names(names + names + [7, None]) elapsed = time.monotonic() - started assert read == names assert elapsed < 5, f"{elapsed:.1f}s to read 100,002 entries" @@ -445,7 +461,7 @@ def test_a_pinned_keyword_that_is_not_checked_breaks_its_rule(tmp_path) -> None: _manifest(tmp_path, document)) assert result.outcome == ps.NOT_CONFORMANT assert result.stdout.splitlines() == [ - "[workbench-pinned-not-checked] /recipe/pinned: pinned keyword(s) ['worms'] " + f"[{PINNED_RULE}] /recipe/pinned: pinned keyword(s) ['worms'] " "are not in checked: every pinned keyword MUST also be checked", "1 violation(s) of the ideation-workbench contract, by " f"{result.validator}"] @@ -457,7 +473,7 @@ def test_a_rules_detail_quotes_a_few_names_and_counts_the_rest(tmp_path) -> None result = default_projection.VALIDATORS[workbench.KIND].validate( _manifest(tmp_path, document)) first = result.stdout.splitlines()[0] - assert first.startswith("[workbench-pinned-not-checked] /recipe/pinned: pinned " + assert first.startswith(f"[{PINNED_RULE}] /recipe/pinned: pinned " "keyword(s) ['k00', 'k01', ") assert "'k09', and 15 more] are not in checked" in first and "'k10'" not in first @@ -479,7 +495,7 @@ def test_a_new_candidate_already_placed_breaks_its_rule(tmp_path) -> None: _manifest(tmp_path, document)) assert result.outcome == ps.NOT_CONFORMANT assert result.stdout.splitlines()[0] == ( - "[workbench-candidate-overlap] /recipe/new_candidates: new_candidates " + f"[{CANDIDATE_RULE}] /recipe/new_candidates: new_candidates " "['notes/out.md', 'notes/in.md'] already appear in members or excluded: a " "new candidate is a document the set has not placed yet") @@ -492,8 +508,8 @@ def test_the_two_rules_are_judged_beside_the_schema_and_never_crash(tmp_path) -> result = default_projection.VALIDATORS[workbench.KIND].validate( _manifest(tmp_path, document)) rules = [line.split("]")[0][1:] for line in result.stdout.splitlines()[:-1]] - assert "workbench-pinned-not-checked" in rules - assert "workbench-candidate-overlap" not in rules + assert PINNED_RULE in rules + assert CANDIDATE_RULE not in rules assert {"type"} <= set(rules), rules @@ -516,5 +532,78 @@ def test_save_with_validate_keeps_a_valid_manifest_and_unwinds_a_broken_one(tmp_ broken.data["recipe"]["pinned"] = ["worms"] with pytest.raises(workbench.ManifestInvalid) as refused: workbench.save(broken, boundary, validate=True) - assert "[workbench-pinned-not-checked] /recipe/pinned:" in str(refused.value) + assert f"[{PINNED_RULE}] /recipe/pinned:" in str(refused.value) assert not (tmp_path / workbench.manifest_relpath("a broken set")).exists() + + +# --------------------------------------------------------------------------- +# 4a — the two rules are the validator's, under their new ids (T085) +# --------------------------------------------------------------------------- + +def _breaking_both() -> dict: + """A manifest that breaks each of the two rules once, and nothing else.""" + document = yaml.safe_load(_recipe_set().render()) + document["recipe"]["pinned"] = ["soil", "worms"] + document["recipe"]["new_candidates"] = ["notes/fresh.md", "notes/out.md"] + return document + + +def test_openDoxs_validator_reports_both_rules_under_their_new_ids() -> None: + """RULED `5920216845`, item 2: the rules are `opendox.validator`'s own, + reported by the validator itself, with no adapter in between.""" + assert tuple(own.OWNED_RULES[workbench.KIND]) == (PINNED_RULE, CANDIDATE_RULE) + found = own.validator_for(workbench.KIND).violations(_breaking_both()) + assert [(v.rule, v.path, v.keyword) for v in found] == [ + (PINNED_RULE, ("recipe", "pinned"), "reference"), + (CANDIDATE_RULE, ("recipe", "new_candidates"), "reference")] + assert own.validate(_breaking_both()) == found + assert own.report(found) == [ + f"[{PINNED_RULE}] /recipe/pinned: pinned keyword(s) ['worms'] are not in " + "checked: every pinned keyword MUST also be checked", + f"[{CANDIDATE_RULE}] /recipe/new_candidates: new_candidates " + "['notes/out.md'] already appear in members or excluded: a new candidate " + "is a document the set has not placed yet"] + assert own.validator_for(workbench.KIND).is_valid( + yaml.safe_load(_recipe_set().render())) + + +def test_no_report_names_a_retired_id_and_none_is_an_alias(tmp_path) -> None: + """The BEFORE-STATE: at `047bb4fa` the adapter reported these two rules as + `RETIRED_RULE_IDS`. Neither is an id any longer, nor an alias of one.""" + result = default_projection.VALIDATORS[workbench.KIND].validate( + _manifest(tmp_path, _breaking_both())) + assert result.outcome == ps.NOT_CONFORMANT + for retired in RETIRED_RULE_IDS: + assert retired not in result.stdout, result.stdout + assert retired not in own.OWNED_RULES[workbench.KIND] + assert all(retired not in rules for rules in own.REFERENCE_RULES.values()) + + +def test_default_projection_checks_neither_rule(tmp_path, monkeypatch) -> None: + """`default_projection` carries no copy of either rule: with the + validator's two taken away, its adapter reports nothing for a manifest + that breaks both. With them in place, it reports each exactly once, so an + adapter that checked them again beside the validator fails here too.""" + assert not hasattr(default_projection, "WORKBENCH_RULES") + assert not hasattr(default_projection.OwnValidator, "_workbench_rules") + path = _manifest(tmp_path, _breaking_both()) + lines = default_projection.VALIDATORS[workbench.KIND].validate(path).stdout.splitlines() + rules = [line.split("]")[0][1:] for line in lines[:-1]] + assert rules == [PINNED_RULE, CANDIDATE_RULE], lines + assert lines[-1].startswith("2 violation(s) of the ideation-workbench contract") + monkeypatch.setattr(own, "OWNED_RULES", {}) + monkeypatch.setattr(own, "_CACHE", {}) + bare = default_projection.VALIDATORS[workbench.KIND].validate(path) + assert (bare.ok, bare.outcome) == (True, ps.VALIDATED), bare.stdout + + +def test_a_copy_with_a_catalog_cannot_also_have_owned_rules() -> None: + """One rule is enforced from one place: a copy that carries an `x-rules` + catalog has its rules from it alone, so owned rules beside it are refused + when the validator is built, never enforced twice.""" + document = contracts.load(workbench.KIND) + copy_id, pointer = own.KIND_ENTRIES[workbench.KIND] + own.KindValidator(workbench.KIND, copy_id, pointer, document, "digest") + with pytest.raises(own.SchemaNotEvaluable, match="x-rules catalog"): + own.KindValidator(workbench.KIND, copy_id, pointer, + {**document, "x-rules": []}, "digest") From 83213eb2f6aa24ea45eefd700c05efe5effd7494 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:29:28 +0000 Subject: [PATCH 2/2] T085: answer the same rail under the status-exemption lock (Copilot r4169543783) register_status_exemption() and register_default_status_exemption() compared the rail with the registered one outside _STATUS_EXEMPTION_LOCK, so a teardown between the comparison and the return let a call report success over an emptied seam. Both now compare under the lock, as projection_seams._Seam.register() does (projection_seams.py:271-275), and probe a new rail outside it. The validators seam already compared under its lock. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/doxbench_packet.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/opendox/doxbench_packet.py b/src/opendox/doxbench_packet.py index 94a25ced..cd0b5849 100644 --- a/src/opendox/doxbench_packet.py +++ b/src/opendox/doxbench_packet.py @@ -302,8 +302,12 @@ def register_status_exemption(rail: object) -> object: refused once one has (R1Q3 (ii)).""" global _status_exemption_rail, _status_exemption_is_default global _status_exemption_default_read - if rail is not None and rail is _status_exemption_rail: - return rail + # The SAME rail is answered under the lock, as `projection_seams._Seam` + # answers it, so a teardown cannot empty the seam between the comparison + # and the return (Copilot at openDox-code#71 e0298cf4, r4169543783). + with _STATUS_EXEMPTION_LOCK: + if rail is not None and rail is _status_exemption_rail: + return rail _require_a_rail(rail, "register_status_exemption()", "the host's") with _STATUS_EXEMPTION_LOCK: held = _status_exemption_rail @@ -346,8 +350,9 @@ def register_default_status_exemption(rail: object) -> object: it is openDox's own defect.""" global _status_exemption_rail, _status_exemption_is_default global _status_exemption_default_read - if rail is not None and rail is _status_exemption_rail: - return rail + with _STATUS_EXEMPTION_LOCK: # as in `register_status_exemption()` + if rail is not None and rail is _status_exemption_rail: + return rail _require_a_rail(rail, "register_default_status_exemption()", "openDox's own") with _STATUS_EXEMPTION_LOCK: if _status_exemption_rail is None: