From a7c97bf59c672787b2be985db842b280ca865b3a Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:34:24 +0000 Subject: [PATCH 01/17] WIP T041 (park): health_contract.py, the finding copy at 7db9438b, the copy-record moves (plan 038) Work in progress, parked for an account swap. Not reviewed, not gated. Mutant pass run 1: 52 killed, 1 survived (names rule checking the pack only), 2 edits not applied; both get fixed next. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/contracts/__init__.py | 31 +- src/opendox/contracts/copies.yaml | 31 +- .../opendox-health-finding.schema.yaml | 396 ++++++ src/opendox/health_contract.py | 733 ++++++++++ tests/test_health_contract.py | 1180 +++++++++++++++++ tests/test_validator_input_set.py | 110 +- 6 files changed, 2429 insertions(+), 52 deletions(-) create mode 100644 src/opendox/contracts/schemas/opendox-health-finding.schema.yaml create mode 100644 src/opendox/health_contract.py create mode 100644 tests/test_health_contract.py diff --git a/src/opendox/contracts/__init__.py b/src/opendox/contracts/__init__.py index e114eb50..91010963 100644 --- a/src/opendox/contracts/__init__.py +++ b/src/opendox/contracts/__init__.py @@ -4,19 +4,27 @@ batch G amends on R1Q11 (a) and R1Q12 (a) (`openxFactory#656` comment `5850003126`): *"openDox's validator validates its spec leg's FOUR kinds ... The code leg carries digest-checked copies of the four, which a test holds to -the spec-leg commit the openDox root pins."* 7.1 also settles how the four +the spec-leg commit the openDox root pins."* 7.1 also settles how the copies reach an install. They ship as PACKAGE DATA, *"so `pip install openDox-code` 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 so do its doxBench validators +the validator's copies from here, and so do its doxBench validators (`opendox.validator.doxbench_validators`, the default T085 registers). +T005's batch Q adds to batch G's text on R2Q22 (a) (`6003486656`) and N-15 +(`6013547504`): openDox-spec's three health schemas travel as the four do, +and the finding's neutral shape is *"a copy the engine reads and NOT a +validator kind"*, because a finding's `kind` is its family and no `kind` +constant can name the schema. Plan 038's T041 carries that copy here, at the +spec-leg commit T060 pinned (`dox-v1.2`), and `opendox.health_contract` +mirrors it. T047 and T054 add the two file kinds after it. + WHAT IS HERE. -* `schemas/`: the four copies. Each one is byte for byte the spec leg's file - of the same name, `contracts/schemas/.schema.yaml` in - opensoft/openDox-spec. +* `schemas/`: the copies, the validator's four and the finding shape. Each one + is byte for byte the spec leg's file of the same name, + `contracts/schemas/.schema.yaml` in opensoft/openDox-spec. * `copies.yaml`: the record. It names the spec-leg commit the copies were taken at, and each copy's id, spec-leg path and sha256. @@ -29,7 +37,7 @@ recomputed digest can never equal an empty recorded one, so an empty digest is drift, and a file that merely exists proves nothing. -CONSUMED, NOT OWNED. openDox-spec owns these four schemas. The code leg +CONSUMED, NOT OWNED. openDox-spec owns these schemas. The code leg releases none of them, and a copy is changed in the spec leg and then copied here again, never edited here. The copies, the record, and the `commit` it names move together, in one commit. @@ -78,11 +86,13 @@ SCHEMA_DIR = "schemas" #: THE INPUT SET, as a record must hold it: openDox's own spec leg's four -#: schemas (7.1, as T007's batch G amends it). A record that names any other +#: validated schemas (7.1, as T007's batch G amends it), and the finding's +#: neutral shape, which the health engine reads and no validator kind names +#: (batch Q's addendum; N-15; plan 038 T041). A record that names any other #: copy, or leaves one of these out, is refused, like a record naming another #: leg. So no edit to the record lets a copy of `gate-intent` or of the #: possibles register (7.1b) be served, even with its file beside the others. -COPY_IDS = frozenset({"ideation-workbench", "opendox-snapshot", +COPY_IDS = frozenset({"ideation-workbench", "opendox-health-finding", "opendox-snapshot", "xfactory-workbench-chat-turn", "xfactory-workbench-model-catalog"}) _ID = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") @@ -214,8 +224,9 @@ def record() -> Record: copies.append(PackagedCopy(copy_id, path, digest)) recorded = {copy.id for copy in copies} if recorded != COPY_IDS: - raise _refuse(f"it records {sorted(recorded)}, not openDox's four, " - f"{sorted(COPY_IDS)} (7.1; 7.1b keeps every other schema out)") + raise _refuse(f"it records {sorted(recorded)}, not the copies openDox carries, " + f"{sorted(COPY_IDS)} (7.1, as batches G and Q amend it; 7.1b keeps " + "every other schema out)") return Record(SPEC_LEG, commit, tuple(copies)) diff --git a/src/opendox/contracts/copies.yaml b/src/opendox/contracts/copies.yaml index cff23db1..5c3414b7 100644 --- a/src/opendox/contracts/copies.yaml +++ b/src/opendox/contracts/copies.yaml @@ -11,17 +11,25 @@ # WHERE THE VALUES CAME FROM, read with `git show : | sha256sum` # in opensoft/openDox-spec. # -# * `commit` is the spec-leg commit the openDox root pins: T053 as landed, -# openDox-spec#16's squash f7ee3c76, which the root's `contracts/spec-pin.yaml` -# and `spec` gitlink name since opensoft/openDox#14 (dox-v1.1). It is the one -# commit that carries all four files. +# * `commit` is the spec-leg commit the openDox root pins: T040 as landed, +# openDox-spec#17's squash 7db9438b, which the root's `contracts/spec-pin.yaml` +# and `spec` gitlink name since opensoft/openDox#20 (plan 038 T060, +# dox-v1.2). It is the one commit that carries all five files. # * Each digest is the one the root's `contracts/manifest.yaml` records for that -# file at that commit. Three of the four files are unchanged from the root's -# previous spec pin, 8fe8c4c7. The fourth is T053's `opendox-snapshot`. +# file at that commit. Four of the five files are unchanged from the root's +# previous spec pin, f7ee3c76 (T053, dox-v1.1). The fifth is T040's +# `opendox-health-finding`, which plan 038's T041 copies here. # -# `commit` moved here from #16's head, cd49eb25, in lockstep with the root's -# spec pin. f7ee3c76 has cd49eb25's tree, so no digest moved. A digest moves -# only when its file changes. +# WHAT THE FIVE ARE (7.1, as T007's batch G amends it and T005's batch Q adds +# to it; N-15). Four are the schemas of the validator's kinds. The fifth is +# the finding's neutral shape: a copy the health engine reads, and no kind the +# validator validates, because a finding's `kind` is its family and no `kind` +# constant can name the schema. `opendox.health_contract` mirrors it. +# +# `commit` moved here from f7ee3c76 in lockstep with the root's spec pin (T041). +# openDox-spec#17 only added files, so no digest of the four moved. Before +# that, it moved from openDox-spec#16's head, cd49eb25, to its squash f7ee3c76, +# which has cd49eb25's tree. A digest moves only when its file changes. # # NEVER EDIT A COPY OR A DIGEST IN PLACE. A copy changes in the spec leg. It # arrives here when the spec leg's file is copied at the pinned commit and its @@ -29,11 +37,14 @@ schema_version: 1 kind: packaged-contract-copies spec_leg: opensoft/openDox-spec -commit: "f7ee3c763b3af4581daf1cd54406e5111e9358e6" +commit: "7db9438b4cc4446ab4e6ab5b552c220312deccc9" copies: - id: ideation-workbench path: contracts/schemas/ideation-workbench.schema.yaml sha256: "d30438491119c20928fbe4e85088fc33682829eeb6558d87dafce651000faafc" + - id: opendox-health-finding + path: contracts/schemas/opendox-health-finding.schema.yaml + sha256: "6fb9b29f23a4270fa0310624d9e59b5d82e9888a5168600671e90ed30f781016" - id: opendox-snapshot path: contracts/schemas/opendox-snapshot.schema.yaml sha256: "f9e3e111af1d4bd4c377c933027d81b582ae2b0a395b66f4e4621992454a584a" diff --git a/src/opendox/contracts/schemas/opendox-health-finding.schema.yaml b/src/opendox/contracts/schemas/opendox-health-finding.schema.yaml new file mode 100644 index 00000000..8ccdc5f6 --- /dev/null +++ b/src/opendox/contracts/schemas/opendox-health-finding.schema.yaml @@ -0,0 +1,396 @@ +# openDox's health finding: the neutral shape of ONE finding (plan 038, T040). +# +# WHY THIS FILE IS JSON. It is YAML whose body is one JSON object, as +# opendox-snapshot.schema.yaml is and for the same reason: this leg's required +# `validate` check installs pytest and nothing else, so +# `tests/test_opendox_health_contracts.py` reads it with Python's built-in +# `json` module once these comment lines are set aside, and proves that PyYAML +# reads the same object wherever PyYAML is installed. +# +# A SHAPE THE ENGINE READS, NOT A FILE KIND (decision N-15). An instance is one +# finding, as `opendox health list --json` emits one per array element. It +# carries no schema_version, and its `kind` is the finding's FAMILY +# (broken-link, orphan, ...), so no `kind` constant can name this schema, and +# openDox's validator validates no instance against it. openDox-code carries a +# digest-checked copy that its health contract module mirrors. The two file +# kinds beside it, opendox-health-packs and opendox-health-dispositions, are +# documents with a schema_version and a `kind` constant of their own. +# +# THE ID RULE (decision N-13, refined by review round 1's ADV-07): +# +# key = {"identity": , "kind": , "pack_id": , +# "path": }, written as JSON with its keys sorted at every +# level, no whitespace, and every character as itself, then encoded +# as UTF-8. In Python: +# json.dumps(key, sort_keys=True, separators=(",", ":"), +# ensure_ascii=False).encode("utf-8") +# h16 = the first 16 hex digits of sha256(key) +# id = pack_id + "." + kind + "." + h16 +# +# `identity` is the key the family supplies, and it is POSITION-INDEPENDENT: a +# link target as written, a pair of paths, a heading key, never a line number. +# The engine's own findings have the engine's own keys: {category, entry} for +# an install-level or pre-run finding, and {collided_id} for a collision, +# whatever the collision's path. +# `locator` is display only and outside the hash, so an edit above a finding +# moves its locator and keeps its id, and an exception keyed by that id keeps +# suppressing it. The engine sets the id, and a pack's own value is ignored. +# +# NO DOCUMENT TEXT (R2Q25 (a)). `evidence` holds locators only, and `message` +# is one line of the family's own words. Neither can carry more than 200 +# characters in one string, and no key of evidence or identity is named +# excerpt, text, content or quote. The finding is CLOSED: a field this schema +# does not name would be a channel around those bounds, so it is refused. +# +# TEXT UTF-8 CAN ENCODE. No string a finding carries holds a lone surrogate +# (U+D800 to U+DFFF). JSON can spell one (\ud800), and a file name that is not +# UTF-8 decodes to them, but UTF-8 cannot encode one, so the id could not be +# computed and the finding could not be stored or emitted. +# +# RULES. Every rule has an identifier, as in opendox-snapshot.schema.yaml. A +# subschema names in x-rule the rule its keywords enforce, and x-rules lists +# every rule with its class. A shape rule is enforced by this schema's own +# keywords. A reference rule is a cross-field rule that no JSON Schema keyword +# can state, so the reader enforces it. +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "opendox-health-finding.schema.yaml", + "title": "openDox health finding", + "contract_schema_version": 1, + "description": "One health finding, in the neutral shape openDox's health engine stores, lists and shows (plan 038 T040, on R2Q10 (a), R2Q22 (a) and R2Q25 (a): opensoft/openxFactory issue 656, comment 6003486656; decisions N-3, N-13, N-15 and OQ-H15-19, comment 6013547504). The product's own families and every manifest-listed pack return findings in this shape; the engine sets id, pack_id, pack_version and baseline_class, and a pack is never asked for them (15.7). The file's comment header, the field descriptions and the x-rules catalog say the rest.", + "x-rule": "finding-keys", + "type": "object", + "required": [ + "id", + "kind", + "pack_id", + "pack_version", + "path", + "identity", + "severity", + "resolution_class", + "message", + "evidence" + ], + "additionalProperties": false, + "properties": { + "id": { + "description": "pack_id, the family and h16, joined by dots (the file's comment header gives the rule). Set by the engine. It is a valid git ref-name component, so the fix draft for a finding is the branch health-fix-.", + "x-rule": "id-is-well-formed", + "type": "string", + "pattern": "^[a-z0-9-]+\\.[a-z0-9-]+\\.[0-9a-f]{16}$(?!\\n)" + }, + "kind": { + "description": "The family that raised the finding: broken-link, orphan, empty-stub, and the rest a family or pack declares. identity-collision is the engine's own (the x-rules catalog).", + "x-rule": "kind-is-a-family-name", + "type": "string", + "pattern": "^[a-z0-9-]+$(?!\\n)" + }, + "pack_id": { + "description": "The pack the finding is attributed to: opendox for the product's own families and for an install-level finding, otherwise the id of the manifest entry that launched the pack (15.7, OQ-H15-19). Stamped by the engine.", + "x-rule": "pack-id-is-a-name", + "type": "string", + "pattern": "^[a-z0-9-]+$(?!\\n)" + }, + "pack_version": { + "description": "The manifest entry's version, or for opendox the installed version (15.7, OQ-H15-18). Stamped by the engine. A family's own version, if it has one, rides in evidence as family_version.", + "x-rule": "pack-version-is-text", + "type": "string", + "minLength": 1, + "pattern": "^(?![\\s\\S]*[\\u0000-\\u001f\\u007f-\\u009f\\ud800-\\udfff])" + }, + "path": { + "description": "The document the finding is about, relative to the corpus root and separated by slashes. Empty for an install-level or pre-run finding, which names no document.", + "x-rule": "path-is-corpus-relative", + "type": "string", + "pattern": "^(?!/)(?![A-Za-z]:)(?![\\s\\S]*[\\\\\\u0000-\\u001f\\u007f-\\u009f\\ud800-\\udfff])(?!(?:[\\s\\S]*/)?\\.\\.(?:/|$))" + }, + "identity": { + "description": "The position-independent key the family supplies, which the id hashes: a link target as written, a pair of paths, a heading key. Empty when the family raises at most one finding per document. It holds no number, because a line number moves with every edit above it and a number has no one canonical JSON spelling across languages (1.0 and 1, 1e21). Its strings and keys are bounded as evidence's are, and hold no lone surrogate.", + "x-rule": "identity-is-an-object", + "type": "object", + "propertyNames": {"$ref": "#/$defs/identity_key"}, + "additionalProperties": {"$ref": "#/$defs/identity_value"} + }, + "locator": { + "description": "Display only, and outside the id: a line span, a link target as written, or both. A reader that shows the passage reads it from git when it renders (R2Q25 (a)).", + "x-rule": "locator-keys", + "type": "object", + "minProperties": 1, + "additionalProperties": false, + "dependentRequired": {"line_start": ["line_end"], "line_end": ["line_start"]}, + "properties": { + "line_start": {"$ref": "#/$defs/line"}, + "line_end": {"$ref": "#/$defs/line"}, + "target": { + "x-rule": "locator-target-is-short-text", + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^(?![\\s\\S]*[\\ud800-\\udfff])" + } + } + }, + "severity": { + "x-rule": "severity-is-known", + "enum": ["error", "warning", "info"] + }, + "resolution_class": { + "description": "The three classes, spelled exactly as 14.6 rules them, in the store, the CLI, the view and the pack contract alike.", + "x-rule": "resolution-class-is-one-of-three", + "enum": ["auto-fix", "assisted", "human-only"] + }, + "message": { + "description": "One line, written by the family in its own words, never document text (ADV-27): at most 200 characters, with no control character, no line or paragraph separator and no lone surrogate.", + "x-rule": "message-is-one-bounded-line", + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^(?![\\s\\S]*[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029\\ud800-\\udfff])" + }, + "evidence": { + "description": "Locators only, never document text (R2Q25 (a)): paths, line spans, link targets as written, digests, the family's own family_version, a refused patch's refused_patch and reason (15.2a). An empty object when the family has none. Every string in it, key or value, at any depth, is at most 200 characters with no lone surrogate, and no key is named excerpt, text, content or quote.", + "x-rule": "evidence-is-an-object", + "type": "object", + "propertyNames": {"$ref": "#/$defs/evidence_key"}, + "additionalProperties": {"$ref": "#/$defs/evidence_value"} + }, + "baseline_class": { + "description": "Set by the engine against the previous default-tip run (R2Q12 (a)). There is no fourth value (I-2 (a)).", + "x-rule": "baseline-class-is-known", + "enum": ["new", "pack-upgrade", "persistent"] + } + }, + "allOf": [ + { + "if": {"required": ["path"], "properties": {"path": {"const": ""}}}, + "then": { + "properties": { + "resolution_class": {"x-rule": "pathless-finding-is-human-only", "const": "human-only"} + } + } + }, + { + "if": { + "required": ["path"], + "properties": {"path": {"const": ""}, "kind": {"not": {"const": "identity-collision"}}} + }, + "then": { + "properties": { + "identity": { + "description": "An install-level or pre-run finding's identity is the engine's own: the failure's category, and the manifest entry it is about, or the empty string for none. A collision of two such findings has an empty path too, and keeps the collision's identity instead.", + "x-rule": "pathless-identity-is-category-and-entry", + "required": ["category", "entry"], + "additionalProperties": false, + "properties": { + "category": { + "x-rule": "pathless-identity-is-category-and-entry", + "type": "string", + "pattern": "^[a-z0-9-]+$(?!\\n)" + }, + "entry": { + "x-rule": "pathless-identity-is-category-and-entry", + "type": "string", + "pattern": "^(?:[a-z0-9-]+)?$(?!\\n)" + } + } + } + } + } + }, + { + "if": {"required": ["kind"], "properties": {"kind": {"const": "identity-collision"}}}, + "then": { + "properties": { + "resolution_class": {"x-rule": "identity-collision-is-human-only", "const": "human-only"}, + "identity": { + "description": "A collision finding's identity is the engine's own: the id the colliding findings shared.", + "x-rule": "collision-identity-is-the-collided-id", + "required": ["collided_id"], + "additionalProperties": false, + "properties": { + "collided_id": { + "x-rule": "collision-identity-is-the-collided-id", + "type": "string", + "pattern": "^[a-z0-9-]+\\.[a-z0-9-]+\\.[0-9a-f]{16}$(?!\\n)" + } + } + } + } + } + } + ], + "$defs": { + "line": { + "x-rule": "locator-line-is-a-line-number", + "type": "integer", + "minimum": 1 + }, + "identity_key": { + "allOf": [ + {"x-rule": "identity-names-no-text", "not": {"enum": ["excerpt", "text", "content", "quote"]}}, + {"x-rule": "identity-strings-are-short-text", "maxLength": 200, "pattern": "^(?![\\s\\S]*[\\ud800-\\udfff])"} + ] + }, + "identity_value": { + "allOf": [ + {"x-rule": "identity-holds-no-number", "type": ["string", "boolean", "null", "array", "object"]}, + {"x-rule": "identity-strings-are-short-text", "maxLength": 200, "pattern": "^(?![\\s\\S]*[\\ud800-\\udfff])"} + ], + "items": {"$ref": "#/$defs/identity_value"}, + "propertyNames": {"$ref": "#/$defs/identity_key"}, + "additionalProperties": {"$ref": "#/$defs/identity_value"} + }, + "evidence_key": { + "allOf": [ + {"x-rule": "evidence-names-no-text", "not": {"enum": ["excerpt", "text", "content", "quote"]}}, + {"x-rule": "evidence-strings-are-short-text", "maxLength": 200, "pattern": "^(?![\\s\\S]*[\\ud800-\\udfff])"} + ] + }, + "evidence_value": { + "x-rule": "evidence-strings-are-short-text", + "maxLength": 200, + "pattern": "^(?![\\s\\S]*[\\ud800-\\udfff])", + "items": {"$ref": "#/$defs/evidence_value"}, + "propertyNames": {"$ref": "#/$defs/evidence_key"}, + "additionalProperties": {"$ref": "#/$defs/evidence_value"} + } + }, + "x-rules": [ + { + "id": "finding-keys", + "class": "shape", + "says": "A finding is an object carrying id, kind, pack_id, pack_version, path, identity, severity, resolution_class, message and evidence, and optionally locator and baseline_class. It carries no other field." + }, + { + "id": "id-is-well-formed", + "class": "shape", + "says": "id is pack_id, the family and 16 lowercase hex digits, joined by dots: two names of lowercase letters, digits and hyphens, then the hash. It is a valid git ref-name component." + }, + { + "id": "kind-is-a-family-name", + "class": "shape", + "says": "kind, the family, is a name of lowercase letters, digits and hyphens." + }, + { + "id": "pack-id-is-a-name", + "class": "shape", + "says": "pack_id is a name of lowercase letters, digits and hyphens (OQ-H15-19): opendox for the product's own families, otherwise a manifest entry's id." + }, + { + "id": "pack-version-is-text", + "class": "shape", + "says": "pack_version is non-empty text with no control character (U+0000 to U+001F, U+007F to U+009F) and no lone surrogate (U+D800 to U+DFFF)." + }, + { + "id": "path-is-corpus-relative", + "class": "shape", + "says": "path is relative to the corpus root: it does not start with a slash or with a drive letter and a colon, holds no backslash, no control character and no lone surrogate, and has no .. segment. It is empty for an install-level or pre-run finding." + }, + { + "id": "identity-is-an-object", + "class": "shape", + "says": "identity is an object: the position-independent key the family supplies, empty when the family raises at most one finding per document." + }, + { + "id": "identity-holds-no-number", + "class": "shape", + "says": "No value in identity, at any depth, is a number: never a line number, and never a value whose canonical JSON differs between languages. Text, true, false, null, lists and objects are admitted." + }, + { + "id": "identity-strings-are-short-text", + "class": "shape", + "says": "Every string in identity, key or value, at any depth, is at most 200 characters and holds no lone surrogate (U+D800 to U+DFFF), which UTF-8 cannot encode and so the id could not hash." + }, + { + "id": "identity-names-no-text", + "class": "shape", + "says": "No key in identity, at any depth, is named excerpt, text, content or quote." + }, + { + "id": "locator-keys", + "class": "shape", + "says": "locator, when present, is an object carrying line_start and line_end together, target, or both, and no other key." + }, + { + "id": "locator-line-is-a-line-number", + "class": "shape", + "says": "locator.line_start and locator.line_end are whole numbers no less than 1." + }, + { + "id": "locator-target-is-short-text", + "class": "shape", + "says": "locator.target is non-empty text of at most 200 characters, with no lone surrogate (U+D800 to U+DFFF)." + }, + { + "id": "severity-is-known", + "class": "shape", + "says": "severity is one of error, warning and info." + }, + { + "id": "resolution-class-is-one-of-three", + "class": "shape", + "says": "resolution_class is one of auto-fix, assisted and human-only, spelled exactly so (14.6)." + }, + { + "id": "message-is-one-bounded-line", + "class": "shape", + "says": "message is one line of 1 to 200 characters, with no control character (U+0000 to U+001F, U+007F to U+009F) no line or paragraph separator (U+2028, U+2029) and no lone surrogate (U+D800 to U+DFFF). It is the family's own words, never document text." + }, + { + "id": "evidence-is-an-object", + "class": "shape", + "says": "evidence is an object of locators: paths, line spans, link targets as written, digests, family_version, and a refused patch's refused_patch and reason. It is empty when the family has none." + }, + { + "id": "evidence-strings-are-short-text", + "class": "shape", + "says": "Every string in evidence, key or value, at any depth, is at most 200 characters (R2Q25 (a)) and holds no lone surrogate (U+D800 to U+DFFF)." + }, + { + "id": "evidence-names-no-text", + "class": "shape", + "says": "No key in evidence, at any depth, is named excerpt, text, content or quote (R2Q25 (a))." + }, + { + "id": "baseline-class-is-known", + "class": "shape", + "says": "baseline_class, when present, is one of new, pack-upgrade and persistent (R2Q12 (a))." + }, + { + "id": "pathless-finding-is-human-only", + "class": "shape", + "says": "A finding whose path is empty, an install-level or pre-run finding, is human-only: it names no document a repair could edit." + }, + { + "id": "pathless-identity-is-category-and-entry", + "class": "shape", + "says": "A finding whose path is empty, other than an identity collision, has the engine's own identity, exactly {category, entry}: category is the failure's category, a name of lowercase letters, digits and hyphens, and entry is the id of the manifest entry it is about, or the empty string for none. So an install-level or pre-run finding keeps one id across runs. A collision keeps {collided_id}, whatever its path." + }, + { + "id": "identity-collision-is-human-only", + "class": "shape", + "says": "A finding of kind identity-collision, which the engine records against the family or pack whose findings shared one id, is human-only." + }, + { + "id": "collision-identity-is-the-collided-id", + "class": "shape", + "says": "A finding of kind identity-collision has the engine's own identity, exactly {collided_id}: the id the colliding findings shared." + }, + { + "id": "id-names-its-pack-and-kind", + "class": "reference", + "says": "The first two parts of id are the finding's own pack_id and kind." + }, + { + "id": "id-is-the-hash-of-its-key", + "class": "reference", + "says": "The last part of id is h16 of the finding's own identity, kind, pack_id and path, by the rule in this file's comment header. locator is outside the hash." + }, + { + "id": "locator-span-is-ordered", + "class": "reference", + "says": "locator.line_end is no less than locator.line_start." + } + ] +} diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py new file mode 100644 index 00000000..b6b7e215 --- /dev/null +++ b/src/opendox/health_contract.py @@ -0,0 +1,733 @@ +"""THE HEALTH CONTRACT: openDox's finding vocabulary, in the ONE module a check +pack may import (plan 038 T041, U-0; decision N-3; R2Q18 (a)). + +WHY THIS FILE EXISTS. Release 2's health engine judges a corpus with the +product's own families and with check packs that a manifest pins (#1144 +requirements 14 and 15). Each of them returns findings in one neutral shape, +which openDox-spec owns as `opendox-health-finding` (T040, landed as +openDox-spec `7db9438b`, bundled as `dox-v1.2` by T060; R2Q22 (a)). This module +states that shape in code: the three resolution classes, spelled exactly as +14.6 rules them (`auto-fix`, `assisted`, `human-only`), the severities, the +fields with `pack_id` and `pack_version` (15.7), the id rule, and the bounds. +openDox-code carries a digest-checked copy of the schema beside the validator +(`opendox.contracts`); this module MIRRORS that copy and never reads it, +because it may import the standard library alone. `tests/test_health_contract.py` +holds the two equal, constant for constant. + +THE STANDARD LIBRARY ONLY (R2Q18 (a); N-3). A pack runs in a sandbox that binds +the install's interpreter, its standard library and this one module, never +`site-packages` (T048). So this module imports nothing else, of openDox or of +anyone, and the test proves it three ways: by its source, in a fresh +interpreter, and run alone under `python -I -S`. + +THE ID RULE (N-13, refined by review round 1's ADV-07), as the copy's header +spells it: + + key = {"identity": , "kind": , "pack_id": , + "path": }, as + json.dumps(key, sort_keys=True, separators=(",", ":"), + ensure_ascii=False).encode("utf-8") + h16 = the first 16 hex digits of sha256(key) + id = pack_id + "." + kind + "." + h16 + +`identity` is the POSITION-INDEPENDENT key a family supplies (a link target as +written, a pair of paths, a heading key), never a line number. `locator` is +display only and outside the hash, so an edit above a finding moves its locator +and keeps its id, and an exception keyed by that id keeps suppressing it. The +ENGINE sets the id: `make_finding()` takes none, so a pack's own value never +reaches a finding. + +`identity` IS STORED AND EMITTED (the holder, `openxFactory#656` `6018624750`): +the engine hashes it into the id, T042 stores it as canonical sorted-key JSON +(`identity_json()`), and the list and the HTTP response emit it. So it is +bounded as the schema bounds it: no key named `excerpt`, `text`, `content` or +`quote`, no number, every string at most 200 characters, `{category, entry}` for +a pathless finding and `{collided_id}` for a collision. + +THE ENGINE'S BOUNDS, STRICTER THAN THE SCHEMA'S. The contract module owns the +field bounds, so the engine's caps are here, each tested at its boundary: + +* `NAME_MAX`: `pack_id` and `kind` are 1 to 40 characters, so `id` is at most + 98 and the fix branch `health-fix-` at most 109, far below a ref path + component's 255 bytes. The schema's patterns bound neither. +* `IDENTITY_MAX_BYTES`: the identity's canonical JSON, in UTF-8, is at most + 2048 bytes. The schema bounds each string and leaves the whole unbounded. + T042 stores what this cap admits, and T046 enforces it at the engine's + door; both cite this constant. +* `NESTING_MAX`: no value in `identity` or `evidence` nests more than 16 + objects and arrays deep, counting the field's own object. The walk below + never recurses, and this bound keeps `json.dumps`, which does, far from + Python's recursion limit. +* Every value is a JSON value: text keys, finite numbers, and no tuple, set or + bytes (`finding-is-json`). + +EVERY RULE HAS AN IDENTIFIER. A violation names the copy's own rule id (its +`x-rules` catalog: 24 shape rules and 3 reference rules) or one of the engine's +four (`ENGINE_RULES`). openDox's validator cannot build the finding schema: it +refuses a copy whose catalog names reference rules it does not implement +(`SchemaNotEvaluable`). The finding is no validator kind anyway (N-15), so all +27 rules are written here by hand, the three reference rules among them: +`id-names-its-pack-and-kind`, `id-is-the-hash-of-its-key` and +`locator-span-is-ordered`. As in openDox-spec's own test, a reference rule +judges only parts whose own rules hold, so one cause is reported once. + +A REFUSAL QUOTES NOTHING OF THE FINDING. A finding may carry document text where +it must not, which is why it is refused. So no violation's text holds any of +the finding's own strings: it names the rule, the place (a JSON pointer built +only from keys this module admitted) and a size, a type or the admitted set. +A finding's names and hashes, once admitted, may be named. + +THE ENGINE'S OWN IDENTITIES (contracts/health-finding.md § The id rule). +`pathless_identity()`, `collision_identity()` and `disappearance_identity()` +build the three keys the engine uses for its own findings, and `CATEGORIES` +fixes the failure categories of an install-level or pre-run finding, so each +such finding keeps one id across runs. + +T045 APPENDS the pack protocol to this module (the static declaration, the +stdout document and the patch type) at T041's landing, a cross-lane hand-off +(plan.md § single-writer files). + +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 copy +import hashlib +import json +import math +import re +from typing import Any, Iterator, Mapping, NamedTuple + +__all__ = [ + "ASSISTED", + "AUTO_FIX", + "BASELINE_CLASSES", + "CATEGORIES", + "ENGINE_FIELDS", + "ENGINE_RULES", + "FIELDS", + "FORBIDDEN_KEYS", + "FindingRefused", + "HASH_DIGITS", + "HUMAN_ONLY", + "IDENTITY_COLLISION", + "IDENTITY_MAX_BYTES", + "ID_MAX", + "NAME_MAX", + "NESTING_MAX", + "OPENDOX", + "OPTIONAL_FIELDS", + "REFERENCE_RULES", + "REQUIRED_FIELDS", + "RESOLUTION_CLASSES", + "SEVERITIES", + "SHAPE_RULES", + "STRING_MAX", + "UNCITED_DISAPPEARANCE", + "Violation", + "canonical_json", + "check_finding", + "collision_identity", + "disappearance_identity", + "finding_id", + "id_key", + "identity_json", + "make_finding", + "pathless_identity", + "to_json", + "violations", +] + +# --------------------------------------------------------------------------- +# the vocabulary +# --------------------------------------------------------------------------- + +#: The three resolution classes, spelled exactly as 14.6 rules them, in the +#: store, the CLI, the view and the pack contract alike. +AUTO_FIX = "auto-fix" +ASSISTED = "assisted" +HUMAN_ONLY = "human-only" +RESOLUTION_CLASSES: tuple[str, ...] = (AUTO_FIX, ASSISTED, HUMAN_ONLY) + +#: The severities, the schema's enum. Not `corpus_adapter.SEVERITIES`, which +#: adds `critical` for another surface. +SEVERITIES: tuple[str, ...] = ("error", "warning", "info") + +#: The baseline classes the engine sets (R2Q12 (a)). There is no fourth value +#: (I-2 (a)). +BASELINE_CLASSES: tuple[str, ...] = ("new", "pack-upgrade", "persistent") + +#: The pack id of the product's own families and of an install-level finding +#: (OQ-H15-19). +OPENDOX = "opendox" + +#: The kind of the engine's finding against a producer two of whose findings +#: in one run shared one id. +IDENTITY_COLLISION = "identity-collision" + +#: The kind of the engine's once-only re-raise of an uncited disappearance +#: (data-model.md § Baseline classes). +UNCITED_DISAPPEARANCE = "uncited-disappearance" + +#: A finding's fields, in the schema's order. +REQUIRED_FIELDS: tuple[str, ...] = ( + "id", "kind", "pack_id", "pack_version", "path", "identity", "severity", + "resolution_class", "message", "evidence") +OPTIONAL_FIELDS: tuple[str, ...] = ("locator", "baseline_class") +FIELDS: tuple[str, ...] = ( + "id", "kind", "pack_id", "pack_version", "path", "identity", "locator", + "severity", "resolution_class", "message", "evidence", "baseline_class") + +#: The fields the ENGINE sets; a family or pack is never asked for them (15.7). +ENGINE_FIELDS: tuple[str, ...] = ("id", "pack_id", "pack_version", "baseline_class") + +#: No key of `identity` or `evidence`, at any depth, is named one of these +#: (R2Q25 (a)). +FORBIDDEN_KEYS: tuple[str, ...] = ("excerpt", "text", "content", "quote") + +#: Every string of `identity`, `evidence` and `locator.target`, key or value, +#: and `message`, is at most this many characters (R2Q25 (a); ADV-27). +STRING_MAX = 200 + +#: The ENGINE's cap on `pack_id` and `kind`, stricter than the schema's +#: patterns, so a fix branch's name stays bounded. +NAME_MAX = 40 + +#: The id's hash: the first 16 hex digits of the key's SHA-256. +HASH_DIGITS = 16 + +#: The longest id: two names at the cap, the hash and two dots. +ID_MAX = 2 * NAME_MAX + HASH_DIGITS + 2 + +#: The ENGINE's cap on the identity's serialized size: its canonical JSON +#: (`identity_json()`), in UTF-8 bytes. The schema bounds each string and +#: leaves the whole unbounded, so this is stricter. The largest identity the +#: plan names, a pair of paths, fits in every UTF-8 width: two 200-character +#: strings of four-byte characters under two 40-character keys are 1693 +#: bytes. T042 stores what it admits, and T046 enforces it. +IDENTITY_MAX_BYTES = 2048 + +#: The ENGINE's bound on nesting in `identity` and `evidence`: objects and +#: arrays, the field's own object counted as the first. +NESTING_MAX = 16 + +#: The failure categories of an install-level or pre-run finding, whose +#: identity is the engine's own `{category, entry}` (contracts/health-finding.md +#: § The id rule; T041 fixes them with the id rule). `entry` is the manifest +#: entry's id, or "" for none. +CATEGORIES: tuple[str, ...] = ( + "no-sandbox", # no live sandbox, so no pack ran (R2Q16 (a)) + "entry-refused", # a manifest entry refused by name (15.1a) + "fetch-failed", # a git-URL source that could not be fetched (OQ-H15-14) + "digest-mismatch", # a pack tree whose digest is not its entry's (15.1a) + "declaration-refused", # a pack's `opendox-pack.yaml` refused (15.2; OQ-H15-10) + "pack-crashed", # a pack that exited non-zero or on a signal (15.6) + "pack-timed-out", # a pack that ran past its time budget (15.6) + "pack-bound-hit", # a pack that hit one of the engine's bounds (15.6) + "pack-output-refused", # a pack's stdout the engine refused (15.5, 15.6) +) + +#: The copy's catalog: the rules its own keywords state, and the three +#: cross-field rules no JSON Schema keyword can state. +SHAPE_RULES: tuple[str, ...] = ( + "finding-keys", "id-is-well-formed", "kind-is-a-family-name", + "pack-id-is-a-name", "pack-version-is-text", "path-is-corpus-relative", + "identity-is-an-object", "identity-holds-no-number", + "identity-strings-are-short-text", "identity-names-no-text", "locator-keys", + "locator-line-is-a-line-number", "locator-target-is-short-text", + "severity-is-known", "resolution-class-is-one-of-three", + "message-is-one-bounded-line", "evidence-is-an-object", + "evidence-strings-are-short-text", "evidence-names-no-text", + "baseline-class-is-known", "pathless-finding-is-human-only", + "pathless-identity-is-category-and-entry", "identity-collision-is-human-only", + "collision-identity-is-the-collided-id") +REFERENCE_RULES: tuple[str, ...] = ( + "id-names-its-pack-and-kind", "id-is-the-hash-of-its-key", + "locator-span-is-ordered") + +#: The engine's own rules, stricter than the copy's. +NAME_CAP = "name-is-at-most-40-characters" +SIZE_CAP = "identity-is-within-the-size-cap" +NESTING_CAP = "nesting-is-within-the-cap" +NOT_JSON = "finding-is-json" +ENGINE_RULES: tuple[str, ...] = (NAME_CAP, SIZE_CAP, NESTING_CAP, NOT_JSON) + +# The schema's patterns, read as Python's `fullmatch`, which admits no trailing +# newline (the schema's `$(?!\n)`). +_NAME = re.compile(r"[a-z0-9-]+") +_ID = re.compile(r"([a-z0-9-]+)\.([a-z0-9-]+)\.([0-9a-f]{16})") +_ENTRY = re.compile(r"(?:[a-z0-9-]+)?") +_DRIVE = re.compile(r"[A-Za-z]:") +_SURROGATE = re.compile("[\ud800-\udfff]") +_CONTROL = re.compile("[\x00-\x1f\x7f-\x9f\ud800-\udfff]") +_NOT_ONE_LINE = re.compile("[\x00-\x1f\x7f-\x9f

\ud800-\udfff]") + + +class Violation(NamedTuple): + """One rule a finding breaks: the rule's id, where (a JSON pointer into the + finding), and what was found. No part quotes the finding's own text.""" + + rule: str + where: str + detail: str + + def line(self) -> str: + return f"[{self.rule}] {self.where}: {self.detail}" + + +class FindingRefused(ValueError): + """A finding, or a part of one, breaks a rule of the contract. `rule` and + `where` are the first violation's; an engine records those, and never the + refused value.""" + + def __init__(self, violation: Violation, more: int = 0) -> None: + self.violation = violation + self.rule = violation.rule + self.where = violation.where + tail = f" (and {more} more)" if more else "" + super().__init__(f"the finding is refused: {violation.line()}{tail}") + + +# --------------------------------------------------------------------------- +# the id rule +# --------------------------------------------------------------------------- + +def canonical_json(value: Any) -> bytes: + """`value` as canonical JSON: sorted keys at every level, no whitespace, + each character as itself, in UTF-8. The id rule's spelling.""" + return json.dumps(value, sort_keys=True, separators=(",", ":"), + ensure_ascii=False).encode("utf-8") + + +def id_key(*, identity: Any, kind: str, pack_id: str, path: str) -> bytes: + """The bytes the id hashes (N-13; ADV-07). It checks nothing: + `finding_id()` checks the parts first.""" + return canonical_json({"identity": identity, "kind": kind, "pack_id": pack_id, + "path": path}) + + +def finding_id(*, identity: Any, kind: str, pack_id: str, path: str) -> str: + """The id the engine sets: `pack_id.kind.h16`. Refuses, with + `FindingRefused`, a key whose parts break their rules, so no id is ever + computed over a part the contract does not admit.""" + found = (list(_name(kind, "/kind", "kind-is-a-family-name")) + + list(_name(pack_id, "/pack_id", "pack-id-is-a-name")) + + list(_path(path)) + + list(_identity(identity, path=path, kind=kind))) + if found: + raise FindingRefused(found[0], len(found) - 1) + digest = hashlib.sha256(id_key(identity=identity, kind=kind, pack_id=pack_id, + path=path)).hexdigest() + return f"{pack_id}.{kind}.{digest[:HASH_DIGITS]}" + + +def identity_json(identity: Any) -> str: + """The identity as T042 stores it: its canonical JSON, after its own + bounds and the engine's cap. (The pathless and collision forms depend on + the finding's path and kind; `check_finding()` judges those.)""" + found = list(_identity(identity)) + if found: + raise FindingRefused(found[0], len(found) - 1) + return canonical_json(identity).decode("utf-8") + + +# --------------------------------------------------------------------------- +# the engine's own identities +# --------------------------------------------------------------------------- + +def pathless_identity(category: str, entry: str = "") -> dict[str, str]: + """An install-level or pre-run finding's identity: one of `CATEGORIES`, + and the manifest entry's id, or "" for none.""" + rule = "pathless-identity-is-category-and-entry" + if not isinstance(category, str) or category not in CATEGORIES: + raise FindingRefused(Violation( + rule, "/identity/category", + f"the category is not one of the engine's: {', '.join(CATEGORIES)}")) + if (not isinstance(entry, str) or not _ENTRY.fullmatch(entry) + or len(entry) > STRING_MAX): + raise FindingRefused(Violation( + rule, "/identity/entry", + "the entry is a manifest entry's id, of lowercase letters, digits and " + f"hyphens and at most {STRING_MAX} characters, or the empty string")) + return {"category": category, "entry": entry} + + +def collision_identity(collided_id: str) -> dict[str, str]: + """A collision finding's identity: the id the colliding findings shared.""" + found = list(_id(collided_id, "/identity/collided_id", + "collision-identity-is-the-collided-id")) + if found: + raise FindingRefused(found[0]) + return {"collided_id": collided_id} + + +def disappearance_identity(disappeared_id: str) -> dict[str, str]: + """The identity of the engine's re-raise of an uncited disappearance: the + original's id. The re-raise has its own id, never the original's.""" + found = list(_id(disappeared_id, "/identity/disappeared_id", "id-is-well-formed")) + if found: + raise FindingRefused(found[0]) + return {"disappeared_id": disappeared_id} + + +# --------------------------------------------------------------------------- +# the rules +# --------------------------------------------------------------------------- + +def _escape(key: str) -> str: + """One JSON-pointer reference token (RFC 6901).""" + return key.replace("~", "~0").replace("/", "~1") + + +def _type(value: Any) -> str: + """The JSON type of a value, for a refusal's detail.""" + if value is None: + return "null" + if isinstance(value, bool): + return "boolean" + if isinstance(value, (int, float)): + return "number" + if isinstance(value, str): + return "string" + if isinstance(value, list): + return "array" + if isinstance(value, dict): + return "object" + return f"a {type(value).__name__}, no JSON value" + + +def _text_problem(value: str) -> str | None: + if len(value) > STRING_MAX: + return f"is {len(value)} characters, above {STRING_MAX}" + if _SURROGATE.search(value): + return "holds a lone surrogate, which UTF-8 cannot encode" + return None + + +def _name(value: Any, where: str, rule: str) -> Iterator[Violation]: + if not isinstance(value, str) or not _NAME.fullmatch(value): + yield Violation(rule, where, "is not a name of lowercase letters, digits and hyphens" + f" ({_type(value)})") + elif len(value) > NAME_MAX: + yield Violation(NAME_CAP, where, f"is {len(value)} characters; the engine's cap is " + f"{NAME_MAX}, so a fix branch's name stays bounded") + + +def _id(value: Any, where: str, rule: str) -> Iterator[Violation]: + match = _ID.fullmatch(value) if isinstance(value, str) else None + if match is None: + yield Violation(rule, where, "is not a pack id, a family and 16 lowercase hex " + f"digits, joined by dots ({_type(value)})") + elif max(len(match.group(1)), len(match.group(2))) > NAME_MAX: + yield Violation(NAME_CAP, where, f"names a pack or a family above the engine's " + f"cap of {NAME_MAX} characters") + + +def _pack_version(value: Any) -> Iterator[Violation]: + if not isinstance(value, str) or not value or _CONTROL.search(value): + yield Violation("pack-version-is-text", "/pack_version", + "is not non-empty text free of control characters and lone " + f"surrogates ({_type(value)})") + + +def _path(value: Any) -> Iterator[Violation]: + if not isinstance(value, str): + yield Violation("path-is-corpus-relative", "/path", f"is not text ({_type(value)})") + return + if value.startswith("/") or _DRIVE.match(value): + problem = "starts at a root or a drive" + elif "\\" in value: + problem = "holds a backslash" + elif _CONTROL.search(value): + problem = "holds a control character or a lone surrogate" + elif ".." in value.split("/"): + problem = "has a .. segment" + else: + return + yield Violation("path-is-corpus-relative", "/path", problem) + + +def _walk(root: dict[Any, Any], where: str, field: str) -> Iterator[Violation]: + """Every rule `identity`'s or `evidence`'s content breaks, at any depth, + by an explicit stack and never by recursion. A refused key's value is not + entered, so no pointer carries a key this module refused.""" + names_rule = f"{field}-names-no-text" + strings_rule = f"{field}-strings-are-short-text" + stack: list[tuple[Any, str, int]] = [(root, where, 1)] + while stack: + value, at, level = stack.pop() + if isinstance(value, (dict, list)) and level > NESTING_MAX: + yield Violation(NESTING_CAP, at, f"nests deeper than the engine's {NESTING_MAX} " + "levels of objects and arrays") + elif isinstance(value, dict): + inner: list[tuple[Any, str, int]] = [] + for key, item in value.items(): + if not isinstance(key, str): + yield Violation(NOT_JSON, at, f"has a key that is not text ({_type(key)})") + elif key in FORBIDDEN_KEYS: + yield Violation(names_rule, at, f"has a key named {key}, which names " + "document text") + elif (problem := _text_problem(key)) is not None: + yield Violation(strings_rule, at, f"has a key that {problem}") + else: + inner.append((item, f"{at}/{_escape(key)}", level + 1)) + stack.extend(reversed(inner)) + elif isinstance(value, list): + stack.extend(reversed([(item, f"{at}/{index}", level + 1) + for index, item in enumerate(value)])) + elif isinstance(value, str): + if (problem := _text_problem(value)) is not None: + yield Violation(strings_rule, at, f"is a string that {problem}") + elif value is None or isinstance(value, bool): + continue + elif isinstance(value, (int, float)): + if field == "identity": + yield Violation("identity-holds-no-number", at, + "is a number, which an identity never holds") + elif isinstance(value, float) and not math.isfinite(value): + yield Violation(NOT_JSON, at, "is a number JSON cannot write") + elif isinstance(value, int) and value.bit_length() > 64: + try: + str(value) + except ValueError: + yield Violation(NOT_JSON, at, "is an integer too long to write as text") + else: + yield Violation(NOT_JSON, at, f"is {_type(value)}") + + +def _identity(value: Any, *, path: Any = None, kind: Any = None) -> Iterator[Violation]: + """Every rule the identity breaks: its own, the pathless or collision form + its finding's path and kind call for (when given), and the engine's size + cap, judged last and only over an identity every other rule admits.""" + if not isinstance(value, dict): + yield Violation("identity-is-an-object", "/identity", f"is {_type(value)}, not an " + "object") + return + found = list(_walk(value, "/identity", "identity")) + if kind == IDENTITY_COLLISION: + found += _collision_form(value) + elif path == "": + found += _pathless_form(value) + yield from found + if not found: + size = len(canonical_json(value)) + if size > IDENTITY_MAX_BYTES: + yield Violation(SIZE_CAP, "/identity", f"its canonical JSON is {size} bytes; the " + f"engine's cap is {IDENTITY_MAX_BYTES}") + + +def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: + rule = "pathless-identity-is-category-and-entry" + if set(identity) != {"category", "entry"}: + yield Violation(rule, "/identity", "a pathless finding's identity is exactly " + "{category, entry}, the engine's own") + return + if not isinstance(identity["category"], str) or not _NAME.fullmatch(identity["category"]): + yield Violation(rule, "/identity/category", "is not a name of lowercase letters, " + "digits and hyphens") + if not isinstance(identity["entry"], str) or not _ENTRY.fullmatch(identity["entry"]): + yield Violation(rule, "/identity/entry", "is neither a manifest entry's id nor the " + "empty string") + + +def _collision_form(identity: dict[Any, Any]) -> Iterator[Violation]: + rule = "collision-identity-is-the-collided-id" + if set(identity) != {"collided_id"}: + yield Violation(rule, "/identity", "a collision's identity is exactly " + "{collided_id}, the engine's own") + return + yield from _id(identity["collided_id"], "/identity/collided_id", rule) + + +def _line(value: Any) -> bool: + """A whole number no less than 1: JSON's integer, which 12.0 is too.""" + if isinstance(value, bool): + return False + if isinstance(value, int): + return value >= 1 + return isinstance(value, float) and value.is_integer() and value >= 1 + + +def _locator(value: Any) -> Iterator[Violation]: + if not isinstance(value, dict): + yield Violation("locator-keys", "/locator", f"is {_type(value)}, not an object") + return + keys = set(value) + if not keys or keys - {"line_start", "line_end", "target"}: + yield Violation("locator-keys", "/locator", "carries line_start and line_end " + "together, target, or both, and no other key") + elif ("line_start" in keys) != ("line_end" in keys): + yield Violation("locator-keys", "/locator", "carries line_start and line_end " + "together or neither") + for key in ("line_start", "line_end"): + if key in value and not _line(value[key]): + yield Violation("locator-line-is-a-line-number", f"/locator/{key}", + f"is not a whole number no less than 1 ({_type(value[key])})") + if "target" in value: + target = value["target"] + if not isinstance(target, str) or not target or _text_problem(target): + yield Violation("locator-target-is-short-text", "/locator/target", + f"is not text of 1 to {STRING_MAX} characters free of lone " + f"surrogates ({_type(target)})") + + +def _one_of(value: Any, allowed: tuple[str, ...], where: str, rule: str) -> Iterator[Violation]: + if not isinstance(value, str) or value not in allowed: + yield Violation(rule, where, f"is not one of {', '.join(allowed)}") + + +def _message(value: Any) -> Iterator[Violation]: + if (not isinstance(value, str) or not value or len(value) > STRING_MAX + or _NOT_ONE_LINE.search(value)): + size = f"{len(value)} characters" if isinstance(value, str) else _type(value) + yield Violation("message-is-one-bounded-line", "/message", + f"is not one line of 1 to {STRING_MAX} characters free of control " + f"characters, line separators and lone surrogates ({size})") + + +def _evidence(value: Any) -> Iterator[Violation]: + if not isinstance(value, dict): + yield Violation("evidence-is-an-object", "/evidence", f"is {_type(value)}, not an " + "object") + return + yield from _walk(value, "/evidence", "evidence") + + +def violations(finding: Any) -> list[Violation]: + """Every rule `finding` breaks, the copy's and the engine's, in the + schema's field order, then the reference rules. Empty when the finding + is admitted.""" + if not isinstance(finding, dict): + return [Violation("finding-keys", "", f"a finding is an object, not {_type(finding)}")] + found: list[Violation] = [] + if any(not isinstance(key, str) for key in finding): + found.append(Violation(NOT_JSON, "", "has a field name that is not text")) + missing = [field for field in REQUIRED_FIELDS if field not in finding] + unknown = [key for key in finding if isinstance(key, str) and key not in FIELDS] + if missing: + found.append(Violation("finding-keys", "", f"lacks {', '.join(missing)}")) + if unknown: + found.append(Violation("finding-keys", "", f"carries {len(unknown)} field(s) the " + f"finding does not name; it names {', '.join(FIELDS)}")) + checks: dict[str, Any] = { + "id": lambda v: _id(v, "/id", "id-is-well-formed"), + "kind": lambda v: _name(v, "/kind", "kind-is-a-family-name"), + "pack_id": lambda v: _name(v, "/pack_id", "pack-id-is-a-name"), + "pack_version": _pack_version, + "path": _path, + "identity": lambda v: _identity(v, path=finding.get("path"), + kind=finding.get("kind")), + "locator": _locator, + "severity": lambda v: _one_of(v, SEVERITIES, "/severity", "severity-is-known"), + "resolution_class": lambda v: _one_of(v, RESOLUTION_CLASSES, "/resolution_class", + "resolution-class-is-one-of-three"), + "message": _message, + "evidence": _evidence, + "baseline_class": lambda v: _one_of(v, BASELINE_CLASSES, "/baseline_class", + "baseline-class-is-known"), + } + broken: set[str] = set() + for field in FIELDS: + if field in finding: + mine = list(checks[field](finding[field])) + if mine: + broken.add(field) + found += mine + found += _conditional(finding, broken) + found += _references(finding, broken) + return found + + +def _conditional(finding: Mapping[str, Any], broken: set[str]) -> Iterator[Violation]: + """The class a pathless or collision finding must have. (The identity's + forms are judged with the identity.) Judged over a known class only, so a + class outside the three is reported once, by its own rule.""" + known = "resolution_class" in finding and "resolution_class" not in broken + if not known or finding["resolution_class"] == HUMAN_ONLY: + return + if finding.get("path") == "": + yield Violation("pathless-finding-is-human-only", "/resolution_class", + "a pathless finding names no document a repair could edit, so it " + "is human-only") + if finding.get("kind") == IDENTITY_COLLISION: + yield Violation("identity-collision-is-human-only", "/resolution_class", + "an identity collision is human-only") + + +def _references(finding: Mapping[str, Any], broken: set[str]) -> Iterator[Violation]: + """The three cross-field rules, each judged only over parts every rule of + their own admits.""" + def admitted(*fields: str) -> bool: + return all(field in finding and field not in broken for field in fields) + + if admitted("id", "kind", "pack_id"): + named_pack, named_kind, _h16 = finding["id"].split(".") + if (named_pack, named_kind) != (finding["pack_id"], finding["kind"]): + yield Violation("id-names-its-pack-and-kind", "/id", + f"names {named_pack} and {named_kind}, and the finding is " + f"{finding['pack_id']}'s {finding['kind']}") + if admitted("id", "kind", "pack_id", "path", "identity"): + want = hashlib.sha256(id_key(identity=finding["identity"], kind=finding["kind"], + pack_id=finding["pack_id"], path=finding["path"]) + ).hexdigest()[:HASH_DIGITS] + have = finding["id"].rsplit(".", 1)[1] + if have != want: + yield Violation("id-is-the-hash-of-its-key", "/id", + f"the hash is {have}, and the finding's key hashes to {want}") + locator = finding.get("locator") if "locator" not in broken else None + if isinstance(locator, dict) and "line_start" in locator and "line_end" in locator: + start, end = locator["line_start"], locator["line_end"] + if end < start: + yield Violation("locator-span-is-ordered", "/locator/line_end", + f"the span ends at {end:g}, before it starts at {start:g}") + + +def check_finding(finding: Any) -> None: + """Refuse `finding`, with `FindingRefused` naming its first violation, + unless it breaks no rule.""" + found = violations(finding) + if found: + raise FindingRefused(found[0], len(found) - 1) + + +# --------------------------------------------------------------------------- +# the engine's door, and the module's own serialization +# --------------------------------------------------------------------------- + +def make_finding(*, kind: str, pack_id: str, pack_version: str, path: str, + identity: dict[str, Any], severity: str, resolution_class: str, + message: str, evidence: dict[str, Any] | None = None, + locator: dict[str, Any] | None = None, + baseline_class: str | None = None) -> dict[str, Any]: + """One finding, its id SET by the id rule and checked whole. + + A family or pack supplies the fields it owns; the engine passes the + `pack_id` and `pack_version` it stamps (15.7) and any `baseline_class` it + sets. There is no `id` parameter: the id is the engine's, and a pack's + own value never reaches a finding. `evidence` defaults to `{}`, so every + finding carries it (14.5; FR-011). The result is the module's own copy, + in the schema's field order; `FindingRefused` names the first rule broken.""" + fid = finding_id(identity=identity, kind=kind, pack_id=pack_id, path=path) + finding: dict[str, Any] = {"id": fid, "kind": kind, "pack_id": pack_id, + "pack_version": pack_version, "path": path, + "identity": identity} + if locator is not None: + finding["locator"] = locator + finding.update(severity=severity, resolution_class=resolution_class, message=message, + evidence={} if evidence is None else evidence) + if baseline_class is not None: + finding["baseline_class"] = baseline_class + check_finding(finding) + # Copied after the check, which bounds the nesting the copy recurses into. + return copy.deepcopy(finding) + + +def to_json(finding: dict[str, Any]) -> str: + """The finding as this module serializes it: checked whole, then canonical + JSON (sorted keys, no whitespace, each character as itself). Nothing the + contract refuses is ever written.""" + check_finding(finding) + return canonical_json(finding).decode("utf-8") diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py new file mode 100644 index 00000000..e054a4fc --- /dev/null +++ b/tests/test_health_contract.py @@ -0,0 +1,1180 @@ +"""openDox's health contract module, `opendox.health_contract` (plan 038 T041, +U-0; decisions N-3, N-13, N-15 and OQ-H15-19; R2Q10 (a), R2Q18 (a), R2Q22 (a) +and R2Q25 (a); the holder's rulings `openxFactory#656` `6018624750` and +`6027706377` item 1). + +THE FALSIFIER, SPLIT BY LAYER (`6027706377` item 1). T041's falsifier asserts +the bound on THIS MODULE'S OWN serialized finding shape: what `to_json()` +writes, the one serialization the contract module owns. What `health list +--json` emits is T046's falsifier, and what the HTTP Health response emits is +T057's. Neither surface exists here, and nothing below pretends to test it. +The bound, on a finding's `identity`, is openDox-spec's finding schema's (T040, +landed as openDox-spec `7db9438b`): + +* no key named `excerpt`, `text`, `content` or `quote`, at any depth; +* no number, at any depth; +* every string, key or value, at most 200 characters; +* `{category, entry}` for a pathless finding, and `{collided_id}` for a + collision. + +The falsifier is section 1: every identity the contract names, built through +the module and serialized, holds the bound, and every identity outside it is +refused before it can be serialized. + +WHAT ELSE IT HOLDS. + +2. THE VOCABULARY IS THE PACKAGED COPY'S. The classes, the severities, the + baseline classes, the fields, the forbidden keys, the string bound and the + rule catalog are read from openDox-code's digest-checked copy of the + finding schema and compared with the module's constants. The module + mirrors the copy; it never reads it (it may import the standard library + alone, R2Q18 (a)). +3. THE ID RULE (N-13, refined by ADV-07). The key is exactly the four fields, + in canonical sorted-key JSON, UTF-8, each character as itself. Every id in + openDox-spec's five finding examples is the id the module computes, each + part of the key moves the id, and nothing outside the key does. +4. NAMES OF 1 TO 40 CHARACTERS. `pack_id` and `kind` are capped at 40, so a + fix branch's name stays bounded. 40 is admitted, 41 refused, and the + schema itself admits 41, so the cap is the engine's, stricter. +5. THE IDENTITY'S SERIALIZED-SIZE CAP, the engine's, stricter than the schema + (`IDENTITY_MAX_BYTES`, which T042 stores and T046 enforces): admitted at + its bound, refused one byte over, and an identity the schema admits is + refused by it. The nesting bound beside it. +6. THE BOUNDS ON `message` AND `evidence` (R2Q25 (a); ADV-27). +7. EVERY RULE THE COPY CATALOGUES IS ENFORCED. Each of its 27 rules has one + negative case, openDox-spec's own negative example rebuilt, refused for + that rule and no other; so are the module's four engine rules. +8. THE ENGINE'S OWN IDENTITIES AND CATEGORIES, and openDox-spec's five + examples rebuilt through `make_finding()`, id included. +9. THE CLOSURE: the module imports the standard library and nothing else. Its + imports are read from its source, it is imported in a fresh interpreter + that blocks openXdox and openxFactory, and it runs from a directory that + holds it alone, under `python -I -S`, with no site-packages: what the + sandbox binds (R2Q18 (a), T048). +10. A REFUSAL QUOTES NOTHING OF THE FINDING: no refused string, key or value + reaches a refusal's text. + +`--noconftest` SAFE. A CREATED file: no carve-manifest row (RULED OQ-C). +""" + +from __future__ import annotations + +import ast +import copy +import hashlib +import json +import math +import os +import re +import shutil +import subprocess +import sys +import textwrap +from pathlib import Path +from typing import Any, Iterator + +import pytest + +from opendox import contracts +from opendox import health_contract as hc + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "src" +MODULE = SRC / "opendox" / "health_contract.py" + +#: The packaged copy of openDox-spec's finding schema (T040), which the +#: module mirrors (N-15: a copy the engine reads, not a validator kind). +FINDING_COPY = "opendox-health-finding" + +#: The bound, as T040's schema and the holder's `6018624750` state it, written +#: here apart from the module so that a changed constant there fails here. +FORBIDDEN = ("excerpt", "text", "content", "quote") +STRING_MAX = 200 +NAME_MAX = 40 +IDENTITY_MAX_BYTES = 2048 + +#: openDox-spec's five finding examples at `7db9438b` +#: (`examples/health/opendox-health-finding-*.example.yaml`), as values. Each +#: id is the one openDox-spec's own test computes by the id rule. +EXAMPLES: dict[str, dict[str, Any]] = { + "broken-link": { + "id": "opendox.broken-link.546cd2aacb1a6738", + "kind": "broken-link", + "pack_id": "opendox", + "pack_version": "0.2.0", + "path": "notes/plan.md", + "identity": {"target": "../old/brief.md"}, + "locator": {"line_start": 12, "line_end": 12}, + "severity": "warning", + "resolution_class": "auto-fix", + "message": "link target does not exist; a unique file of that name exists elsewhere", + "evidence": {"candidate": "archive/brief.md", "family_version": "1"}, + "baseline_class": "new", + }, + "identity-collision": { + "id": "house-style.identity-collision.b722027596ad8c96", + "kind": "identity-collision", + "pack_id": "house-style", + "pack_version": "1.4.0", + "path": "notes/plan.md", + "identity": {"collided_id": "house-style.heading-case.0f1e2d3c4b5a6978"}, + "severity": "error", + "resolution_class": "human-only", + "message": ("two findings of one run share one id; the pack must give them " + "identity keys that differ"), + "evidence": {"collided_id": "house-style.heading-case.0f1e2d3c4b5a6978", + "count": 2}, + "baseline_class": "new", + }, + "no-sandbox": { + "id": "opendox.no-sandbox.5ccbb79891acb2e8", + "kind": "no-sandbox", + "pack_id": "opendox", + "pack_version": "0.2.0", + "path": "", + "identity": {"category": "no-sandbox", "entry": ""}, + "severity": "warning", + "resolution_class": "human-only", + "message": "no live sandbox on this install, so no check pack ran", + "evidence": {"probe": "bwrap", "verdict": "absent"}, + }, + "orphan": { + "id": "opendox.orphan.516a58a1fcc2c37f", + "kind": "orphan", + "pack_id": "opendox", + "pack_version": "0.2.0", + "path": "notes/appendix.md", + "identity": {}, + "severity": "info", + "resolution_class": "human-only", + "message": "nothing links to this document", + "evidence": {}, + "baseline_class": "persistent", + }, + "pack-upgrade": { + "id": "house-style.heading-case.a5db1a71f8b8340d", + "kind": "heading-case", + "pack_id": "house-style", + "pack_version": "1.4.0", + "path": "notes/caf\u00e9-menu.md", + "identity": {"heading": "seasonal-specials"}, + "locator": {"line_start": 3, "line_end": 3}, + "severity": "info", + "resolution_class": "assisted", + "message": "a heading is not in the house case", + "evidence": {"family_version": "2", "lines": [3, 3]}, + "baseline_class": "pack-upgrade", + }, +} + +#: openDox-spec's 201-character passage, from its negative examples. +PASSAGE = ("The old brief said the launch would wait for the second review, and that " + "the budget would be settled by the steering group before any vendor was " + "asked to quote; this sentence is document text, two hund") + + +def _example(name: str) -> dict[str, Any]: + return copy.deepcopy(EXAMPLES[name]) + + +def _rules(finding: Any) -> list[str]: + return [v.rule for v in hc.violations(finding)] + + +def _independent_id(pack_id: str, kind: str, path: str, identity: Any) -> str: + """The id rule as openDox-spec's own test spells it, apart from the + module.""" + key = json.dumps({"identity": identity, "kind": kind, "pack_id": pack_id, "path": path}, + sort_keys=True, separators=(",", ":"), ensure_ascii=False) + return f"{pack_id}.{kind}.{hashlib.sha256(key.encode('utf-8')).hexdigest()[:16]}" + + +def _walk(value: Any) -> Iterator[tuple[str, Any]]: + """Every (role, item) under `value`: role is "key" for an object's key, + "value" for anything else, at any depth.""" + stack = [value] + while stack: + item = stack.pop() + yield "value", item + if isinstance(item, dict): + for key, inner in item.items(): + yield "key", key + stack.append(inner) + elif isinstance(item, list): + stack.extend(item) + + +def _fields(finding: dict[str, Any]) -> dict[str, Any]: + """`make_finding()`'s arguments for an example: every field but the + engine's id.""" + return {k: v for k, v in finding.items() if k != "id"} + + +# --------------------------------------------------------------------------- +# 1. the falsifier: the module's own serialized identity holds the bound +# --------------------------------------------------------------------------- + +#: A pair of corpus paths, each at the 200-character string bound. +_PATH_A = "notes/" + "a" * 191 + ".md" +_PATH_B = "notes/" + "b" * 191 + ".md" + +#: Each identity the contract names, as (path, kind, identity, class). +_IDENTITIES: dict[str, tuple[str, str, Any, str]] = { + "a link target as written": ("notes/plan.md", "broken-link", + {"target": "../old/brief.md"}, "auto-fix"), + "a pair of paths": ("notes/plan.md", "near-duplicate", + {"first": _PATH_A, "second": _PATH_B}, "assisted"), + "a heading key": ("notes/caf\u00e9-menu.md", "heading-case", + {"heading": "seasonal-specials"}, "assisted"), + "every value an identity admits": ( + "notes/plan.md", "house-rule", + {"flags": [True, False, None], "nested": {"k": ["a", {"b": "c"}]}, + "long": "x" * 200, "y" * 200: "short", "empty": [], "none": None}, + "human-only"), + "an empty identity": ("notes/appendix.md", "orphan", {}, "human-only"), + "a pathless finding": ("", "no-sandbox", hc.pathless_identity("no-sandbox"), + "human-only"), + "a pathless finding about an entry": ( + "", "fetch-failed", hc.pathless_identity("fetch-failed", "house-style"), + "human-only"), + "a collision": ("notes/plan.md", "identity-collision", + hc.collision_identity("house-style.heading-case.0f1e2d3c4b5a6978"), + "human-only"), + "a pathless collision": ("", "identity-collision", + hc.collision_identity("opendox.no-sandbox.5ccbb79891acb2e8"), + "human-only"), + "an uncited disappearance": ( + "notes/plan.md", "uncited-disappearance", + hc.disappearance_identity("opendox.broken-link.546cd2aacb1a6738"), "human-only"), +} + + +def _build(name: str) -> dict[str, Any]: + path, kind, identity, resolution = _IDENTITIES[name] + return hc.make_finding(kind=kind, pack_id="opendox", pack_version="0.2.0", path=path, + identity=identity, severity="warning", + resolution_class=resolution, message="a finding") + + +@pytest.mark.parametrize("name", list(_IDENTITIES)) +def test_the_serialized_identity_holds_the_schemas_bound(name: str) -> None: + """THE FALSIFIER (T041, split by layer, `6027706377` item 1): the + identity this module writes holds exactly T040's bound. It is stored and + emitted with the finding, and the id is its hash (`6018624750`).""" + finding = _build(name) + emitted = json.loads(hc.to_json(finding)) + identity = emitted["identity"] + assert isinstance(identity, dict) + keys = [item for role, item in _walk(identity) if role == "key"] + values = [item for role, item in _walk(identity) if role == "value"] + # no key named excerpt, text, content or quote, at any depth + assert not set(keys) & set(FORBIDDEN), keys + # no number, at any depth (true and false are not numbers) + assert not [v for v in values + if isinstance(v, (int, float)) and not isinstance(v, bool)], values + # every string, key or value, at most 200 characters + strings = keys + [v for v in values if isinstance(v, str)] + assert strings == [] or max(len(s) for s in strings) <= STRING_MAX + # the engine's own keys + if emitted["kind"] == "identity-collision": + assert sorted(identity) == ["collided_id"] + elif emitted["path"] == "": + assert sorted(identity) == ["category", "entry"] + # stored and emitted, and hashed into the id + assert identity == _IDENTITIES[name][2] + assert emitted["id"] == _independent_id(emitted["pack_id"], emitted["kind"], + emitted["path"], identity) + assert hc.identity_json(identity) == json.dumps( + identity, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + + +_REFUSED_IDENTITIES = [ + *[({word: "a"}, "notes/plan.md", "broken-link", "identity-names-no-text") + for word in FORBIDDEN], + *[({"a": {word: "b"}}, "notes/plan.md", "broken-link", "identity-names-no-text") + for word in FORBIDDEN], + *[({"a": [{word: "b"}]}, "notes/plan.md", "broken-link", "identity-names-no-text") + for word in FORBIDDEN], + ({"target": "../old/brief.md", "line": 12}, "notes/plan.md", "broken-link", + "identity-holds-no-number"), + ({"a": 1.5}, "notes/plan.md", "broken-link", "identity-holds-no-number"), + ({"a": 0}, "notes/plan.md", "broken-link", "identity-holds-no-number"), + ({"a": -3}, "notes/plan.md", "broken-link", "identity-holds-no-number"), + ({"a": [True, 1]}, "notes/plan.md", "broken-link", "identity-holds-no-number"), + ({"a": {"b": 1e21}}, "notes/plan.md", "broken-link", "identity-holds-no-number"), + ({"target": PASSAGE}, "notes/plan.md", "broken-link", + "identity-strings-are-short-text"), + ({"a" * 201: "b"}, "notes/plan.md", "broken-link", "identity-strings-are-short-text"), + ({"a": ["x" * 201]}, "notes/plan.md", "broken-link", "identity-strings-are-short-text"), + ({"a": {"b": "x" * 201}}, "notes/plan.md", "broken-link", + "identity-strings-are-short-text"), + ({"a": {"b" * 201: "x"}}, "notes/plan.md", "broken-link", + "identity-strings-are-short-text"), + (["../old/brief.md"], "notes/plan.md", "broken-link", "identity-is-an-object"), + ("../old/brief.md", "notes/plan.md", "broken-link", "identity-is-an-object"), + ({}, "", "no-sandbox", "pathless-identity-is-category-and-entry"), + ({"category": "no-sandbox"}, "", "no-sandbox", + "pathless-identity-is-category-and-entry"), + ({"entry": ""}, "", "no-sandbox", "pathless-identity-is-category-and-entry"), + ({"category": "no-sandbox", "entry": "", "detail": "x"}, "", "no-sandbox", + "pathless-identity-is-category-and-entry"), + ({"category": "No_Sandbox", "entry": ""}, "", "no-sandbox", + "pathless-identity-is-category-and-entry"), + ({"category": "no-sandbox", "entry": "House"}, "", "no-sandbox", + "pathless-identity-is-category-and-entry"), + ({"target": "../old/brief.md"}, "", "broken-link", + "pathless-identity-is-category-and-entry"), + ({"id": "house-style.heading-case.0f1e2d3c4b5a6978"}, "notes/plan.md", + "identity-collision", "collision-identity-is-the-collided-id"), + ({"collided_id": "house-style.heading-case.0f1e2d3c4b5a6978", "count": "2"}, + "notes/plan.md", "identity-collision", "collision-identity-is-the-collided-id"), + ({"collided_id": "not an id"}, "notes/plan.md", "identity-collision", + "collision-identity-is-the-collided-id"), + ({"category": "no-sandbox", "entry": ""}, "", "identity-collision", + "collision-identity-is-the-collided-id"), +] + + +@pytest.mark.parametrize("identity, path, kind, rule", _REFUSED_IDENTITIES) +def test_an_identity_outside_the_bound_is_never_serialized( + identity: Any, path: str, kind: str, rule: str) -> None: + """The other half of the falsifier: an identity that breaks the bound is + refused by `make_finding()`, and a finding assembled by hand around it, + with the right id, is refused by `to_json()`. So no serialization this + module writes can carry it.""" + with pytest.raises(hc.FindingRefused) as refused: + hc.make_finding(kind=kind, pack_id="opendox", pack_version="0.2.0", path=path, + identity=identity, severity="info", resolution_class="human-only", + message="a finding") + assert refused.value.rule == rule, refused.value + by_hand = {"id": _independent_id("opendox", kind, path, identity), "kind": kind, + "pack_id": "opendox", "pack_version": "0.2.0", "path": path, + "identity": identity, "severity": "info", "resolution_class": "human-only", + "message": "a finding", "evidence": {}} + with pytest.raises(hc.FindingRefused) as again: + hc.to_json(by_hand) + assert again.value.rule == rule, again.value + assert rule in _rules(by_hand) + + +# --------------------------------------------------------------------------- +# 2. the vocabulary is the packaged copy's +# --------------------------------------------------------------------------- + +def test_the_vocabulary_is_the_packaged_copys() -> None: + """The module mirrors the digest-checked copy of T040's schema, constant + for constant, and 14.6's three classes are spelled exactly.""" + schema = contracts.load(FINDING_COPY) + props = schema["properties"] + defs = schema["$defs"] + assert hc.RESOLUTION_CLASSES == tuple(props["resolution_class"]["enum"]) == ( + "auto-fix", "assisted", "human-only") + assert (hc.AUTO_FIX, hc.ASSISTED, hc.HUMAN_ONLY) == hc.RESOLUTION_CLASSES + assert hc.SEVERITIES == tuple(props["severity"]["enum"]) == ("error", "warning", "info") + assert hc.BASELINE_CLASSES == tuple(props["baseline_class"]["enum"]) == ( + "new", "pack-upgrade", "persistent") + assert hc.REQUIRED_FIELDS == tuple(schema["required"]) + assert hc.REQUIRED_FIELDS + hc.OPTIONAL_FIELDS == tuple( + field for field in props if field in schema["required"]) + tuple( + field for field in props if field not in schema["required"]) + assert hc.FIELDS == tuple(props) + assert set(hc.ENGINE_FIELDS) == {"id", "pack_id", "pack_version", "baseline_class"} + for name in ("identity_key", "evidence_key"): + assert tuple(defs[name]["allOf"][0]["not"]["enum"]) == hc.FORBIDDEN_KEYS == FORBIDDEN + assert defs[name]["allOf"][1]["maxLength"] == hc.STRING_MAX == STRING_MAX + assert defs["identity_value"]["allOf"][1]["maxLength"] == STRING_MAX + assert defs["evidence_value"]["maxLength"] == STRING_MAX + assert props["message"]["maxLength"] == STRING_MAX + assert props["locator"]["properties"]["target"]["maxLength"] == STRING_MAX + assert hc.OPENDOX == "opendox" + assert hc.IDENTITY_COLLISION == "identity-collision" + assert hc.UNCITED_DISAPPEARANCE == "uncited-disappearance" + + +def test_the_rule_catalog_is_the_copys_plus_the_engines_own() -> None: + schema = contracts.load(FINDING_COPY) + catalog = {rule["id"]: rule["class"] for rule in schema["x-rules"]} + assert len(catalog) == 27 + assert set(hc.SHAPE_RULES) == {r for r, c in catalog.items() if c == "shape"} + assert set(hc.REFERENCE_RULES) == {r for r, c in catalog.items() if c == "reference"} == { + "id-is-the-hash-of-its-key", "id-names-its-pack-and-kind", "locator-span-is-ordered"} + assert len(hc.SHAPE_RULES) == len(set(hc.SHAPE_RULES)) == 24 + assert set(hc.ENGINE_RULES) == {"name-is-at-most-40-characters", + "identity-is-within-the-size-cap", + "nesting-is-within-the-cap", "finding-is-json"} + assert not set(hc.ENGINE_RULES) & set(catalog) + + +def test_the_id_rule_is_the_one_the_copys_header_spells() -> None: + """The copy's header gives the canonical JSON in Python; the module's key + is those bytes.""" + header = contracts.verified_bytes(FINDING_COPY).decode("utf-8").split("{\n", 1)[0] + assert 'json.dumps(key, sort_keys=True, separators=(",", ":"),' in header + assert 'ensure_ascii=False).encode("utf-8")' in header + finding = _example("broken-link") + key = {"identity": finding["identity"], "kind": finding["kind"], + "pack_id": finding["pack_id"], "path": finding["path"]} + assert hc.id_key(identity=finding["identity"], kind=finding["kind"], + pack_id=finding["pack_id"], path=finding["path"]) == json.dumps( + key, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + + +# --------------------------------------------------------------------------- +# 3. the id rule +# --------------------------------------------------------------------------- + +def test_the_key_is_exactly_the_contracts() -> None: + finding = _example("broken-link") + key = hc.id_key(identity=finding["identity"], kind=finding["kind"], + pack_id=finding["pack_id"], path=finding["path"]) + assert key == (b'{"identity":{"target":"../old/brief.md"},"kind":"broken-link",' + b'"pack_id":"opendox","path":"notes/plan.md"}') + assert hashlib.sha256(key).hexdigest()[:16] == "546cd2aacb1a6738" + + +@pytest.mark.parametrize("name", sorted(EXAMPLES)) +def test_every_example_id_is_the_id_the_module_computes(name: str) -> None: + """openDox-spec's five examples, each id computed by its own test, are the + ids this module computes, and each example breaks no rule here.""" + finding = _example(name) + assert hc.finding_id(identity=finding["identity"], kind=finding["kind"], + pack_id=finding["pack_id"], path=finding["path"]) == finding["id"] + assert hc.violations(finding) == [] + hc.check_finding(finding) + + +@pytest.mark.parametrize("field, value", [ + ("identity", {"target": "../old/brief-2.md"}), + ("kind", "orphan"), + ("pack_id", "house-style"), + ("path", "notes/plan-2.md"), +]) +def test_each_part_of_the_key_moves_the_id(field: str, value: Any) -> None: + """A hash that ignored any one of the four parts would leave the id + unmoved here.""" + finding = _example("broken-link") + fields = {"identity": finding["identity"], "kind": finding["kind"], + "pack_id": finding["pack_id"], "path": finding["path"]} + moved = hc.finding_id(**{**fields, field: value}) + assert moved != finding["id"] + assert moved.rsplit(".", 1)[1] != finding["id"].rsplit(".", 1)[1] + assert moved == _independent_id(**{**fields, field: value}) + changed = {**finding, field: value} + assert set(_rules(changed)) & {"id-is-the-hash-of-its-key", "id-names-its-pack-and-kind"} + + +@pytest.mark.parametrize("field, value", [ + ("locator", {"line_start": 40, "line_end": 41}), + ("locator", None), + ("severity", "error"), + ("message", "another message"), + ("evidence", {}), + ("pack_version", "9.9.9"), + ("baseline_class", "persistent"), + ("resolution_class", "assisted"), +]) +def test_nothing_outside_the_key_moves_the_id(field: str, value: Any) -> None: + """An edit above a finding moves its locator and keeps its id, so an + exception keyed by that id keeps suppressing it (N-13, ADV-07).""" + fields = _fields(_example("broken-link")) + if value is None: + del fields[field] + else: + fields[field] = value + assert hc.make_finding(**fields)["id"] == EXAMPLES["broken-link"]["id"] + + +def test_the_id_hashes_each_character_as_itself() -> None: + """UTF-8, never a \\u escape: the pack-upgrade example's path holds a + character outside ASCII.""" + finding = _example("pack-upgrade") + assert not finding["path"].isascii() + key = {"identity": finding["identity"], "kind": finding["kind"], + "pack_id": finding["pack_id"], "path": finding["path"]} + escaped = json.dumps(key, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + assert hc.finding_id(**key) == finding["id"] + assert not finding["id"].endswith(hashlib.sha256(escaped.encode()).hexdigest()[:16]) + + +def test_the_order_of_an_identitys_keys_does_not_move_the_id() -> None: + one = {"b": "2", "a": {"d": "4", "c": "3"}} + two = {"a": {"c": "3", "d": "4"}, "b": "2"} + assert list(one) != list(two) + ids = {hc.finding_id(identity=identity, kind="k", pack_id="p", path="x.md") + for identity in (one, two)} + assert len(ids) == 1 + + +def test_a_pack_cannot_set_the_engines_fields() -> None: + """The id, and nothing a family supplies, is the engine's: `make_finding()` + takes no id.""" + fields = _fields(_example("broken-link")) + with pytest.raises(TypeError): + hc.make_finding(id="opendox.broken-link.0000000000000000", **fields) + + +def test_every_id_is_a_git_branch_name() -> None: + """R2Q10 (a): `health-fix-` is a branch name, and the longest id, two + 40-character names and the hash, is 98 characters.""" + git = shutil.which("git") + assert git, "git is required here, as on CI" + longest = hc.finding_id(identity={}, kind="k" * 40, pack_id="p" * 40, path="a.md") + assert len(longest) == 98 == hc.ID_MAX + ids = [finding["id"] for finding in EXAMPLES.values()] + [longest] + for fid in ids: + done = subprocess.run([git, "check-ref-format", "--branch", f"health-fix-{fid}"], + capture_output=True, text=True, timeout=60) + assert done.returncode == 0, (fid, done.stderr) + + +# --------------------------------------------------------------------------- +# 4. names of 1 to 40 characters +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("field", ["pack_id", "kind"]) +def test_a_name_of_40_characters_is_admitted_and_41_is_refused(field: str) -> None: + fields = _fields(_example("orphan")) + forty, forty_one = "a-" * 20, "a-" * 20 + "b" + assert (len(forty), len(forty_one)) == (40, 41) + admitted = hc.make_finding(**{**fields, field: forty}) + assert admitted[field] == forty + assert len(admitted["id"]) <= hc.ID_MAX + with pytest.raises(hc.FindingRefused) as refused: + hc.make_finding(**{**fields, field: forty_one}) + assert refused.value.rule == "name-is-at-most-40-characters" + assert refused.value.where == f"/{field}" + by_hand = {**fields, field: forty_one} + by_hand["id"] = _independent_id(by_hand["pack_id"], by_hand["kind"], by_hand["path"], + by_hand["identity"]) + assert set(_rules(by_hand)) == {"name-is-at-most-40-characters"} + assert [v.where for v in hc.violations(by_hand)] == ["/id", f"/{field}"] + + +@pytest.mark.parametrize("field", ["pack_id", "kind"]) +def test_the_schema_itself_admits_41_so_the_cap_is_the_engines(field: str) -> None: + pattern = contracts.load(FINDING_COPY)["properties"][field]["pattern"] + assert re.search(pattern, "a" * 41) + assert re.search(pattern, "a" * 41 + "\n") is None + + +@pytest.mark.parametrize("field, rule", [("pack_id", "pack-id-is-a-name"), + ("kind", "kind-is-a-family-name")]) +@pytest.mark.parametrize("value", ["", "House", "house_style", "house.style", "h\u00e9", + "house\n", 7, None]) +def test_a_name_outside_the_alphabet_is_refused(field: str, rule: str, value: Any) -> None: + finding = {**_example("orphan"), field: value} + assert rule in _rules(finding) + + +@pytest.mark.parametrize("make", [hc.collision_identity, hc.disappearance_identity]) +def test_an_engine_identity_naming_a_41_character_part_is_refused(make) -> None: + assert make("p" * 40 + "." + "k" * 40 + "." + "0" * 16) + with pytest.raises(hc.FindingRefused) as refused: + make("p" * 41 + ".k." + "0" * 16) + assert refused.value.rule == "name-is-at-most-40-characters" + + +# --------------------------------------------------------------------------- +# 5. the identity's serialized-size cap, and the nesting bound +# --------------------------------------------------------------------------- + +def _canonical(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), + ensure_ascii=False).encode("utf-8") + + +def _identity_of(size: int, char: str = "x") -> dict[str, str]: + """An identity whose canonical JSON is exactly `size` bytes, every string + in it at most 200 characters. Fillers of `char` close the gap until one + last ASCII value can close it exactly; each filler leaves 20 bytes of + margin, more than the next key costs, so the gap never goes negative.""" + identity: dict[str, str] = {} + width = len(char.encode("utf-8")) + for index in range(1000): + key = f"k{index:03d}" + gap = size - len(_canonical({**identity, key: ""})) + assert gap >= 0, f"no identity of {size} bytes" + if gap <= STRING_MAX: + identity[key] = "x" * gap + assert len(_canonical(identity)) == size + return identity + identity[key] = char * min(STRING_MAX, (gap - 20) // width) + raise AssertionError(f"no identity of {size} bytes") + + +def test_the_cap_is_2048_bytes_of_canonical_utf8() -> None: + assert hc.IDENTITY_MAX_BYTES == IDENTITY_MAX_BYTES + + +@pytest.mark.parametrize("char", ["x", "\u00e9", "\u20ac", "\U0001f50e"]) +def test_the_identity_cap_admits_its_bound_and_refuses_one_byte_over(char: str) -> None: + at = _identity_of(IDENTITY_MAX_BYTES, char) + over = _identity_of(IDENTITY_MAX_BYTES + 1, char) + assert len(_canonical(at)) == IDENTITY_MAX_BYTES + assert len(_canonical(over)) == IDENTITY_MAX_BYTES + 1 + fields = _fields(_example("orphan")) + admitted = hc.make_finding(**{**fields, "identity": at}) + assert len(hc.identity_json(admitted["identity"]).encode("utf-8")) == IDENTITY_MAX_BYTES + with pytest.raises(hc.FindingRefused) as refused: + hc.make_finding(**{**fields, "identity": over}) + assert (refused.value.rule, refused.value.where) == ( + "identity-is-within-the-size-cap", "/identity") + with pytest.raises(hc.FindingRefused): + hc.identity_json(over) + with pytest.raises(hc.FindingRefused): + hc.finding_id(identity=over, kind="orphan", pack_id="opendox", path="a.md") + + +def test_the_cap_is_stricter_than_the_schema() -> None: + """An identity the schema admits (every string at most 200 characters, no + number, no forbidden key) is refused by the cap alone.""" + over = _identity_of(IDENTITY_MAX_BYTES + 1) + assert all(len(k) <= STRING_MAX and len(v) <= STRING_MAX for k, v in over.items()) + assert not set(over) & set(FORBIDDEN) + finding = {**_example("orphan"), "identity": over} + finding["id"] = _independent_id("opendox", "orphan", finding["path"], over) + assert _rules(finding) == ["identity-is-within-the-size-cap"] + + +def test_the_plans_largest_identity_fits_in_every_utf8_width() -> None: + """A pair of paths, each at the 200-character bound in four-byte + characters, under two 40-character keys, fits the cap.""" + widest = {"a" * 40: "\U0001f50e" * 200, "b" * 40: "\U0001f50e" * 200} + assert len(_canonical(widest)) < IDENTITY_MAX_BYTES + hc.make_finding(**{**_fields(_example("orphan")), "identity": widest}) + + +def _nested(levels: int, leaf: Any = "x") -> dict[str, Any]: + """An object `levels` containers deep, counting itself: lists inside.""" + value: Any = leaf + for _ in range(levels - 1): + value = [value] + return {"a": value} + + +@pytest.mark.parametrize("field", ["identity", "evidence"]) +def test_the_nesting_bound_admits_its_bound_and_refuses_one_level_over(field: str) -> None: + assert hc.NESTING_MAX == 16 + fields = _fields(_example("orphan")) + hc.make_finding(**{**fields, field: _nested(16)}) + with pytest.raises(hc.FindingRefused) as refused: + hc.make_finding(**{**fields, field: _nested(17)}) + assert refused.value.rule == "nesting-is-within-the-cap" + + +@pytest.mark.parametrize("field", ["identity", "evidence"]) +def test_a_deep_value_is_refused_never_a_recursion_error(field: str) -> None: + """A value nested past Python's recursion limit is judged, and refused, + by the bound: the walk does not recurse, and nothing deep reaches + `json.dumps`.""" + deep = _nested(sys.getrecursionlimit() * 5) + finding = {**_example("orphan"), field: deep} + assert "nesting-is-within-the-cap" in _rules(finding) + with pytest.raises(hc.FindingRefused): + hc.to_json(finding) + with pytest.raises(hc.FindingRefused): + hc.make_finding(**{**_fields(_example("orphan")), field: deep}) + + +# --------------------------------------------------------------------------- +# 6. the bounds on message and evidence (R2Q25 (a); ADV-27) +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("message, admitted", [ + ("x" * 200, True), + ("caf\u00e9 \u2713 \U0001f50e", True), + ("x" * 201, False), + ("", False), + *[(f"a{c}b", False) for c in ("\n", "\r", "\t", "\x00", "\x1f", "\x7f", "\x85", "\x9f", + "\u2028", "\u2029", "\ud800", "\udfff")], + ("\u00a0 is admitted", True), + (None, False), + (["a line"], False), +]) +def test_the_message_is_one_bounded_line(message: Any, admitted: bool) -> None: + finding = {**_example("broken-link"), "message": message} + rules = _rules(finding) + assert rules == ([] if admitted else ["message-is-one-bounded-line"]) + + +@pytest.mark.parametrize("evidence, where, rule", [ + ({}, None, None), + ({"span": [3, 4], "ok": True, "none": None, "digest": "ab" * 32, "ratio": 0.5}, None, None), + ({"a": "x" * 200}, None, None), + ({"x" * 200: "a"}, None, None), + ({"a": "\U0001f50e caf\u00e9"}, None, None), + ({"count": 2, "paths": ["a.md", "b.md"]}, None, None), + ({"a": "x" * 201}, "/evidence/a", "evidence-strings-are-short-text"), + ({"a": {"b": ["x" * 201]}}, "/evidence/a/b/0", "evidence-strings-are-short-text"), + ({"x" * 201: "a"}, "/evidence", "evidence-strings-are-short-text"), + ({"a": {"x" * 201: 1}}, "/evidence/a", "evidence-strings-are-short-text"), + ({"a": "\udc80"}, "/evidence/a", "evidence-strings-are-short-text"), + *[({word: "a"}, "/evidence", "evidence-names-no-text") for word in FORBIDDEN], + *[({"a": [{"b": {word: "c"}}]}, "/evidence/a/0/b", "evidence-names-no-text") + for word in FORBIDDEN], + ({"a/b": {"c~d": "x" * 201}}, "/evidence/a~1b/c~0d", "evidence-strings-are-short-text"), + ("archive/brief.md", "/evidence", "evidence-is-an-object"), + (["archive/brief.md"], "/evidence", "evidence-is-an-object"), + ({"a": float("nan")}, "/evidence/a", "finding-is-json"), + ({"a": float("inf")}, "/evidence/a", "finding-is-json"), + ({"a": ("a", "b")}, "/evidence/a", "finding-is-json"), + ({"a": b"bytes"}, "/evidence/a", "finding-is-json"), + ({1: "a"}, "/evidence", "finding-is-json"), + ({"a": 10 ** 5000}, "/evidence/a", "finding-is-json"), +]) +def test_evidence_holds_locators_only(evidence: Any, where: str | None, + rule: str | None) -> None: + finding = {**_example("broken-link"), "evidence": evidence} + found = hc.violations(finding) + if rule is None: + assert found == [] + assert json.loads(hc.to_json(finding))["evidence"] == evidence + else: + assert [(v.rule, v.where) for v in found] == [(rule, where)] + + +def test_evidence_is_an_empty_object_when_a_family_supplies_none() -> None: + """14.5, FR-011: every emitted finding carries `evidence`.""" + fields = _fields(_example("orphan")) + del fields["evidence"] + finding = hc.make_finding(**fields) + assert finding["evidence"] == {} + assert json.loads(hc.to_json(finding))["evidence"] == {} + assert "locator" not in finding + + +@pytest.mark.parametrize("identity, rule", [ + ({"a": float("nan")}, "identity-holds-no-number"), + ({"a": ("b",)}, "finding-is-json"), + ({1: "a"}, "finding-is-json"), + ({"a": "\ud800"}, "identity-strings-are-short-text"), + ({"\udfff": "a"}, "identity-strings-are-short-text"), +]) +def test_an_identity_that_is_not_text_utf8_can_encode_is_refused(identity: Any, + rule: str) -> None: + with pytest.raises(hc.FindingRefused) as refused: + hc.finding_id(identity=identity, kind="orphan", pack_id="opendox", path="a.md") + assert refused.value.rule == rule + + +@pytest.mark.parametrize("locator, rules", [ + ({"line_start": 1, "line_end": 1}, []), + ({"target": "../old/brief.md"}, []), + ({"line_start": 3, "line_end": 9, "target": "x" * 200}, []), + ({"line_start": 12.0, "line_end": 12}, []), + ({}, ["locator-keys"]), + ({"line_end": 12}, ["locator-keys"]), + ({"line_start": 1, "line_end": 2, "column": 3}, ["locator-keys"]), + ("12", ["locator-keys"]), + ({"line_start": 0, "line_end": 1}, ["locator-line-is-a-line-number"]), + ({"line_start": 1, "line_end": True}, ["locator-line-is-a-line-number"]), + ({"line_start": 1.5, "line_end": 2}, ["locator-line-is-a-line-number"]), + ({"line_start": "1", "line_end": 2}, ["locator-line-is-a-line-number"]), + ({"line_start": float("nan"), "line_end": 2}, ["locator-line-is-a-line-number"]), + ({"line_start": 5, "line_end": 4}, ["locator-span-is-ordered"]), + ({"line_start": 0, "line_end": -1}, ["locator-line-is-a-line-number", + "locator-line-is-a-line-number"]), + ({"target": ""}, ["locator-target-is-short-text"]), + ({"target": "x" * 201}, ["locator-target-is-short-text"]), + ({"target": "\ud800"}, ["locator-target-is-short-text"]), +]) +def test_the_locator_is_display_only_and_bounded(locator: Any, rules: list[str]) -> None: + finding = {**_example("broken-link"), "locator": locator} + assert _rules(finding) == rules + + +@pytest.mark.parametrize("path, admitted", [ + ("notes/plan.md", True), ("", True), ("a/..b/c..", True), ("caf\u00e9.md", True), + ("/etc/passwd", False), ("C:notes.md", False), ("c:/notes.md", False), + ("notes\\plan.md", False), ("../plan.md", False), ("notes/../plan.md", False), + ("notes/..", False), ("..", False), ("a\nb", False), ("a\x7fb", False), + ("a\udc80b", False), (None, False), (3, False), +]) +def test_the_path_is_corpus_relative(path: Any, admitted: bool) -> None: + finding = _example("orphan") + finding["path"] = path + if path == "": + finding["identity"] = {"category": "x", "entry": ""} + rules = [r for r in _rules(finding) if r != "id-is-the-hash-of-its-key"] + assert rules == ([] if admitted else ["path-is-corpus-relative"]) + + +@pytest.mark.parametrize("version, admitted", [ + ("0.2.0", True), ("1.4.0+local build", True), ("", False), ("1\n", False), + ("1\x85", False), ("1\ud800", False), (1, False)]) +def test_the_pack_version_is_text(version: Any, admitted: bool) -> None: + finding = {**_example("broken-link"), "pack_version": version} + assert _rules(finding) == ([] if admitted else ["pack-version-is-text"]) + + +@pytest.mark.parametrize("field, value, rule", [ + ("severity", "critical", "severity-is-known"), + ("severity", None, "severity-is-known"), + ("resolution_class", "auto_fix", "resolution-class-is-one-of-three"), + ("resolution_class", "Auto-Fix", "resolution-class-is-one-of-three"), + ("resolution_class", ["auto-fix"], "resolution-class-is-one-of-three"), + ("baseline_class", "gone", "baseline-class-is-known"), + ("baseline_class", None, "baseline-class-is-known"), +]) +def test_the_closed_values_are_closed(field: str, value: Any, rule: str) -> None: + finding = {**_example("broken-link"), field: value} + assert _rules(finding) == [rule] + + +# --------------------------------------------------------------------------- +# 7. every rule the copy catalogues is enforced, and the engine's own +# --------------------------------------------------------------------------- + +def _with(name: str, **changes: Any) -> dict[str, Any]: + finding = _example(name) + for key, value in changes.items(): + if value is _DROP: + del finding[key] + else: + finding[key] = value + return finding + + +_DROP = object() + +#: openDox-spec's negative example for each catalogued rule, rebuilt as the +#: positive example it names with its one change, then the engine's own. +NEGATIVES: dict[str, dict[str, Any]] = { + "finding-keys": _with("broken-link", patch="--- a/notes/plan.md\n+++ b/notes/plan.md\n"), + "id-is-well-formed": _with("broken-link", id="opendox.broken-link.546CD2AACB1A6738"), + "kind-is-a-family-name": _with("broken-link", kind="Broken_Link"), + "pack-id-is-a-name": _with("broken-link", pack_id="house_style"), + "pack-version-is-text": _with("broken-link", pack_version=""), + "path-is-corpus-relative": _with("broken-link", path="../outside/plan.md"), + "identity-is-an-object": _with("broken-link", identity=["../old/brief.md"]), + "identity-holds-no-number": _with("broken-link", + identity={"target": "../old/brief.md", "line": 12}), + "identity-strings-are-short-text": _with("broken-link", identity={"target": PASSAGE}), + "identity-names-no-text": _with("broken-link", identity={"target": "../old/brief.md", + "text": "see the old brief"}), + "locator-keys": _with("broken-link", locator={"line_start": 12}), + "locator-line-is-a-line-number": _with("broken-link", + locator={"line_start": 0, "line_end": 12}), + "locator-target-is-short-text": _with("broken-link", locator={ + "line_start": 12, "line_end": 12, "target": PASSAGE}), + "severity-is-known": _with("broken-link", severity="critical"), + "resolution-class-is-one-of-three": _with("broken-link", resolution_class="auto_fix"), + "message-is-one-bounded-line": _with("broken-link", message=( + "link target does not exist;\na unique file of that name exists elsewhere")), + "evidence-is-an-object": _with("broken-link", evidence="archive/brief.md"), + "evidence-strings-are-short-text": _with("broken-link", evidence={ + "candidate": "archive/brief.md", "family_version": "1", "context": PASSAGE}), + "evidence-names-no-text": _with("broken-link", evidence={ + "candidate": "archive/brief.md", "family_version": "1", + "excerpt": "see the old brief"}), + "baseline-class-is-known": _with("broken-link", baseline_class="unclassed"), + "pathless-finding-is-human-only": _with("no-sandbox", resolution_class="assisted"), + "pathless-identity-is-category-and-entry": _with("no-sandbox", identity={}), + "identity-collision-is-human-only": _with("identity-collision", + resolution_class="assisted"), + "collision-identity-is-the-collided-id": _with("identity-collision", identity={ + "id": "house-style.heading-case.0f1e2d3c4b5a6978"}), + "id-names-its-pack-and-kind": _with("broken-link", + id="house-style.broken-link.546cd2aacb1a6738"), + "id-is-the-hash-of-its-key": _with("broken-link", + identity={"target": "../old/brief-2.md"}), + "locator-span-is-ordered": _with("broken-link", + locator={"line_start": 14, "line_end": 12}), + # the engine's own + # a 41-character kind under the orphan's own id: the id is well formed, + # and the reference rules judge no part their own rules refuse + "name-is-at-most-40-characters": _with("orphan", kind="o" * 41), + "identity-is-within-the-size-cap": _with( + "orphan", identity=_identity_of(IDENTITY_MAX_BYTES + 1), + id=_independent_id("opendox", "orphan", "notes/appendix.md", + _identity_of(IDENTITY_MAX_BYTES + 1))), + "nesting-is-within-the-cap": _with("orphan", evidence=_nested(17)), + "finding-is-json": _with("orphan", evidence={"a": ("b",)}), +} + + +def test_every_catalogued_rule_and_every_engine_rule_has_a_negative() -> None: + assert set(NEGATIVES) == set(hc.SHAPE_RULES) | set(hc.REFERENCE_RULES) | set( + hc.ENGINE_RULES) + + +@pytest.mark.parametrize("rule", sorted(NEGATIVES)) +def test_each_negative_breaks_its_rule_and_no_other(rule: str) -> None: + finding = NEGATIVES[rule] + assert _rules(finding) == [rule], hc.violations(finding) + with pytest.raises(hc.FindingRefused) as refused: + hc.check_finding(finding) + assert refused.value.rule == rule + assert refused.value.violation == hc.violations(finding)[0] + + +@pytest.mark.parametrize("missing", hc.REQUIRED_FIELDS) +def test_a_finding_without_a_required_field_is_refused(missing: str) -> None: + finding = _with("broken-link", **{missing: _DROP}) + assert "finding-keys" in _rules(finding) + + +@pytest.mark.parametrize("finding", [[], "a finding", None, 3]) +def test_a_finding_that_is_not_an_object_is_refused(finding: Any) -> None: + assert [(v.rule, v.where) for v in hc.violations(finding)] == [("finding-keys", "")] + + +def test_a_pathless_finding_breaks_the_human_only_rule_only_over_a_known_class() -> None: + """A class outside the three is that rule's refusal, and not also the + pathless rule's: one cause, one violation.""" + assert _rules(_with("no-sandbox", resolution_class="later")) == [ + "resolution-class-is-one-of-three"] + assert _rules(_with("identity-collision", resolution_class="later")) == [ + "resolution-class-is-one-of-three"] + + +def test_the_reference_rules_judge_only_admitted_parts() -> None: + """A malformed part is reported once, by its own rule: the hash is not + judged over a key it could not be computed from, as in openDox-spec's own + test.""" + assert _rules(_with("broken-link", kind="Broken_Link")) == ["kind-is-a-family-name"] + assert _rules(_with("no-sandbox", identity={})) == [ + "pathless-identity-is-category-and-entry"] + assert _rules(_with("broken-link", identity={"target": "\ud800"})) == [ + "identity-strings-are-short-text"] + + +def test_a_pathless_collision_keeps_the_collisions_identity() -> None: + finding = hc.make_finding( + kind="identity-collision", pack_id="opendox", pack_version="0.2.0", path="", + identity=hc.collision_identity("opendox.no-sandbox.5ccbb79891acb2e8"), + severity="error", resolution_class="human-only", message="m") + assert hc.violations(finding) == [] + assert _rules({**finding, "resolution_class": "assisted"}) == [ + "pathless-finding-is-human-only", "identity-collision-is-human-only"] + + +# --------------------------------------------------------------------------- +# 8. the engine's own identities and categories; the examples rebuilt +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("name", sorted(EXAMPLES)) +def test_make_finding_rebuilds_each_example_id_and_all(name: str) -> None: + """The engine's door: given a family's or pack's fields and the engine's + pack_id and pack_version, `make_finding()` stamps the example's own id + and writes nothing else.""" + example = _example(name) + built = hc.make_finding(**_fields(example)) + assert built == example + assert list(built) == [field for field in hc.FIELDS if field in example] + assert json.loads(hc.to_json(built)) == example + + +def test_make_finding_returns_its_own_copy() -> None: + fields = _fields(_example("broken-link")) + built = hc.make_finding(**fields) + fields["identity"]["target"] = "elsewhere" + fields["evidence"]["candidate"] = "elsewhere" + fields["locator"]["line_start"] = 99 + assert built == EXAMPLES["broken-link"] + + +def test_to_json_is_the_canonical_serialization() -> None: + built = hc.make_finding(**_fields(_example("pack-upgrade"))) + text = hc.to_json(built) + assert text == json.dumps(built, sort_keys=True, separators=(",", ":"), + ensure_ascii=False) + assert "caf\u00e9" in text + + +def test_the_categories_are_names_and_the_copys_example_is_one() -> None: + assert "no-sandbox" in hc.CATEGORIES + assert len(set(hc.CATEGORIES)) == len(hc.CATEGORIES) + for category in hc.CATEGORIES: + assert re.fullmatch(r"[a-z0-9-]{1,40}", category), category + + +@pytest.mark.parametrize("category", hc.CATEGORIES) +def test_each_category_makes_a_pathless_finding_with_one_id_across_runs( + category: str) -> None: + def build() -> dict[str, Any]: + return hc.make_finding( + kind=category, pack_id="opendox", pack_version="0.2.0", path="", + identity=hc.pathless_identity(category, "house-style"), severity="warning", + resolution_class="human-only", message="the pack did not run") + first, second = build(), build() + assert first["id"] == second["id"] + assert first["identity"] == {"category": category, "entry": "house-style"} + + +@pytest.mark.parametrize("category, entry", [ + ("not-a-category", ""), ("No-Sandbox", ""), ("", ""), (None, ""), + ("no-sandbox", "House"), ("no-sandbox", "a" * 201), ("no-sandbox", None)]) +def test_the_pathless_identity_refuses_what_is_not_the_engines(category: Any, + entry: Any) -> None: + with pytest.raises(hc.FindingRefused) as refused: + hc.pathless_identity(category, entry) + assert refused.value.rule == "pathless-identity-is-category-and-entry" + + +@pytest.mark.parametrize("make, rule", [ + (hc.collision_identity, "collision-identity-is-the-collided-id"), + (hc.disappearance_identity, "id-is-well-formed")]) +@pytest.mark.parametrize("value", ["", "opendox.broken-link", "opendox.broken-link.XYZ", + "opendox.broken-link.546cd2aacb1a673", + "opendox.broken-link.546cd2aacb1a6738\n", None]) +def test_an_engine_identity_names_a_well_formed_id(make, rule: str, value: Any) -> None: + with pytest.raises(hc.FindingRefused) as refused: + make(value) + assert refused.value.rule == rule + + +def test_the_engines_identities_are_their_keys() -> None: + fid = "opendox.broken-link.546cd2aacb1a6738" + assert hc.collision_identity(fid) == {"collided_id": fid} + assert hc.disappearance_identity(fid) == {"disappeared_id": fid} + assert hc.pathless_identity("no-sandbox") == {"category": "no-sandbox", "entry": ""} + + +# --------------------------------------------------------------------------- +# 9. the closure: the standard library, and nothing else +# --------------------------------------------------------------------------- + +def test_the_module_names_only_the_standard_library() -> None: + """Read from the source: every import is the standard library's, and none + is relative or of openDox (R2Q18 (a); N-3).""" + tree = ast.parse(MODULE.read_text(encoding="utf-8")) + names: list[str] = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + names += [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom): + assert node.level == 0, "a relative import" + names.append(node.module or "") + elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + assert node.func.id not in ("__import__", "exec", "eval"), node.func.id + assert names + assert sorted({n for n in names if n.split(".")[0] not in sys.stdlib_module_names + and n != "__future__"}) == [] + + +#: openXdox, and openxFactory's packages, blocked at the finder. +_BLOCKED = ("openxdox", "doc_health", "ideation_dashboard", + "corpus_adapter_openxfactory", "scripts", "yaml") + +_IMPORT_WEIGHT = """ +import json, sys +for name in {blocked!r}: + sys.modules[name] = None +before = set(sys.modules) +import opendox.health_contract +added = set(sys.modules) - before +print(json.dumps({{ + "foreign": sorted(m for m in added + if m.split(".")[0] not in sys.stdlib_module_names + and m.split(".")[0] != "opendox"), + "own": sorted(m for m in added if m.split(".")[0] == "opendox"), +}})) +""" + + +def test_the_module_loads_the_standard_library_and_nothing_else_of_opendox() -> None: + env = {**os.environ, + "PYTHONPATH": os.pathsep.join([str(SRC), os.environ.get("PYTHONPATH", "")])} + done = subprocess.run( + [sys.executable, "-c", textwrap.dedent(_IMPORT_WEIGHT.format(blocked=_BLOCKED))], + capture_output=True, text=True, cwd=str(SRC), env=env, timeout=120) + assert done.returncode == 0, done.stderr + found = json.loads(done.stdout.strip().splitlines()[-1]) + assert found == {"foreign": [], "own": ["opendox", "opendox.health_contract"]}, found + + +_ALONE = """ +import json, sys +sys.path.insert(0, {root!r}) +import opendox.health_contract as hc +finding = hc.make_finding(kind="broken-link", pack_id="opendox", pack_version="0.2.0", + path="notes/plan.md", identity={{"target": "../old/brief.md"}}, + locator={{"line_start": 12, "line_end": 12}}, severity="warning", + resolution_class="auto-fix", message="m", evidence={{}}) +print(json.dumps({{ + "json": hc.to_json(finding), + "site": "site" in sys.modules, + "paths": [p for p in sys.path if "site-packages" in p or "dist-packages" in p], + "file": hc.__file__, +}})) +""" + + +def test_the_module_runs_alone_with_no_site_packages(tmp_path: Path) -> None: + """What the sandbox binds (R2Q18 (a)): the interpreter, its standard + library and this one module. It is copied into a directory that holds it + and an empty package marker, and run under `-I -S`.""" + package = tmp_path / "opendox" + package.mkdir() + (package / "__init__.py").write_text("", encoding="utf-8") + shutil.copyfile(MODULE, package / "health_contract.py") + assert sorted(p.name for p in package.iterdir()) == ["__init__.py", "health_contract.py"] + done = subprocess.run( + [sys.executable, "-I", "-S", "-c", textwrap.dedent(_ALONE.format(root=str(tmp_path)))], + capture_output=True, text=True, cwd=str(tmp_path), timeout=120, + env={"PATH": os.environ.get("PATH", ""), "LANG": "C.UTF-8"}) + assert done.returncode == 0, done.stderr + found = json.loads(done.stdout.strip().splitlines()[-1]) + assert found["site"] is False and found["paths"] == [] + assert Path(found["file"]).parent == package + here = hc.make_finding(kind="broken-link", pack_id="opendox", pack_version="0.2.0", + path="notes/plan.md", identity={"target": "../old/brief.md"}, + locator={"line_start": 12, "line_end": 12}, severity="warning", + resolution_class="auto-fix", message="m", evidence={}) + assert found["json"] == hc.to_json(here) + assert json.loads(found["json"])["id"] == EXAMPLES["broken-link"]["id"] + + +# --------------------------------------------------------------------------- +# 10. a refusal quotes nothing of the finding +# --------------------------------------------------------------------------- + +_SECRET = "SECRET-PASSAGE-" + "q" * 300 + + +@pytest.mark.parametrize("change", [ + {"identity": {"target": _SECRET}}, + {"identity": {_SECRET: "a"}}, + {"identity": {"a": [_SECRET]}}, + {"evidence": {"a": _SECRET}}, + {"evidence": {_SECRET: "a"}}, + {"message": _SECRET}, + {"locator": {"target": _SECRET}}, + {"pack_version": _SECRET + "\n"}, + {"path": "../" + _SECRET}, + {"kind": _SECRET}, + {_SECRET: "an unknown field"}, +]) +def test_a_refusal_quotes_nothing_of_the_finding(change: dict[str, Any]) -> None: + finding = {**_example("broken-link"), **change} + found = hc.violations(finding) + assert found + for violation in found: + assert "SECRET" not in violation.line(), violation + with pytest.raises(hc.FindingRefused) as refused: + hc.check_finding(finding) + assert "SECRET" not in str(refused.value) + + +def test_a_violation_reads_as_its_rule_its_place_and_its_detail() -> None: + violation = hc.violations(NEGATIVES["locator-span-is-ordered"])[0] + assert violation.rule == "locator-span-is-ordered" + assert violation.where == "/locator/line_end" + assert violation.line() == f"[locator-span-is-ordered] /locator/line_end: {violation.detail}" + assert "12" in violation.detail and "14" in violation.detail + refused = hc.FindingRefused(violation) + assert isinstance(refused, ValueError) + assert (refused.rule, refused.where) == (violation.rule, violation.where) + assert violation.line() in str(refused) + + +def test_no_number_check_admits_booleans_and_refuses_every_number() -> None: + """`True` is an `int` in Python and a boolean in JSON: the identity admits + it and refuses `1`.""" + assert isinstance(True, int) + hc.finding_id(identity={"a": True, "b": False}, kind="k", pack_id="p", path="a.md") + for number in (1, 0, -1, 1.0, 0.5, math.pi, 10 ** 30): + with pytest.raises(hc.FindingRefused) as refused: + hc.finding_id(identity={"a": number}, kind="k", pack_id="p", path="a.md") + assert refused.value.rule == "identity-holds-no-number" diff --git a/tests/test_validator_input_set.py b/tests/test_validator_input_set.py index 3ba756db..85e92a07 100644 --- a/tests/test_validator_input_set.py +++ b/tests/test_validator_input_set.py @@ -1,4 +1,5 @@ -"""openDox's validator's input set, held to plan 034's T057. +"""openDox's validator's input set, held to plan 034's T057 and plan 038's +T041. T057 realizes #1144's 7.1, 7.1a, 7.1b and 7.2, with 7.1 as T007's batch G amends it (R1Q11 (a) and R1Q12 (a), `openxFactory#656` comment `5850003126`): @@ -9,6 +10,16 @@ openDox root pins (R1Q12 (a)) ... A test asserts that `gate-intent` and `ideation-possibles-register` are NOT in the set (7.1b). +T005's batch Q adds to batch G's text (R2Q22 (a), `6003486656`; N-15, +`6013547504`): openDox-spec's three health schemas travel as the four do, and +the finding's neutral shape is *"a copy the engine reads and NOT a validator +kind: a finding's `kind` field is its family, so no `kind` constant can name +the schema (N-15). openDox-code's `tests/test_validator_input_set.py` +therefore reads 'the validator's kinds plus the finding shape'."* Plan 038's +T041 makes the first of those copies: the finding shape, at the spec-leg +commit T060 pinned (openDox-spec `7db9438b`, `dox-v1.2`). T047 and T054 add +the two file kinds after it. + Its falsifier is *"the packaged-copy digest test and the 7.1b test"*. They are the first two cases below: @@ -18,9 +29,10 @@ WHAT ELSE IT HOLDS. -1. THE SET IS EXACTLY FOUR: the record, the validator's kinds and the files on - disk all name the same four copies, and none of the six schemas outside - the set is carried. +1. THE SET IS THE VALIDATOR'S KINDS PLUS THE FINDING SHAPE (N-15): the + validator's kinds read the same copies the record names, less the finding + shape, which the record and the files on disk carry and no kind reads. + None of the six schemas outside the set is carried. 2. PRESENCE IS NOT IDENTITY: a copy that differs from its digest, is absent, or has no digest recorded is refused before a byte of it is parsed, and a validator is never built over it. A record that cannot hold every copy to a @@ -55,25 +67,37 @@ ROOT = Path(__file__).resolve().parents[1] PACKAGE = ROOT / "src" / "opendox" / "contracts" -#: openDox's own spec leg's four schemas (7.1, as batch G amends it). -THE_FOUR = ("ideation-workbench", "opendox-snapshot", "xfactory-workbench-chat-turn", - "xfactory-workbench-model-catalog") +#: The copies the validator's kinds read: openDox's own spec leg's four +#: validated schemas (7.1, as batch G amends it). +VALIDATOR_COPIES = ("ideation-workbench", "opendox-snapshot", + "xfactory-workbench-chat-turn", "xfactory-workbench-model-catalog") + +#: The finding's neutral shape (plan 038 T040 and T041): a copy the health +#: engine reads, and no validator kind (N-15). +FINDING_SHAPE = "opendox-health-finding" + +#: Every copy the record names, in its order: the validator's plus the finding +#: shape. +THE_COPIES = tuple(sorted(VALIDATOR_COPIES + (FINDING_SHAPE,))) -#: The spec-leg commit the openDox root pins, which the copies are held to: T053 -#: as landed, openDox-spec#16's squash (opensoft/openDox#14 moved the root's spec -#: pin to it). It is the one commit that carries all four. It moved from #16's -#: head, cd49eb25, with the record, in lockstep. The two commits have one tree. -SPEC_COMMIT = "f7ee3c763b3af4581daf1cd54406e5111e9358e6" +#: The spec-leg commit the openDox root pins, which the copies are held to: +#: T040 as landed, openDox-spec#17's squash, which plan 038's T060 pinned +#: (opensoft/openDox#20, `6a9f4902`, tagged `dox-v1.2`). It is the one commit +#: that carries all five. It moved from T053's f7ee3c76 with the record, in +#: lockstep. #17 only added files, so the four earlier copies are unchanged. +SPEC_COMMIT = "7db9438b4cc4446ab4e6ab5b552c220312deccc9" #: WHAT THE OPENDOX ROOT PINS, stated here apart from the record, so that a copy -#: and its recorded digest cannot move together unseen. All four are the +#: and its recorded digest cannot move together unseen. All five are the #: digests the root's `contracts/manifest.yaml` records for them at its spec -#: pin, `SPEC_COMMIT` (the root's `main` at 52005213, opensoft/openDox#14). The -#: first three are unchanged from the root's previous spec pin, 8fe8c4c7. The -#: fourth is T053's new `opendox-snapshot` entry. +#: pin, `SPEC_COMMIT` (the root's `main` at 6a9f4902, opensoft/openDox#20). The +#: first four are unchanged from the root's previous spec pin, f7ee3c76. The +#: fifth is T040's new `opendox-health-finding` entry. PINNED_BY_THE_ROOT = { "ideation-workbench": "d30438491119c20928fbe4e85088fc33682829eeb6558d87dafce651000faafc", + "opendox-health-finding": + "6fb9b29f23a4270fa0310624d9e59b5d82e9888a5168600671e90ed30f781016", "opendox-snapshot": "f9e3e111af1d4bd4c377c933027d81b582ae2b0a395b66f4e4621992454a584a", "xfactory-workbench-chat-turn": @@ -104,7 +128,7 @@ def test_each_packaged_copy_is_the_spec_legs_file_at_the_pinned_commit() -> None f"the copies are recorded at {record.commit}, and this test holds them at " f"{SPEC_COMMIT}. They move together, in one commit: copy the spec leg's " "files at the commit the openDox root pins, and move both") - assert record.ids == THE_FOUR + assert record.ids == THE_COPIES for copy in record.copies: on_disk = PACKAGE / "schemas" / f"{copy.id}.schema.yaml" digest = hashlib.sha256(on_disk.read_bytes()).hexdigest() @@ -142,29 +166,49 @@ def test_gate_intent_and_the_possibles_register_are_not_in_the_set() -> None: # --------------------------------------------------------------------------- -# the set is exactly four +# the set: the validator's kinds plus the finding shape # --------------------------------------------------------------------------- -def test_the_set_is_the_spec_legs_four_and_nothing_else() -> None: - """7.1: openDox validates its own spec leg's kinds. The record, the kinds' - entries and the files on disk name the same four copies. None of the - consumer's three and none of openxFactory's four is carried.""" +def test_the_set_is_the_validators_kinds_plus_the_finding_shape() -> None: + """7.1, as batch Q amends it (N-15): openDox validates its own spec leg's + kinds, and its engine reads the finding shape. The kinds' entries name the + validator's copies; the record and the files on disk name those plus the + finding shape. None of the consumer's three and none of openxFactory's + four is carried.""" entries = {copy for copy, _pointer in validator.KIND_ENTRIES.values()} on_disk = {path.name.removesuffix(".schema.yaml") for path in (PACKAGE / "schemas").iterdir()} - assert set(THE_FOUR) == entries == on_disk == set(contracts.record().ids) - assert contracts.COPY_IDS == set(THE_FOUR) + assert set(VALIDATOR_COPIES) == entries + assert entries | {FINDING_SHAPE} == on_disk == set(contracts.record().ids) + assert contracts.COPY_IDS == set(THE_COPIES) + assert FINDING_SHAPE not in entries assert not (set(OPENXDOX_SPECS) | set(OPENXFACTORYS)) & on_disk assert sorted(p.name for p in PACKAGE.iterdir() if p.name != "__pycache__") == [ "__init__.py", "copies.yaml", "schemas"] +def test_the_finding_shape_is_a_copy_and_no_validator_kind() -> None: + """N-15: a finding's `kind` is its family, so the shape declares no `kind` + const, no kind of the validator reads it, and no validator is built over + it. Its copy is still proved before it is read.""" + shape = contracts.load(FINDING_SHAPE) + assert "const" not in shape["properties"]["kind"] + assert "schema_version" not in shape["properties"] + assert FINDING_SHAPE not in validator.KIND_ENTRIES + with pytest.raises(validator.UnknownKind): + validator.validator_for(FINDING_SHAPE) + assert contracts.verified_bytes(FINDING_SHAPE) == ( + PACKAGE / "schemas" / f"{FINDING_SHAPE}.schema.yaml").read_bytes() + + def test_each_kind_is_the_const_its_entry_declares() -> None: """The validator's map of kinds is the copies' own: each kind's entry declares that kind as its `kind` const. So a kind the map names and no - copy declares, or a copy's kind the map leaves out, fails here.""" + copy declares, or a copy's kind the map leaves out, fails here. It reads + the VALIDATOR's copies only: the finding shape declares no `kind` const + (N-15), and the set test above holds the two lists apart.""" derived = {} - for copy_id in THE_FOUR: + for copy_id in VALIDATOR_COPIES: document = contracts.load(copy_id) envelopes = [document] if "oneOf" not in document else [ validator._at_pointer(document, branch["$ref"][1:]) @@ -313,15 +357,17 @@ def test_an_absent_copy_is_refused(monkeypatch: pytest.MonkeyPatch) -> None: ({"kind": "pinned_contract_manifest"}, "kind is"), ({"schema_version": True}, "schema_version is"), ({"unread": 1}, "its keys are"), - # 7.1b at run time: an edited record cannot let a fifth schema in, even a - # well-formed entry whose file sits beside the four, nor leave one out. + # 7.1b at run time: an edited record cannot let another schema in, even a + # well-formed entry whose file sits beside the copies, nor leave one out. ({"copies": _SHIPPED + [{"id": "gate-intent", "path": "contracts/schemas/gate-intent.schema.yaml", - "sha256": "0" * 64}]}, "not openDox's four"), - ({"copies": _SHIPPED[:3]}, "not openDox's four"), + "sha256": "0" * 64}]}, "not the copies openDox carries"), + ({"copies": _SHIPPED[:-1]}, "not the copies openDox carries"), + ({"copies": [entry for entry in _SHIPPED if entry["id"] != "opendox-health-finding"]}, + "not the copies openDox carries"), ], ids=["empty digest", "no digest", "wrong path", "repeated id", "no copies", "short commit", "another leg", "another kind", "boolean version", "unknown key", - "a fifth copy", "three copies"]) + "a copy outside the set", "a copy left out", "the finding shape left out"]) def test_a_record_that_cannot_hold_every_copy_is_refused( monkeypatch: pytest.MonkeyPatch, changes: dict, says: str) -> None: """An empty or absent digest is drift and never a pass, and so is a record @@ -403,7 +449,7 @@ def test_the_record_as_shipped_is_accepted() -> None: """The negative cases above change one field each of the shipped record, so this is their control.""" record = contracts.record() - assert (record.commit, record.ids) == (SPEC_COMMIT, THE_FOUR) + assert (record.commit, record.ids) == (SPEC_COMMIT, THE_COPIES) # --------------------------------------------------------------------------- From 6f85bee02db9d78444ce0eecf32939cd1d5a59ba Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:16:08 +0000 Subject: [PATCH 02/17] WIP T041 (park 2): the names rule's survivor killed, the separator pattern escaped (plan 038) A test of an id naming the right pack and the wrong kind kills the one mutant run 1 left alive; the message pattern's U+2028 and U+2029 are written as escapes, not as raw characters; the refused-identity cases get ids. Mutant run 2: 57 of 57 killed. The full suite is still running. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 2 +- tests/test_health_contract.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index b6b7e215..043f47dc 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -263,7 +263,7 @@ _DRIVE = re.compile(r"[A-Za-z]:") _SURROGATE = re.compile("[\ud800-\udfff]") _CONTROL = re.compile("[\x00-\x1f\x7f-\x9f\ud800-\udfff]") -_NOT_ONE_LINE = re.compile("[\x00-\x1f\x7f-\x9f

\ud800-\udfff]") +_NOT_ONE_LINE = re.compile("[\x00-\x1f\x7f-\x9f\u2028\u2029\ud800-\udfff]") class Violation(NamedTuple): diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index e054a4fc..1775e4aa 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -335,7 +335,9 @@ def test_the_serialized_identity_holds_the_schemas_bound(name: str) -> None: ] -@pytest.mark.parametrize("identity, path, kind, rule", _REFUSED_IDENTITIES) +@pytest.mark.parametrize("identity, path, kind, rule", _REFUSED_IDENTITIES, + ids=[f"{case[3]}-{index}" for index, case + in enumerate(_REFUSED_IDENTITIES)]) def test_an_identity_outside_the_bound_is_never_serialized( identity: Any, path: str, kind: str, rule: str) -> None: """The other half of the falsifier: an identity that breaks the bound is @@ -928,6 +930,17 @@ def test_a_pathless_finding_breaks_the_human_only_rule_only_over_a_known_class() "resolution-class-is-one-of-three"] +@pytest.mark.parametrize("fid", [ + "house-style.broken-link.546cd2aacb1a6738", + "opendox.orphan.546cd2aacb1a6738", + "house-style.orphan.546cd2aacb1a6738", +], ids=["another pack", "another kind", "both"]) +def test_the_id_names_the_findings_own_pack_and_its_own_kind(fid: str) -> None: + """Each of the id's two names is judged: its hash is still the hash of + the finding's own key, so this rule alone is broken.""" + assert _rules(_with("broken-link", id=fid)) == ["id-names-its-pack-and-kind"] + + def test_the_reference_rules_judge_only_admitted_parts() -> None: """A malformed part is reported once, by its own rule: the hash is not judged over a key it could not be computed from, as in openDox-spec's own From 8e4065b28d87d3a1769dee5d0eaa5b19462a88ff Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:50:24 +0000 Subject: [PATCH 03/17] T041 review round 1: every checked finding serializes; the disappearance key is T046's (plan 038) Copilot r4223399258 and Codex r4223401860 on openDox-code#97, both fix-now: a locator line past Python's integer-string limit is refused as finding-is-json, so to_json() never raises ValueError on a checked finding; and the span refusal no longer formats its lines with :g, which raised OverflowError on a 401-digit line. It quotes no value of the finding now. Copilot r4223399173, fix-now in T041's scope: disappearance_identity() and UNCITED_DISAPPEARANCE are removed. T041's task names the pathless and the collision keys only; the re-raise of an uncited disappearance is the baseline's (T046), and its pathless case is the holder's to rule (the PR body's "For the holder", item 2). Red first: the new locator cases fail on the previous head, 3 failed, one with OverflowError. Mutants: 60 of 60 killed. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 62 ++++++++++++++++++-------------- tests/test_health_contract.py | 65 +++++++++++++++++++++++++--------- 2 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 043f47dc..0d9f5ed8 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -78,10 +78,14 @@ A finding's names and hashes, once admitted, may be named. THE ENGINE'S OWN IDENTITIES (contracts/health-finding.md § The id rule). -`pathless_identity()`, `collision_identity()` and `disappearance_identity()` -build the three keys the engine uses for its own findings, and `CATEGORIES` -fixes the failure categories of an install-level or pre-run finding, so each -such finding keeps one id across runs. +`pathless_identity()` and `collision_identity()` build the two keys T041's +task names for the engine's own findings, and `CATEGORIES` fixes the failure +categories of an install-level or pre-run finding, so each such finding keeps +one id across runs. The third engine key, `{disappeared_id}` for the re-raise +of an uncited disappearance, is the baseline's (T046), and is not built here: +for a pathless original, the finding schema's +`pathless-identity-is-category-and-entry` rule refuses it, which is the +holder's to rule (openDox-code#97, "For the holder", item 2). T045 APPENDS the pack protocol to this module (the static declaration, the stdout document and the patch type) at T041's landing, a cross-lane hand-off @@ -126,12 +130,10 @@ "SEVERITIES", "SHAPE_RULES", "STRING_MAX", - "UNCITED_DISAPPEARANCE", "Violation", "canonical_json", "check_finding", "collision_identity", - "disappearance_identity", "finding_id", "id_key", "identity_json", @@ -168,10 +170,6 @@ #: in one run shared one id. IDENTITY_COLLISION = "identity-collision" -#: The kind of the engine's once-only re-raise of an uncited disappearance -#: (data-model.md § Baseline classes). -UNCITED_DISAPPEARANCE = "uncited-disappearance" - #: A finding's fields, in the schema's order. REQUIRED_FIELDS: tuple[str, ...] = ( "id", "kind", "pack_id", "pack_version", "path", "identity", "severity", @@ -364,15 +362,6 @@ def collision_identity(collided_id: str) -> dict[str, str]: return {"collided_id": collided_id} -def disappearance_identity(disappeared_id: str) -> dict[str, str]: - """The identity of the engine's re-raise of an uncited disappearance: the - original's id. The re-raise has its own id, never the original's.""" - found = list(_id(disappeared_id, "/identity/disappeared_id", "id-is-well-formed")) - if found: - raise FindingRefused(found[0]) - return {"disappeared_id": disappeared_id} - - # --------------------------------------------------------------------------- # the rules # --------------------------------------------------------------------------- @@ -489,11 +478,8 @@ def _walk(root: dict[Any, Any], where: str, field: str) -> Iterator[Violation]: "is a number, which an identity never holds") elif isinstance(value, float) and not math.isfinite(value): yield Violation(NOT_JSON, at, "is a number JSON cannot write") - elif isinstance(value, int) and value.bit_length() > 64: - try: - str(value) - except ValueError: - yield Violation(NOT_JSON, at, "is an integer too long to write as text") + elif isinstance(value, int) and not _writable(value): + yield Violation(NOT_JSON, at, "is an integer too long to write as text") else: yield Violation(NOT_JSON, at, f"is {_type(value)}") @@ -542,6 +528,19 @@ def _collision_form(identity: dict[Any, Any]) -> Iterator[Violation]: yield from _id(identity["collided_id"], "/identity/collided_id", rule) +def _writable(value: int) -> bool: + """Whether `json.dumps` can write this integer: Python refuses one longer + than its integer-string conversion limit (4300 digits by default), with a + `ValueError`. So a checked finding always serializes.""" + if value.bit_length() <= 64: + return True + try: + str(value) + except ValueError: + return False + return True + + def _line(value: Any) -> bool: """A whole number no less than 1: JSON's integer, which 12.0 is too.""" if isinstance(value, bool): @@ -563,9 +562,15 @@ def _locator(value: Any) -> Iterator[Violation]: yield Violation("locator-keys", "/locator", "carries line_start and line_end " "together or neither") for key in ("line_start", "line_end"): - if key in value and not _line(value[key]): + if key not in value: + continue + line = value[key] + if isinstance(line, int) and not isinstance(line, bool) and not _writable(line): + yield Violation(NOT_JSON, f"/locator/{key}", "is an integer too long to write " + "as text") + elif not _line(line): yield Violation("locator-line-is-a-line-number", f"/locator/{key}", - f"is not a whole number no less than 1 ({_type(value[key])})") + f"is not a whole number no less than 1 ({_type(line)})") if "target" in value: target = value["target"] if not isinstance(target, str) or not target or _text_problem(target): @@ -681,8 +686,11 @@ def admitted(*fields: str) -> bool: if isinstance(locator, dict) and "line_start" in locator and "line_end" in locator: start, end = locator["line_start"], locator["line_end"] if end < start: + # The lines are not quoted: a refusal names no value of the finding, + # and a line may be any integer JSON can write. yield Violation("locator-span-is-ordered", "/locator/line_end", - f"the span ends at {end:g}, before it starts at {start:g}") + "line_end is less than line_start, so the span ends before " + "it starts") def check_finding(finding: Any) -> None: diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 1775e4aa..93a863b2 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -243,9 +243,6 @@ def _fields(finding: dict[str, Any]) -> dict[str, Any]: "a pathless collision": ("", "identity-collision", hc.collision_identity("opendox.no-sandbox.5ccbb79891acb2e8"), "human-only"), - "an uncited disappearance": ( - "notes/plan.md", "uncited-disappearance", - hc.disappearance_identity("opendox.broken-link.546cd2aacb1a6738"), "human-only"), } @@ -390,7 +387,6 @@ def test_the_vocabulary_is_the_packaged_copys() -> None: assert props["locator"]["properties"]["target"]["maxLength"] == STRING_MAX assert hc.OPENDOX == "opendox" assert hc.IDENTITY_COLLISION == "identity-collision" - assert hc.UNCITED_DISAPPEARANCE == "uncited-disappearance" def test_the_rule_catalog_is_the_copys_plus_the_engines_own() -> None: @@ -568,11 +564,10 @@ def test_a_name_outside_the_alphabet_is_refused(field: str, rule: str, value: An assert rule in _rules(finding) -@pytest.mark.parametrize("make", [hc.collision_identity, hc.disappearance_identity]) -def test_an_engine_identity_naming_a_41_character_part_is_refused(make) -> None: - assert make("p" * 40 + "." + "k" * 40 + "." + "0" * 16) +def test_a_collision_naming_a_41_character_part_is_refused() -> None: + assert hc.collision_identity("p" * 40 + "." + "k" * 40 + "." + "0" * 16) with pytest.raises(hc.FindingRefused) as refused: - make("p" * 41 + ".k." + "0" * 16) + hc.collision_identity("p" * 41 + ".k." + "0" * 16) assert refused.value.rule == "name-is-at-most-40-characters" @@ -785,6 +780,47 @@ def test_the_locator_is_display_only_and_bounded(locator: Any, rules: list[str]) assert _rules(finding) == rules +#: Integers JSON admits that Python's encoder cannot write: past the +#: integer-string conversion limit (4300 digits by default). +_UNWRITABLE = 10 ** 5000 + + +@pytest.mark.parametrize("locator, rules", [ + ({"line_start": _UNWRITABLE, "line_end": _UNWRITABLE}, + ["finding-is-json", "finding-is-json"]), + ({"line_start": 1, "line_end": _UNWRITABLE}, ["finding-is-json"]), + # openDox-code#97, Codex r4223401860: a 401-digit line is writable, and an + # out-of-order span of it is refused by its rule, never an OverflowError + ({"line_start": 10 ** 400, "line_end": 1}, ["locator-span-is-ordered"]), + ({"line_start": 1, "line_end": 10 ** 400}, []), + ({"line_start": 1e308, "line_end": 1}, ["locator-span-is-ordered"]), + ({"line_start": 1, "line_end": 1e308}, []), +], ids=["both unwritable", "line_end unwritable", "401 digits out of order", + "401 digits in order", "1e308 out of order", "1e308 in order"]) +def test_a_locator_of_any_size_is_judged_never_raised(locator: Any, rules: list[str]) -> None: + """Copilot r4223399258 and Codex r4223401860 on openDox-code#97: a checked + finding always serializes, and judging one never raises.""" + finding = {**_example("broken-link"), "locator": locator} + assert _rules(finding) == rules + if rules: + with pytest.raises(hc.FindingRefused): + hc.to_json(finding) + with pytest.raises(hc.FindingRefused): + hc.make_finding(**{**_fields(_example("broken-link")), "locator": locator}) + else: + assert json.loads(hc.to_json(finding))["locator"] == locator + + +def test_evidence_of_any_size_is_judged_never_raised() -> None: + finding = {**_example("broken-link"), "evidence": {"a": [_UNWRITABLE], "b": 10 ** 400}} + assert [(v.rule, v.where) for v in hc.violations(finding)] == [ + ("finding-is-json", "/evidence/a/0")] + with pytest.raises(hc.FindingRefused): + hc.to_json(finding) + finding["evidence"] = {"b": 10 ** 400} + assert json.loads(hc.to_json(finding))["evidence"] == {"b": 10 ** 400} + + @pytest.mark.parametrize("path, admitted", [ ("notes/plan.md", True), ("", True), ("a/..b/c..", True), ("caf\u00e9.md", True), ("/etc/passwd", False), ("C:notes.md", False), ("c:/notes.md", False), @@ -1025,22 +1061,18 @@ def test_the_pathless_identity_refuses_what_is_not_the_engines(category: Any, assert refused.value.rule == "pathless-identity-is-category-and-entry" -@pytest.mark.parametrize("make, rule", [ - (hc.collision_identity, "collision-identity-is-the-collided-id"), - (hc.disappearance_identity, "id-is-well-formed")]) @pytest.mark.parametrize("value", ["", "opendox.broken-link", "opendox.broken-link.XYZ", "opendox.broken-link.546cd2aacb1a673", "opendox.broken-link.546cd2aacb1a6738\n", None]) -def test_an_engine_identity_names_a_well_formed_id(make, rule: str, value: Any) -> None: +def test_a_collision_names_a_well_formed_id(value: Any) -> None: with pytest.raises(hc.FindingRefused) as refused: - make(value) - assert refused.value.rule == rule + hc.collision_identity(value) + assert refused.value.rule == "collision-identity-is-the-collided-id" def test_the_engines_identities_are_their_keys() -> None: fid = "opendox.broken-link.546cd2aacb1a6738" assert hc.collision_identity(fid) == {"collided_id": fid} - assert hc.disappearance_identity(fid) == {"disappeared_id": fid} assert hc.pathless_identity("no-sandbox") == {"category": "no-sandbox", "entry": ""} @@ -1175,7 +1207,8 @@ def test_a_violation_reads_as_its_rule_its_place_and_its_detail() -> None: assert violation.rule == "locator-span-is-ordered" assert violation.where == "/locator/line_end" assert violation.line() == f"[locator-span-is-ordered] /locator/line_end: {violation.detail}" - assert "12" in violation.detail and "14" in violation.detail + # it names no value of the finding, the lines included + assert "12" not in violation.detail and "14" not in violation.detail refused = hc.FindingRefused(violation) assert isinstance(refused, ValueError) assert (refused.rule, refused.where) == (violation.rule, violation.where) From 8e2124e61abcdb8d1dcc45c7759fc339083bbdce Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:03:46 +0000 Subject: [PATCH 04/17] T041: SonarCloud's ten new issues on openDox-code#97 at 8e4065b2 (plan 038) health_contract.py: _walk split into _walk, _members and _scalar (cognitive complexity 31, S3776); the pointers /identity, /locator and /resolution_class named once each (S1192). No behaviour moves: 342 passed over the two test files, and mutants 61 of 61 killed. tests/test_health_contract.py: each pytest.raises block holds one call that may raise (S5778, three); the two composite assertions split (S9073); the one assertion that put the expected value first rewritten (S3415). Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 99 ++++++++++++++++++++-------------- tests/test_health_contract.py | 17 +++--- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 0d9f5ed8..ccd2ebef 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -253,6 +253,11 @@ NOT_JSON = "finding-is-json" ENGINE_RULES: tuple[str, ...] = (NAME_CAP, SIZE_CAP, NESTING_CAP, NOT_JSON) +# The pointers of the fields more than one rule reports at. +_AT_IDENTITY = "/identity" +_AT_LOCATOR = "/locator" +_AT_RESOLUTION_CLASS = "/resolution_class" + # The schema's patterns, read as Python's `fullmatch`, which admits no trailing # newline (the schema's `$(?!\n)`). _NAME = re.compile(r"[a-z0-9-]+") @@ -443,45 +448,59 @@ def _walk(root: dict[Any, Any], where: str, field: str) -> Iterator[Violation]: """Every rule `identity`'s or `evidence`'s content breaks, at any depth, by an explicit stack and never by recursion. A refused key's value is not entered, so no pointer carries a key this module refused.""" - names_rule = f"{field}-names-no-text" - strings_rule = f"{field}-strings-are-short-text" stack: list[tuple[Any, str, int]] = [(root, where, 1)] while stack: value, at, level = stack.pop() - if isinstance(value, (dict, list)) and level > NESTING_MAX: + if not isinstance(value, (dict, list)): + yield from _scalar(value, at, field) + elif level > NESTING_MAX: yield Violation(NESTING_CAP, at, f"nests deeper than the engine's {NESTING_MAX} " "levels of objects and arrays") elif isinstance(value, dict): - inner: list[tuple[Any, str, int]] = [] - for key, item in value.items(): - if not isinstance(key, str): - yield Violation(NOT_JSON, at, f"has a key that is not text ({_type(key)})") - elif key in FORBIDDEN_KEYS: - yield Violation(names_rule, at, f"has a key named {key}, which names " - "document text") - elif (problem := _text_problem(key)) is not None: - yield Violation(strings_rule, at, f"has a key that {problem}") - else: - inner.append((item, f"{at}/{_escape(key)}", level + 1)) + refused, inner = _members(value, at, level, field) + yield from refused stack.extend(reversed(inner)) - elif isinstance(value, list): + else: stack.extend(reversed([(item, f"{at}/{index}", level + 1) for index, item in enumerate(value)])) - elif isinstance(value, str): - if (problem := _text_problem(value)) is not None: - yield Violation(strings_rule, at, f"is a string that {problem}") - elif value is None or isinstance(value, bool): - continue - elif isinstance(value, (int, float)): - if field == "identity": - yield Violation("identity-holds-no-number", at, - "is a number, which an identity never holds") - elif isinstance(value, float) and not math.isfinite(value): - yield Violation(NOT_JSON, at, "is a number JSON cannot write") - elif isinstance(value, int) and not _writable(value): - yield Violation(NOT_JSON, at, "is an integer too long to write as text") + + +def _members(value: dict[Any, Any], at: str, level: int, field: str + ) -> tuple[list[Violation], list[tuple[Any, str, int]]]: + """An object's refused keys, and the members to enter: those whose keys + every rule admits.""" + refused: list[Violation] = [] + inner: list[tuple[Any, str, int]] = [] + for key, item in value.items(): + if not isinstance(key, str): + refused.append(Violation(NOT_JSON, at, f"has a key that is not text ({_type(key)})")) + elif key in FORBIDDEN_KEYS: + refused.append(Violation(f"{field}-names-no-text", at, + f"has a key named {key}, which names document text")) + elif (problem := _text_problem(key)) is not None: + refused.append(Violation(f"{field}-strings-are-short-text", at, + f"has a key that {problem}")) else: - yield Violation(NOT_JSON, at, f"is {_type(value)}") + inner.append((item, f"{at}/{_escape(key)}", level + 1)) + return refused, inner + + +def _scalar(value: Any, at: str, field: str) -> Iterator[Violation]: + """The rules a value that is no object or array breaks.""" + if isinstance(value, str): + if (problem := _text_problem(value)) is not None: + yield Violation(f"{field}-strings-are-short-text", at, f"is a string that {problem}") + elif value is None or isinstance(value, bool): + return + elif not isinstance(value, (int, float)): + yield Violation(NOT_JSON, at, f"is {_type(value)}") + elif field == "identity": + yield Violation("identity-holds-no-number", at, + "is a number, which an identity never holds") + elif isinstance(value, float) and not math.isfinite(value): + yield Violation(NOT_JSON, at, "is a number JSON cannot write") + elif isinstance(value, int) and not _writable(value): + yield Violation(NOT_JSON, at, "is an integer too long to write as text") def _identity(value: Any, *, path: Any = None, kind: Any = None) -> Iterator[Violation]: @@ -489,10 +508,10 @@ def _identity(value: Any, *, path: Any = None, kind: Any = None) -> Iterator[Vio its finding's path and kind call for (when given), and the engine's size cap, judged last and only over an identity every other rule admits.""" if not isinstance(value, dict): - yield Violation("identity-is-an-object", "/identity", f"is {_type(value)}, not an " + yield Violation("identity-is-an-object", _AT_IDENTITY, f"is {_type(value)}, not an " "object") return - found = list(_walk(value, "/identity", "identity")) + found = list(_walk(value, _AT_IDENTITY, "identity")) if kind == IDENTITY_COLLISION: found += _collision_form(value) elif path == "": @@ -501,14 +520,14 @@ def _identity(value: Any, *, path: Any = None, kind: Any = None) -> Iterator[Vio if not found: size = len(canonical_json(value)) if size > IDENTITY_MAX_BYTES: - yield Violation(SIZE_CAP, "/identity", f"its canonical JSON is {size} bytes; the " + yield Violation(SIZE_CAP, _AT_IDENTITY, f"its canonical JSON is {size} bytes; the " f"engine's cap is {IDENTITY_MAX_BYTES}") def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: rule = "pathless-identity-is-category-and-entry" if set(identity) != {"category", "entry"}: - yield Violation(rule, "/identity", "a pathless finding's identity is exactly " + yield Violation(rule, _AT_IDENTITY, "a pathless finding's identity is exactly " "{category, entry}, the engine's own") return if not isinstance(identity["category"], str) or not _NAME.fullmatch(identity["category"]): @@ -522,7 +541,7 @@ def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: def _collision_form(identity: dict[Any, Any]) -> Iterator[Violation]: rule = "collision-identity-is-the-collided-id" if set(identity) != {"collided_id"}: - yield Violation(rule, "/identity", "a collision's identity is exactly " + yield Violation(rule, _AT_IDENTITY, "a collision's identity is exactly " "{collided_id}, the engine's own") return yield from _id(identity["collided_id"], "/identity/collided_id", rule) @@ -552,14 +571,14 @@ def _line(value: Any) -> bool: def _locator(value: Any) -> Iterator[Violation]: if not isinstance(value, dict): - yield Violation("locator-keys", "/locator", f"is {_type(value)}, not an object") + yield Violation("locator-keys", _AT_LOCATOR, f"is {_type(value)}, not an object") return keys = set(value) if not keys or keys - {"line_start", "line_end", "target"}: - yield Violation("locator-keys", "/locator", "carries line_start and line_end " + yield Violation("locator-keys", _AT_LOCATOR, "carries line_start and line_end " "together, target, or both, and no other key") elif ("line_start" in keys) != ("line_end" in keys): - yield Violation("locator-keys", "/locator", "carries line_start and line_end " + yield Violation("locator-keys", _AT_LOCATOR, "carries line_start and line_end " "together or neither") for key in ("line_start", "line_end"): if key not in value: @@ -627,7 +646,7 @@ def violations(finding: Any) -> list[Violation]: kind=finding.get("kind")), "locator": _locator, "severity": lambda v: _one_of(v, SEVERITIES, "/severity", "severity-is-known"), - "resolution_class": lambda v: _one_of(v, RESOLUTION_CLASSES, "/resolution_class", + "resolution_class": lambda v: _one_of(v, RESOLUTION_CLASSES, _AT_RESOLUTION_CLASS, "resolution-class-is-one-of-three"), "message": _message, "evidence": _evidence, @@ -654,11 +673,11 @@ class outside the three is reported once, by its own rule.""" if not known or finding["resolution_class"] == HUMAN_ONLY: return if finding.get("path") == "": - yield Violation("pathless-finding-is-human-only", "/resolution_class", + yield Violation("pathless-finding-is-human-only", _AT_RESOLUTION_CLASS, "a pathless finding names no document a repair could edit, so it " "is human-only") if finding.get("kind") == IDENTITY_COLLISION: - yield Violation("identity-collision-is-human-only", "/resolution_class", + yield Violation("identity-collision-is-human-only", _AT_RESOLUTION_CLASS, "an identity collision is human-only") diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 93a863b2..004de329 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -271,7 +271,7 @@ def test_the_serialized_identity_holds_the_schemas_bound(name: str) -> None: if isinstance(v, (int, float)) and not isinstance(v, bool)], values # every string, key or value, at most 200 characters strings = keys + [v for v in values if isinstance(v, str)] - assert strings == [] or max(len(s) for s in strings) <= STRING_MAX + assert all(len(s) <= STRING_MAX for s in strings), [len(s) for s in strings] # the engine's own keys if emitted["kind"] == "identity-collision": assert sorted(identity) == ["collided_id"] @@ -654,8 +654,9 @@ def test_the_nesting_bound_admits_its_bound_and_refuses_one_level_over(field: st assert hc.NESTING_MAX == 16 fields = _fields(_example("orphan")) hc.make_finding(**{**fields, field: _nested(16)}) + over = {**fields, field: _nested(17)} with pytest.raises(hc.FindingRefused) as refused: - hc.make_finding(**{**fields, field: _nested(17)}) + hc.make_finding(**over) assert refused.value.rule == "nesting-is-within-the-cap" @@ -669,8 +670,9 @@ def test_a_deep_value_is_refused_never_a_recursion_error(field: str) -> None: assert "nesting-is-within-the-cap" in _rules(finding) with pytest.raises(hc.FindingRefused): hc.to_json(finding) + fields = {**_fields(_example("orphan")), field: deep} with pytest.raises(hc.FindingRefused): - hc.make_finding(**{**_fields(_example("orphan")), field: deep}) + hc.make_finding(**fields) # --------------------------------------------------------------------------- @@ -805,8 +807,9 @@ def test_a_locator_of_any_size_is_judged_never_raised(locator: Any, rules: list[ if rules: with pytest.raises(hc.FindingRefused): hc.to_json(finding) + fields = {**_fields(_example("broken-link")), "locator": locator} with pytest.raises(hc.FindingRefused): - hc.make_finding(**{**_fields(_example("broken-link")), "locator": locator}) + hc.make_finding(**fields) else: assert json.loads(hc.to_json(finding))["locator"] == locator @@ -1161,7 +1164,8 @@ def test_the_module_runs_alone_with_no_site_packages(tmp_path: Path) -> None: env={"PATH": os.environ.get("PATH", ""), "LANG": "C.UTF-8"}) assert done.returncode == 0, done.stderr found = json.loads(done.stdout.strip().splitlines()[-1]) - assert found["site"] is False and found["paths"] == [] + assert found["site"] is False + assert found["paths"] == [] assert Path(found["file"]).parent == package here = hc.make_finding(kind="broken-link", pack_id="opendox", pack_version="0.2.0", path="notes/plan.md", identity={"target": "../old/brief.md"}, @@ -1208,7 +1212,8 @@ def test_a_violation_reads_as_its_rule_its_place_and_its_detail() -> None: assert violation.where == "/locator/line_end" assert violation.line() == f"[locator-span-is-ordered] /locator/line_end: {violation.detail}" # it names no value of the finding, the lines included - assert "12" not in violation.detail and "14" not in violation.detail + assert "12" not in violation.detail + assert "14" not in violation.detail refused = hc.FindingRefused(violation) assert isinstance(refused, ValueError) assert (refused.rule, refused.where) == (violation.rule, violation.where) From 674efcbb1c4f334147ba9f513a0061b2f242fa22 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:40:04 +0000 Subject: [PATCH 05/17] T041: the holder's rulings 6069024023 and lane 3's REVIEW-W1 EARLY (plan 038) Item 1: the nine categories stand as drafted, and manifest-refused and dispositions-refused are added; pathless_identity() admits both. What is the contract module's to encode of the item's mappings: a category is its finding's kind (ENGINE_KINDS, for T045's refusal of a pack declaring one); one pack_id per category (ENTRY_CATEGORIES: against the entry's pack, the rest against opendox); and engine_finding() builds the one finding per category and entry, human-only, pathless, with one id across runs. Item 2, option (D): disappearance_identity(original) returns a pathed original's {disappeared_id} and a pathless original's {category: , entry: }. UNCITED_DISAPPEARANCE is back. Both forms are tested, and so is the refusal of {disappeared_id} on an empty path. Lane 3's REVIEW-W1 EARLY: the non-text field-name refusal is tested (its mutant survived); the docstrings now say that where names admitted keys verbatim and is unbounded, and a test pins it; the T045 hand-off cites plan.md:458; lane 3's reversed 401-digit span is a test row. Two test files: 379 passed. Mutants: 75 of 75 killed. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 139 +++++++++++++++++++---- tests/test_health_contract.py | 197 ++++++++++++++++++++++++++++++--- 2 files changed, 297 insertions(+), 39 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index ccd2ebef..af12c768 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -71,25 +71,40 @@ `locator-span-is-ordered`. As in openDox-spec's own test, a reference rule judges only parts whose own rules hold, so one cause is reported once. -A REFUSAL QUOTES NOTHING OF THE FINDING. A finding may carry document text where -it must not, which is why it is refused. So no violation's text holds any of -the finding's own strings: it names the rule, the place (a JSON pointer built -only from keys this module admitted) and a size, a type or the admitted set. -A finding's names and hashes, once admitted, may be named. - -THE ENGINE'S OWN IDENTITIES (contracts/health-finding.md § The id rule). -`pathless_identity()` and `collision_identity()` build the two keys T041's -task names for the engine's own findings, and `CATEGORIES` fixes the failure -categories of an install-level or pre-run finding, so each such finding keeps -one id across runs. The third engine key, `{disappeared_id}` for the re-raise -of an uncited disappearance, is the baseline's (T046), and is not built here: -for a pathless original, the finding schema's -`pathless-identity-is-category-and-entry` rule refuses it, which is the -holder's to rule (openDox-code#97, "For the holder", item 2). +A REFUSAL QUOTES NOTHING IT REFUSED. A finding may carry document text where +it must not, which is why it is refused. So no refused string, key or value +reaches a violation: its `detail` names a size, a type or the admitted set, +never a value of the finding, and a finding's names and hashes are named only +once admitted. Its `where` is a JSON pointer built only from keys this module +ADMITTED, so it does name those keys, verbatim: each is at most 200 +characters and no forbidden word, but a key may still be document text (a +heading key is), and `NESTING_MAX` of them make a pointer far longer than any +finding field. So `rule` is what an engine records about a refusal; one that +records `where` too bounds it as it bounds any string it stores. + +THE ENGINE'S OWN FINDINGS (contracts/health-finding.md § The id rule; the +holder's rulings `openxFactory#656` `6069024023`, items 1 and 2). + +* `CATEGORIES` fixes the failure categories of an install-level or pre-run + finding. A category is also that finding's `kind`, its `pack_id` is fixed + per category (`ENTRY_CATEGORIES`), and its identity is `{category, entry}`, + so the engine raises ONE such finding per category and entry in a run, + with the reasons in `evidence`, and it keeps one id across runs. + `pathless_identity()` builds the identity and `engine_finding()` the whole + finding. +* `collision_identity()` builds a collision's `{collided_id}`. +* `disappearance_identity()` builds the identity of the re-raise of an + uncited disappearance, in the form its original's path calls for: a pathed + original's `{disappeared_id}`, and for a pathless original the pathless form + the schema requires, `{category: , entry: }` (option (D)). T046 raises it. +* `ENGINE_KINDS` are the kinds only the engine raises, which T045 refuses in a + pack's declaration. T045 APPENDS the pack protocol to this module (the static declaration, the stdout document and the patch type) at T041's landing, a cross-lane hand-off -(plan.md § single-writer files). +(plan.md:458, § "Parallel slices, and the files only one writer may touch at +a time"). A CREATED FILE: it has no row in openxFactory's `docs/opendox-carve-manifest.yaml`, because the manifest declares what LEAVES @@ -111,7 +126,9 @@ "BASELINE_CLASSES", "CATEGORIES", "ENGINE_FIELDS", + "ENGINE_KINDS", "ENGINE_RULES", + "ENTRY_CATEGORIES", "FIELDS", "FORBIDDEN_KEYS", "FindingRefused", @@ -130,10 +147,13 @@ "SEVERITIES", "SHAPE_RULES", "STRING_MAX", + "UNCITED_DISAPPEARANCE", "Violation", "canonical_json", "check_finding", "collision_identity", + "disappearance_identity", + "engine_finding", "finding_id", "id_key", "identity_json", @@ -170,6 +190,10 @@ #: in one run shared one id. IDENTITY_COLLISION = "identity-collision" +#: The kind of the engine's once-only re-raise of an uncited disappearance +#: (data-model.md § Baseline classes; T046 raises it). +UNCITED_DISAPPEARANCE = "uncited-disappearance" + #: A finding's fields, in the schema's order. REQUIRED_FIELDS: tuple[str, ...] = ( "id", "kind", "pack_id", "pack_version", "path", "identity", "severity", @@ -215,7 +239,9 @@ #: The failure categories of an install-level or pre-run finding, whose #: identity is the engine's own `{category, entry}` (contracts/health-finding.md #: § The id rule; T041 fixes them with the id rule). `entry` is the manifest -#: entry's id, or "" for none. +#: entry's id, or "" for none. The nine T041 drafted are ACCEPTED as written, +#: and the last two are ADDED, by the holder's `6069024023` item 1. A category +#: is also its finding's `kind`. CATEGORIES: tuple[str, ...] = ( "no-sandbox", # no live sandbox, so no pack ran (R2Q16 (a)) "entry-refused", # a manifest entry refused by name (15.1a) @@ -224,10 +250,32 @@ "declaration-refused", # a pack's `opendox-pack.yaml` refused (15.2; OQ-H15-10) "pack-crashed", # a pack that exited non-zero or on a signal (15.6) "pack-timed-out", # a pack that ran past its time budget (15.6) - "pack-bound-hit", # a pack that hit one of the engine's bounds (15.6) - "pack-output-refused", # a pack's stdout the engine refused (15.5, 15.6) + "pack-bound-hit", # a pack that hit one of the engine's bounds, its + # stdout cap included (15.6) + "pack-output-refused", # a pack's stdout that is not one JSON document, or + # breaks the contract; refused whole (15.5, 15.6) + "manifest-refused", # `health/packs.yaml` refused as a whole; entry "" (15.1a) + "dispositions-refused", # `health/dispositions.yaml` refused; entry "" + # (contracts/health-exceptions.md; T054 raises it) ) +#: The categories whose finding is against the PACK its manifest entry +#: launched: the finding's `pack_id` is the entry's id, which is its pack's id +#: (15.7), so `entry` is never empty for them. Every other category's finding is +#: against the product itself, `opendox`: no live sandbox, a refused manifest or +#: dispositions file, and a refused entry, whose id may be malformed, reserved +#: or repeated and so is no pack id to attribute a finding to. One `pack_id` +#: per category (the holder's `6069024023` item 1). +ENTRY_CATEGORIES: frozenset[str] = frozenset({ + "fetch-failed", "digest-mismatch", "declaration-refused", "pack-crashed", + "pack-timed-out", "pack-bound-hit", "pack-output-refused"}) + +#: The kinds only the ENGINE raises: its categories, a collision and the +#: re-raise of a disappearance. A pack declares none of them (T045 refuses +#: one; the holder's `6069024023` item 1). +ENGINE_KINDS: frozenset[str] = frozenset(CATEGORIES) | {IDENTITY_COLLISION, + UNCITED_DISAPPEARANCE} + #: The copy's catalog: the rules its own keywords state, and the three #: cross-field rules no JSON Schema keyword can state. SHAPE_RULES: tuple[str, ...] = ( @@ -271,7 +319,8 @@ class Violation(NamedTuple): """One rule a finding breaks: the rule's id, where (a JSON pointer into the - finding), and what was found. No part quotes the finding's own text.""" + finding, built from keys this module admitted, which it names verbatim), + and what was found. No part quotes a string, key or value it refused.""" rule: str where: str @@ -283,8 +332,9 @@ def line(self) -> str: class FindingRefused(ValueError): """A finding, or a part of one, breaks a rule of the contract. `rule` and - `where` are the first violation's; an engine records those, and never the - refused value.""" + `where` are the first violation's. An engine records `rule`, and never the + refused value; `where` names admitted keys verbatim and is unbounded in + length, so an engine that records it bounds it first.""" def __init__(self, violation: Violation, more: int = 0) -> None: self.violation = violation @@ -367,6 +417,25 @@ def collision_identity(collided_id: str) -> dict[str, str]: return {"collided_id": collided_id} +def disappearance_identity(original: dict[str, Any]) -> dict[str, str]: + """The identity of the engine's re-raise of an uncited disappearance, in + the form the ORIGINAL's path calls for (option (D), the holder's + `6069024023` item 2). T046 raises the re-raise: kind + `uncited-disappearance`, the original's path, human-only, and the + original's id and the baseline run in `evidence`. + + * A pathed original's: `{"disappeared_id": }`. + * A pathless original's: the pathless form the schema requires, + `{"category": , "entry": }`. A + pathless collision has no entry, so its entry is "". + + `original` is the finding as stored, checked whole first.""" + check_finding(original) + if original["path"] != "": + return {"disappeared_id": original["id"]} + return {"category": original["kind"], "entry": original["identity"].get("entry", "")} + + # --------------------------------------------------------------------------- # the rules # --------------------------------------------------------------------------- @@ -752,6 +821,32 @@ def make_finding(*, kind: str, pack_id: str, pack_version: str, path: str, return copy.deepcopy(finding) +def engine_finding(category: str, entry: str = "", *, pack_version: str, severity: str, + message: str, evidence: dict[str, Any] | None = None) -> dict[str, Any]: + """An install-level or pre-run finding, as the engine raises it (the + holder's `6069024023` item 1). Its `kind` is its category, its `path` is + empty, its identity is `{category, entry}`, and it is human-only. Its + `pack_id` is the one its category fixes: for `ENTRY_CATEGORIES`, `entry`, + the manifest entry's id, which is never empty and never `opendox`; for + every other category, `opendox`. `pack_version` is that pack's: the + entry's version, or the installed version for `opendox`. The reasons ride + in `evidence`, so a run raises one such finding per category and entry, + and it keeps one id across runs.""" + identity = pathless_identity(category, entry) + pack_id = OPENDOX + if category in ENTRY_CATEGORIES: + if entry in ("", OPENDOX): + raise FindingRefused(Violation( + "pathless-identity-is-category-and-entry", "/identity/entry", + f"a {category} finding is against the pack its manifest entry " + f"launched, so its entry is that pack's id, never empty and never " + f"{OPENDOX}")) + pack_id = entry + return make_finding(kind=category, pack_id=pack_id, pack_version=pack_version, path="", + identity=identity, severity=severity, resolution_class=HUMAN_ONLY, + message=message, evidence=evidence) + + def to_json(finding: dict[str, Any]) -> str: """The finding as this module serializes it: checked whole, then canonical JSON (sorted keys, no whitespace, each character as itself). Nothing the diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 004de329..452c4c37 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -44,8 +44,11 @@ 7. EVERY RULE THE COPY CATALOGUES IS ENFORCED. Each of its 27 rules has one negative case, openDox-spec's own negative example rebuilt, refused for that rule and no other; so are the module's four engine rules. -8. THE ENGINE'S OWN IDENTITIES AND CATEGORIES, and openDox-spec's five - examples rebuilt through `make_finding()`, id included. +8. THE ENGINE'S OWN FINDINGS (the holder's `6069024023`): the eleven ruled + categories, each one finding per category and entry with one id across + runs and one `pack_id` per category (`engine_finding()`); the disappearance + re-raise in both forms, option (D); and openDox-spec's five examples + rebuilt through `make_finding()` and `engine_finding()`, id included. 9. THE CLOSURE: the module imports the standard library and nothing else. Its imports are read from its source, it is imported in a fresh interpreter that blocks openXdox and openxFactory, and it runs from a directory that @@ -240,6 +243,16 @@ def _fields(finding: dict[str, Any]) -> dict[str, Any]: "a collision": ("notes/plan.md", "identity-collision", hc.collision_identity("house-style.heading-case.0f1e2d3c4b5a6978"), "human-only"), + "a refused manifest, the whole file": ( + "", "manifest-refused", hc.pathless_identity("manifest-refused"), "human-only"), + "a refused dispositions file": ( + "", "dispositions-refused", hc.pathless_identity("dispositions-refused"), "human-only"), + "the disappearance of a pathed finding": ( + "notes/plan.md", "uncited-disappearance", + hc.disappearance_identity(EXAMPLES["broken-link"]), "human-only"), + "the disappearance of a pathless finding": ( + "", "uncited-disappearance", + hc.disappearance_identity(EXAMPLES["no-sandbox"]), "human-only"), "a pathless collision": ("", "identity-collision", hc.collision_identity("opendox.no-sandbox.5ccbb79891acb2e8"), "human-only"), @@ -387,6 +400,7 @@ def test_the_vocabulary_is_the_packaged_copys() -> None: assert props["locator"]["properties"]["target"]["maxLength"] == STRING_MAX assert hc.OPENDOX == "opendox" assert hc.IDENTITY_COLLISION == "identity-collision" + assert hc.UNCITED_DISAPPEARANCE == "uncited-disappearance" def test_the_rule_catalog_is_the_copys_plus_the_engines_own() -> None: @@ -794,11 +808,14 @@ def test_the_locator_is_display_only_and_bounded(locator: Any, rules: list[str]) # openDox-code#97, Codex r4223401860: a 401-digit line is writable, and an # out-of-order span of it is refused by its rule, never an OverflowError ({"line_start": 10 ** 400, "line_end": 1}, ["locator-span-is-ordered"]), + # lane 3's REVIEW-W1 MAJOR probe: a reversed span of 401-digit integers + ({"line_start": 10 ** 400, "line_end": 10 ** 399}, ["locator-span-is-ordered"]), ({"line_start": 1, "line_end": 10 ** 400}, []), ({"line_start": 1e308, "line_end": 1}, ["locator-span-is-ordered"]), ({"line_start": 1, "line_end": 1e308}, []), ], ids=["both unwritable", "line_end unwritable", "401 digits out of order", - "401 digits in order", "1e308 out of order", "1e308 in order"]) + "lane 3's reversed 401-digit span", "401 digits in order", "1e308 out of order", + "1e308 in order"]) def test_a_locator_of_any_size_is_judged_never_raised(locator: Any, rules: list[str]) -> None: """Copilot r4223399258 and Codex r4223401860 on openDox-code#97: a checked finding always serializes, and judging one never raises.""" @@ -955,6 +972,15 @@ def test_a_finding_without_a_required_field_is_refused(missing: str) -> None: assert "finding-keys" in _rules(finding) +def test_a_field_name_that_is_not_text_is_refused() -> None: + """Lane 3's REVIEW-W1 MINOR: with this refusal gone, the finding was + admitted and `to_json()` raised `TypeError`.""" + finding = {**_example("broken-link"), 1: "x"} + assert [(v.rule, v.where) for v in hc.violations(finding)] == [("finding-is-json", "")] + with pytest.raises(hc.FindingRefused): + hc.to_json(finding) + + @pytest.mark.parametrize("finding", [[], "a finding", None, 3]) def test_a_finding_that_is_not_an_object_is_refused(finding: Any) -> None: assert [(v.rule, v.where) for v in hc.violations(finding)] == [("finding-keys", "")] @@ -1034,24 +1060,88 @@ def test_to_json_is_the_canonical_serialization() -> None: assert "caf\u00e9" in text -def test_the_categories_are_names_and_the_copys_example_is_one() -> None: - assert "no-sandbox" in hc.CATEGORIES - assert len(set(hc.CATEGORIES)) == len(hc.CATEGORIES) +#: The categories as the holder ruled them (`6069024023` item 1): the nine +#: T041 drafted, accepted as written, and the two added. +RULED_CATEGORIES = ("no-sandbox", "entry-refused", "fetch-failed", "digest-mismatch", + "declaration-refused", "pack-crashed", "pack-timed-out", + "pack-bound-hit", "pack-output-refused", "manifest-refused", + "dispositions-refused") + +#: The categories whose finding is against the product itself; the rest are +#: against the pack their manifest entry launched. +AGAINST_OPENDOX = {"no-sandbox", "entry-refused", "manifest-refused", "dispositions-refused"} + + +def test_the_categories_are_the_ruled_ones_and_names() -> None: + assert hc.CATEGORIES == RULED_CATEGORIES + assert "no-sandbox" in hc.CATEGORIES # openDox-spec's own released example for category in hc.CATEGORIES: assert re.fullmatch(r"[a-z0-9-]{1,40}", category), category + assert set(hc.CATEGORIES) - hc.ENTRY_CATEGORIES == AGAINST_OPENDOX + assert hc.ENTRY_CATEGORIES < set(hc.CATEGORIES) + assert hc.ENGINE_KINDS == set(RULED_CATEGORIES) | {"identity-collision", + "uncited-disappearance"} + + +@pytest.mark.parametrize("category", RULED_CATEGORIES) +def test_each_category_makes_one_engine_finding_per_entry_with_one_id(category: str) -> None: + """A category is its finding's kind, the path is empty, it is human-only, + one `pack_id` per category, and one id per category and entry across runs + (the holder's `6069024023` item 1).""" + entry = "house-style" + + def build(entry: str = entry, message: str = "the pack did not run") -> dict[str, Any]: + return hc.engine_finding(category, entry, pack_version="1.4.0", severity="warning", + message=message, evidence={"reasons": ["one", "two"]}) + + first = build() + assert first["id"] == build(message="another run, other words")["id"] + assert first["id"] != build(entry="other-pack")["id"] + assert (first["kind"], first["path"], first["resolution_class"]) == ( + category, "", "human-only") + assert first["identity"] == {"category": category, "entry": entry} + assert first["pack_id"] == ("opendox" if category in AGAINST_OPENDOX else entry) + assert first["evidence"] == {"reasons": ["one", "two"]} + assert hc.violations(first) == [] + + +@pytest.mark.parametrize("category", sorted(AGAINST_OPENDOX)) +def test_a_finding_against_the_product_may_name_no_entry(category: str) -> None: + built = hc.engine_finding(category, pack_version="0.2.0", severity="warning", + message="the run could not use it") + assert (built["pack_id"], built["identity"]) == ("opendox", {"category": category, + "entry": ""}) + + +@pytest.mark.parametrize("entry", ["", "opendox"]) +@pytest.mark.parametrize("category", sorted(set(RULED_CATEGORIES) - AGAINST_OPENDOX)) +def test_a_finding_against_a_pack_names_that_packs_entry(category: str, entry: str) -> None: + with pytest.raises(hc.FindingRefused) as refused: + hc.engine_finding(category, entry, pack_version="0.2.0", severity="warning", + message="the pack did not run") + assert refused.value.rule == "pathless-identity-is-category-and-entry" + + +def test_an_engine_finding_against_a_41_character_pack_is_refused() -> None: + hc.engine_finding("pack-crashed", "p" * 40, pack_version="1", severity="error", + message="the pack crashed") + with pytest.raises(hc.FindingRefused) as refused: + hc.engine_finding("pack-crashed", "p" * 41, pack_version="1", severity="error", + message="the pack crashed") + assert refused.value.rule == "name-is-at-most-40-characters" + + +def test_engine_finding_rebuilds_openDox_specs_no_sandbox_example() -> None: + example = _example("no-sandbox") + built = hc.engine_finding("no-sandbox", pack_version=example["pack_version"], + severity=example["severity"], message=example["message"], + evidence=example["evidence"]) + assert built == example -@pytest.mark.parametrize("category", hc.CATEGORIES) -def test_each_category_makes_a_pathless_finding_with_one_id_across_runs( - category: str) -> None: - def build() -> dict[str, Any]: - return hc.make_finding( - kind=category, pack_id="opendox", pack_version="0.2.0", path="", - identity=hc.pathless_identity(category, "house-style"), severity="warning", - resolution_class="human-only", message="the pack did not run") - first, second = build(), build() - assert first["id"] == second["id"] - assert first["identity"] == {"category": category, "entry": "house-style"} +@pytest.mark.parametrize("category", ["manifest-refused", "dispositions-refused"]) +def test_the_two_added_categories_are_admitted(category: str) -> None: + assert hc.pathless_identity(category) == {"category": category, "entry": ""} @pytest.mark.parametrize("category, entry", [ @@ -1079,6 +1169,68 @@ def test_the_engines_identities_are_their_keys() -> None: assert hc.pathless_identity("no-sandbox") == {"category": "no-sandbox", "entry": ""} +def _pathless_collision() -> dict[str, Any]: + return hc.make_finding(kind="identity-collision", pack_id="house-style", + pack_version="1.4.0", path="", + identity=hc.collision_identity( + "house-style.pack-crashed.0f1e2d3c4b5a6978"), + severity="error", resolution_class="human-only", + message="two findings of one run share one id") + + +#: Each original, and the identity its re-raise takes (option (D)). +_DISAPPEARED = { + "a pathed finding": (lambda: _example("broken-link"), + {"disappeared_id": "opendox.broken-link.546cd2aacb1a6738"}), + "a pathless finding": (lambda: _example("no-sandbox"), + {"category": "no-sandbox", "entry": ""}), + "a pathless finding about an entry": ( + lambda: hc.engine_finding("pack-crashed", "house-style", pack_version="1.4.0", + severity="error", message="the pack crashed"), + {"category": "pack-crashed", "entry": "house-style"}), + "a pathless collision": (_pathless_collision, + {"category": "identity-collision", "entry": ""}), +} + + +@pytest.mark.parametrize("name", list(_DISAPPEARED)) +def test_the_disappearance_takes_the_form_its_originals_path_calls_for(name: str) -> None: + """Option (D) (the holder's `6069024023` item 2): a pathed original's + re-raise keeps `{disappeared_id}` and the original's path; a pathless + original's takes the pathless form the schema requires. Either re-raise + is a finding the contract admits, under its own id.""" + make_original, identity = _DISAPPEARED[name] + original = make_original() + assert hc.disappearance_identity(original) == identity + reraise = hc.make_finding( + kind="uncited-disappearance", pack_id="opendox", pack_version="0.2.0", + path=original["path"], identity=hc.disappearance_identity(original), + severity="warning", resolution_class="human-only", + message="a finding disappeared with no landed repair citing it", + evidence={"disappeared_id": original["id"], "baseline_run": "run-1"}) + assert hc.violations(reraise) == [] + assert reraise["id"] != original["id"] + assert reraise["path"] == original["path"] + + +def test_the_disappeared_id_alone_is_refused_for_a_pathless_original() -> None: + """Why the pathless form exists: `{disappeared_id}` on an empty path breaks + the schema's `pathless-identity-is-category-and-entry`.""" + with pytest.raises(hc.FindingRefused) as refused: + hc.make_finding(kind="uncited-disappearance", pack_id="opendox", + pack_version="0.2.0", path="", + identity={"disappeared_id": EXAMPLES["no-sandbox"]["id"]}, + severity="warning", resolution_class="human-only", message="m") + assert refused.value.rule == "pathless-identity-is-category-and-entry" + + +def test_the_disappearance_of_a_malformed_original_is_refused() -> None: + original = _with("broken-link", id="opendox.broken-link.0000000000000000") + with pytest.raises(hc.FindingRefused) as refused: + hc.disappearance_identity(original) + assert refused.value.rule == "id-is-the-hash-of-its-key" + + # --------------------------------------------------------------------------- # 9. the closure: the standard library, and nothing else # --------------------------------------------------------------------------- @@ -1206,6 +1358,17 @@ def test_a_refusal_quotes_nothing_of_the_finding(change: dict[str, Any]) -> None assert "SECRET" not in str(refused.value) +def test_where_names_an_admitted_key_and_detail_names_none() -> None: + """Lane 3's REVIEW-W1 MINOR, as the docstrings now state it: `where` is a + pointer built from keys the module ADMITTED, and names them verbatim, so + an engine bounds it before it records it; `detail` names no key.""" + key = "ADMITTED-HEADING-KEY" + violation = hc.violations({**_example("broken-link"), "identity": {key: 5}})[0] + assert violation.rule == "identity-holds-no-number" + assert violation.where == f"/identity/{key}" + assert key not in violation.detail + + def test_a_violation_reads_as_its_rule_its_place_and_its_detail() -> None: violation = hc.violations(NEGATIVES["locator-span-is-ordered"])[0] assert violation.rule == "locator-span-is-ordered" From 920a1c699dcd361da7a1c93e8e29a4f64261b250 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 22:01:41 +0000 Subject: [PATCH 06/17] WIP T041 (park 2155): the single-hash-path test, red until the module routes its reference check through finding_id() (plan 038) The holder's 6069507373 (T046 item 2): the module-level finding_id() is the single hash path. This test patches it; it is red at this commit, because violations() still recomputes the hash with hashlib. The module change is the next act. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_health_contract.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 452c4c37..66609c01 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -517,6 +517,26 @@ def test_the_order_of_an_identitys_keys_does_not_move_the_id() -> None: assert len(ids) == 1 +def test_finding_id_is_the_one_hash_path(monkeypatch: pytest.MonkeyPatch) -> None: + """The module-level `finding_id()` is the SINGLE hash path (the holder's + `6069507373`, T046 item 2): a test that patches it patches both the id + `make_finding()` sets and the id the reference rule checks.""" + real = hc.finding_id + seen: list[dict[str, Any]] = [] + + def patched(**key: Any) -> str: + seen.append(key) + return real(**key)[:-16] + "0" * 16 + + monkeypatch.setattr(hc, "finding_id", patched) + built = hc.make_finding(**_fields(_example("broken-link"))) + assert built["id"] == "opendox.broken-link.0000000000000000" + assert len(seen) == 2, "the id it set, and the id its own check recomputed" + assert hc.violations(built) == [] + assert _rules(_example("broken-link")) == ["id-is-the-hash-of-its-key"] + assert len(seen) == 4 + + def test_a_pack_cannot_set_the_engines_fields() -> None: """The id, and nothing a family supplies, is the engine's: `make_finding()` takes no id.""" From f9cc4b0c59181171127baed27c89b20e8740fd87 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:26:10 +0000 Subject: [PATCH 07/17] T041: finding_id() is the one hash path; a refusal's where is bounded (plan 038) The holder's 6069507373 (T046 item 2): the reference rule's hash check now calls the module-level finding_id(), as make_finding() already did, so a test that patches health_contract.finding_id patches every id the module sets or checks. test_finding_id_is_the_one_hash_path, red at the previous commit, is green; the mutants that restore a direct hashlib path are killed. Lane 3's REVIEW-W1 MINOR at 8e2124e6, bounded as the holder preferred: a violation's where is at most STRING_MAX characters, the place's own pointer or its nearest ancestor's that fits, and a pointer that stops stays stopped, so it never names a path that is not there. Fifteen nested 200-character keys of "/" made a 6026-character pointer; now it is /identity. The docstrings say so. Two test files: 389 passed. Red first: 6 failed against the previous module. Mutants: 80 of 80 killed. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 86 ++++++++++++++++++++++------------ tests/test_health_contract.py | 43 +++++++++++++++-- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index af12c768..35f37cf4 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -35,7 +35,8 @@ display only and outside the hash, so an edit above a finding moves its locator and keeps its id, and an exception keyed by that id keeps suppressing it. The ENGINE sets the id: `make_finding()` takes none, so a pack's own value never -reaches a finding. +reaches a finding. `finding_id()` is the module's ONE hash path: every id it +sets or checks is computed there. `identity` IS STORED AND EMITTED (the holder, `openxFactory#656` `6018624750`): the engine hashes it into the id, T042 stores it as canonical sorted-key JSON @@ -71,16 +72,17 @@ `locator-span-is-ordered`. As in openDox-spec's own test, a reference rule judges only parts whose own rules hold, so one cause is reported once. -A REFUSAL QUOTES NOTHING IT REFUSED. A finding may carry document text where -it must not, which is why it is refused. So no refused string, key or value -reaches a violation: its `detail` names a size, a type or the admitted set, -never a value of the finding, and a finding's names and hashes are named only -once admitted. Its `where` is a JSON pointer built only from keys this module -ADMITTED, so it does name those keys, verbatim: each is at most 200 -characters and no forbidden word, but a key may still be document text (a -heading key is), and `NESTING_MAX` of them make a pointer far longer than any -finding field. So `rule` is what an engine records about a refusal; one that -records `where` too bounds it as it bounds any string it stores. +A REFUSAL QUOTES NOTHING IT REFUSED, AND ITS POINTER IS BOUNDED. A finding may +carry document text where it must not, which is why it is refused. So no +refused string, key or value reaches a violation: its `detail` names a size, a +type or the admitted set, never a value of the finding, and a finding's names +and hashes are named only once admitted. Its `where` is a JSON pointer built +only from keys this module ADMITTED, so it names those verbatim (each within +the contract's own bound on a key) and never a refused one, and it is at most +`STRING_MAX` characters: the place's own pointer, or, when that is longer, its +nearest ancestor's that fits. So an engine may record a refusal's `rule` and +`where` as `evidence` of the finding against the pack, whose every string is +bounded at 200 characters. THE ENGINE'S OWN FINDINGS (contracts/health-finding.md § The id rule; the holder's rulings `openxFactory#656` `6069024023`, items 1 and 2). @@ -319,8 +321,10 @@ class Violation(NamedTuple): """One rule a finding breaks: the rule's id, where (a JSON pointer into the - finding, built from keys this module admitted, which it names verbatim), - and what was found. No part quotes a string, key or value it refused.""" + finding of at most `STRING_MAX` characters, built from keys this module + admitted, which it names verbatim; past the bound, the nearest ancestor's + pointer that fits), and what was found. No part quotes a string, key or + value it refused.""" rule: str where: str @@ -332,9 +336,9 @@ def line(self) -> str: class FindingRefused(ValueError): """A finding, or a part of one, breaks a rule of the contract. `rule` and - `where` are the first violation's. An engine records `rule`, and never the - refused value; `where` names admitted keys verbatim and is unbounded in - length, so an engine that records it bounds it first.""" + `where` are the first violation's, and an engine records those, never the + refused value: `where` is at most `STRING_MAX` characters and names only + admitted keys.""" def __init__(self, violation: Violation, more: int = 0) -> None: self.violation = violation @@ -365,7 +369,11 @@ def id_key(*, identity: Any, kind: str, pack_id: str, path: str) -> bytes: def finding_id(*, identity: Any, kind: str, pack_id: str, path: str) -> str: """The id the engine sets: `pack_id.kind.h16`. Refuses, with `FindingRefused`, a key whose parts break their rules, so no id is ever - computed over a part the contract does not admit.""" + computed over a part the contract does not admit. + + THE ONE HASH PATH (the holder's `6069507373`, T046 item 2). Every id this + module sets or checks is computed here, and its callers look it up at call + time, so a test that patches `health_contract.finding_id` patches them all.""" found = (list(_name(kind, "/kind", "kind-is-a-family-name")) + list(_name(pack_id, "/pack_id", "pack-id-is-a-name")) + list(_path(path)) @@ -513,33 +521,51 @@ def _path(value: Any) -> Iterator[Violation]: yield Violation("path-is-corpus-relative", "/path", problem) +#: One place in a walk: the value, its bounded pointer, whether that pointer +#: has stopped at an ancestor, and the value's nesting level. +_Place = tuple[Any, str, bool, int] + + +def _child(at: str, stopped: bool, token: str) -> tuple[str, bool]: + """A child's pointer, bounded: `at` extended by `token` while the result + is at most `STRING_MAX` characters, and otherwise `at` itself, marked as + STOPPED. A stopped pointer stays stopped for every descendant, so `where` + names the nearest ancestor that fits and never a path that is not there.""" + if not stopped: + child = f"{at}/{_escape(token)}" + if len(child) <= STRING_MAX: + return child, False + return at, True + + def _walk(root: dict[Any, Any], where: str, field: str) -> Iterator[Violation]: """Every rule `identity`'s or `evidence`'s content breaks, at any depth, by an explicit stack and never by recursion. A refused key's value is not - entered, so no pointer carries a key this module refused.""" - stack: list[tuple[Any, str, int]] = [(root, where, 1)] + entered, so no pointer carries a key this module refused, and every + pointer is bounded (`_child`).""" + stack: list[_Place] = [(root, where, False, 1)] while stack: - value, at, level = stack.pop() + value, at, stopped, level = stack.pop() if not isinstance(value, (dict, list)): yield from _scalar(value, at, field) elif level > NESTING_MAX: yield Violation(NESTING_CAP, at, f"nests deeper than the engine's {NESTING_MAX} " "levels of objects and arrays") elif isinstance(value, dict): - refused, inner = _members(value, at, level, field) + refused, inner = _members(value, at, stopped, level, field) yield from refused stack.extend(reversed(inner)) else: - stack.extend(reversed([(item, f"{at}/{index}", level + 1) + stack.extend(reversed([(item, *_child(at, stopped, str(index)), level + 1) for index, item in enumerate(value)])) -def _members(value: dict[Any, Any], at: str, level: int, field: str - ) -> tuple[list[Violation], list[tuple[Any, str, int]]]: +def _members(value: dict[Any, Any], at: str, stopped: bool, level: int, field: str + ) -> tuple[list[Violation], list[_Place]]: """An object's refused keys, and the members to enter: those whose keys every rule admits.""" refused: list[Violation] = [] - inner: list[tuple[Any, str, int]] = [] + inner: list[_Place] = [] for key, item in value.items(): if not isinstance(key, str): refused.append(Violation(NOT_JSON, at, f"has a key that is not text ({_type(key)})")) @@ -550,7 +576,7 @@ def _members(value: dict[Any, Any], at: str, level: int, field: str refused.append(Violation(f"{field}-strings-are-short-text", at, f"has a key that {problem}")) else: - inner.append((item, f"{at}/{_escape(key)}", level + 1)) + inner.append((item, *_child(at, stopped, key), level + 1)) return refused, inner @@ -763,9 +789,11 @@ def admitted(*fields: str) -> bool: f"names {named_pack} and {named_kind}, and the finding is " f"{finding['pack_id']}'s {finding['kind']}") if admitted("id", "kind", "pack_id", "path", "identity"): - want = hashlib.sha256(id_key(identity=finding["identity"], kind=finding["kind"], - pack_id=finding["pack_id"], path=finding["path"]) - ).hexdigest()[:HASH_DIGITS] + # finding_id() is the ONE hash path (the holder's `6069507373`, T046 + # item 2), looked up at call time: a test that patches + # `health_contract.finding_id` patches this check and make_finding() alike. + want = finding_id(identity=finding["identity"], kind=finding["kind"], + pack_id=finding["pack_id"], path=finding["path"]).rsplit(".", 1)[1] have = finding["id"].rsplit(".", 1)[1] if have != want: yield Violation("id-is-the-hash-of-its-key", "/id", diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 66609c01..504dd721 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -1379,9 +1379,8 @@ def test_a_refusal_quotes_nothing_of_the_finding(change: dict[str, Any]) -> None def test_where_names_an_admitted_key_and_detail_names_none() -> None: - """Lane 3's REVIEW-W1 MINOR, as the docstrings now state it: `where` is a - pointer built from keys the module ADMITTED, and names them verbatim, so - an engine bounds it before it records it; `detail` names no key.""" + """`where` is a pointer built from keys the module ADMITTED, and names them + verbatim within its bound (below); `detail` names no key.""" key = "ADMITTED-HEADING-KEY" violation = hc.violations({**_example("broken-link"), "identity": {key: 5}})[0] assert violation.rule == "identity-holds-no-number" @@ -1389,6 +1388,44 @@ def test_where_names_an_admitted_key_and_detail_names_none() -> None: assert key not in violation.detail +def _nested_keys(levels: int, key: str, leaf: Any) -> dict[str, Any]: + """`levels` objects, each holding the next under `key`, and `leaf` last.""" + value: Any = leaf + for _ in range(levels): + value = {key: value} + return value + + +@pytest.mark.parametrize("identity, where", [ + ({"k" * 171: 5}, "/identity/" + "k" * 171), + ({"k" * 190: 5}, "/identity/" + "k" * 190), + ({"k" * 191: 5}, "/identity"), + (_nested_keys(15, "/" * 200, 5), "/identity"), + ({"a": {"b" * 195: {"c": 5}}}, "/identity/a"), + ({"~" * 95: 5}, "/identity/" + "~0" * 95), + ({"~" * 96: 5}, "/identity"), + ({"a": [["x"], [True, 1]]}, "/identity/a/1/1"), +], ids=["a 171-character key, named", "the bound, 200 characters", + "one past it, the ancestor", "lane 3's fifteen nested 200-character keys", + "a stopped pointer stays stopped", "escapes count, at the bound", + "escapes count, one past it", "array indices"]) +def test_where_is_bounded_at_the_nearest_ancestor_that_fits(identity: Any, where: str) -> None: + """Lane 3's REVIEW-W1 MINOR, bounded as the holder preferred: `where` is at + most 200 characters, the place's own pointer or its nearest ancestor's that + fits, so an engine may carry it as evidence. Unbounded, fifteen nested + 200-character keys of `/` made a 6026-character pointer. A pointer that + stops stays stopped, so it never names a path that is not there.""" + found = hc.violations({**_example("broken-link"), "identity": identity}) + assert [(v.rule, v.where) for v in found] == [("identity-holds-no-number", where)] + assert len(where) <= STRING_MAX + + +def test_where_is_bounded_in_evidence_too() -> None: + evidence = {"a": {"b" * 195: {"text": "c"}}} + found = hc.violations({**_example("broken-link"), "evidence": evidence}) + assert [(v.rule, v.where) for v in found] == [("evidence-names-no-text", "/evidence/a")] + + def test_a_violation_reads_as_its_rule_its_place_and_its_detail() -> None: violation = hc.violations(NEGATIVES["locator-span-is-ordered"])[0] assert violation.rule == "locator-span-is-ordered" From 0e24062166d960009f3c1beebb5ecc4e5a862392 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 01:09:49 +0000 Subject: [PATCH 08/17] T041: U+0000 refused in every string a finding carries; the pack_id ruling cited; SonarCloud's pre-check (plan 038) The holder's openxFactory#656 6072086385 item 4 (the NUL seam, fix-now): Postgres jsonb cannot store U+0000 (SQLSTATE 22P05), so no string a finding carries holds it, keys and values alike. The schema admits it in identity, evidence and locator.target, so the engine's new rule text-holds-no-nul (ENGINE_RULES, now five) refuses it there, as a FindingRefused with a bounded where; in message, path and pack_version the schema's own control-character rules refuse it already. For a pack's output the refusal is T045's whole-output pack-output-refused (6069024023 item 1's mapping); T042's _json_text refuses it too. locator.target's check moves into _target(), which keeps _locator's cognitive complexity at 12. 6072086385 item 1: ENTRY_CATEGORIES is confirmed as written; its comment, the module docstring and engine_finding() cite it beside 6069024023. SonarCloud, checked before the push: "/identity/entry" is named once (_AT_ENTRY, S1192), and the one expected-first assertion SonarCloud's S3415 flow named at 8e4065b2 now puts the module's constant first. Tests: 409 passed over the two files; 19 of the new cases fail against f9cc4b0c's module. Mutants: 85 of 85 killed, five of them new. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 63 ++++++++++++++++++++++++---------- tests/test_health_contract.py | 55 +++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 35f37cf4..eb68edc7 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -61,10 +61,20 @@ Python's recursion limit. * Every value is a JSON value: text keys, finite numbers, and no tuple, set or bytes (`finding-is-json`). +* No string a finding carries holds U+0000 (`text-holds-no-nul`; the holder's + `openxFactory#656` `6072086385` item 4), keys and values alike: Postgres + `jsonb` cannot store it (SQLSTATE 22P05), so such a finding could never be + stored. The schema admits it in `identity`, `evidence` and `locator.target`, + so this rule refuses it there, with a bounded `where`; in `message`, `path` + and `pack_version` the schema's own control-character rules refuse it + already. For a pack's output, this refusal is T045's whole-output + `pack-output-refused` (`6069024023` item 1's mapping), so a broken or hostile + pack loses its own output, never the run. T042's `_json_text` refuses it + too, as defence in depth. EVERY RULE HAS AN IDENTIFIER. A violation names the copy's own rule id (its `x-rules` catalog: 24 shape rules and 3 reference rules) or one of the engine's -four (`ENGINE_RULES`). openDox's validator cannot build the finding schema: it +five (`ENGINE_RULES`). openDox's validator cannot build the finding schema: it refuses a copy whose catalog names reference rules it does not implement (`SchemaNotEvaluable`). The finding is no validator kind anyway (N-15), so all 27 rules are written here by hand, the three reference rules among them: @@ -89,9 +99,10 @@ * `CATEGORIES` fixes the failure categories of an install-level or pre-run finding. A category is also that finding's `kind`, its `pack_id` is fixed - per category (`ENTRY_CATEGORIES`), and its identity is `{category, entry}`, - so the engine raises ONE such finding per category and entry in a run, - with the reasons in `evidence`, and it keeps one id across runs. + per category (`ENTRY_CATEGORIES`; the value is `6072086385` item 1's), and + its identity is `{category, entry}`, so the engine raises ONE such finding + per category and entry in a run, with the reasons in `evidence`, and it + keeps one id across runs. `pathless_identity()` builds the identity and `engine_finding()` the whole finding. * `collision_identity()` builds a collision's `{collided_id}`. @@ -266,8 +277,10 @@ #: (15.7), so `entry` is never empty for them. Every other category's finding is #: against the product itself, `opendox`: no live sandbox, a refused manifest or #: dispositions file, and a refused entry, whose id may be malformed, reserved -#: or repeated and so is no pack id to attribute a finding to. One `pack_id` -#: per category (the holder's `6069024023` item 1). +#: or repeated and so is no pack id to attribute a finding to; it rides only in +#: `identity.entry`. This is the holder's `6072086385` item 1, beside +#: `6069024023` item 1: it names the value that ruling's "one pack_id per +#: category" left open, after dox-v1.2's schema text for `pack_id`. ENTRY_CATEGORIES: frozenset[str] = frozenset({ "fetch-failed", "digest-mismatch", "declaration-refused", "pack-crashed", "pack-timed-out", "pack-bound-hit", "pack-output-refused"}) @@ -301,11 +314,14 @@ SIZE_CAP = "identity-is-within-the-size-cap" NESTING_CAP = "nesting-is-within-the-cap" NOT_JSON = "finding-is-json" -ENGINE_RULES: tuple[str, ...] = (NAME_CAP, SIZE_CAP, NESTING_CAP, NOT_JSON) +NO_NUL = "text-holds-no-nul" +ENGINE_RULES: tuple[str, ...] = (NAME_CAP, SIZE_CAP, NESTING_CAP, NOT_JSON, NO_NUL) # The pointers of the fields more than one rule reports at. _AT_IDENTITY = "/identity" +_AT_ENTRY = "/identity/entry" _AT_LOCATOR = "/locator" +_AT_TARGET = "/locator/target" _AT_RESOLUTION_CLASS = "/resolution_class" # The schema's patterns, read as Python's `fullmatch`, which admits no trailing @@ -317,6 +333,8 @@ _SURROGATE = re.compile("[\ud800-\udfff]") _CONTROL = re.compile("[\x00-\x1f\x7f-\x9f\ud800-\udfff]") _NOT_ONE_LINE = re.compile("[\x00-\x1f\x7f-\x9f\u2028\u2029\ud800-\udfff]") +_NUL = "\x00" +_NUL_DETAIL = "holds U+0000, which a stored finding cannot carry (jsonb refuses it)" class Violation(NamedTuple): @@ -410,7 +428,7 @@ def pathless_identity(category: str, entry: str = "") -> dict[str, str]: if (not isinstance(entry, str) or not _ENTRY.fullmatch(entry) or len(entry) > STRING_MAX): raise FindingRefused(Violation( - rule, "/identity/entry", + rule, _AT_ENTRY, "the entry is a manifest entry's id, of lowercase letters, digits and " f"hyphens and at most {STRING_MAX} characters, or the empty string")) return {"category": category, "entry": entry} @@ -575,6 +593,8 @@ def _members(value: dict[Any, Any], at: str, stopped: bool, level: int, field: s elif (problem := _text_problem(key)) is not None: refused.append(Violation(f"{field}-strings-are-short-text", at, f"has a key that {problem}")) + elif _NUL in key: + refused.append(Violation(NO_NUL, at, f"has a key that {_NUL_DETAIL}")) else: inner.append((item, *_child(at, stopped, key), level + 1)) return refused, inner @@ -585,6 +605,8 @@ def _scalar(value: Any, at: str, field: str) -> Iterator[Violation]: if isinstance(value, str): if (problem := _text_problem(value)) is not None: yield Violation(f"{field}-strings-are-short-text", at, f"is a string that {problem}") + elif _NUL in value: + yield Violation(NO_NUL, at, f"is a string that {_NUL_DETAIL}") elif value is None or isinstance(value, bool): return elif not isinstance(value, (int, float)): @@ -629,7 +651,7 @@ def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: yield Violation(rule, "/identity/category", "is not a name of lowercase letters, " "digits and hyphens") if not isinstance(identity["entry"], str) or not _ENTRY.fullmatch(identity["entry"]): - yield Violation(rule, "/identity/entry", "is neither a manifest entry's id nor the " + yield Violation(rule, _AT_ENTRY, "is neither a manifest entry's id nor the " "empty string") @@ -686,11 +708,16 @@ def _locator(value: Any) -> Iterator[Violation]: yield Violation("locator-line-is-a-line-number", f"/locator/{key}", f"is not a whole number no less than 1 ({_type(line)})") if "target" in value: - target = value["target"] - if not isinstance(target, str) or not target or _text_problem(target): - yield Violation("locator-target-is-short-text", "/locator/target", - f"is not text of 1 to {STRING_MAX} characters free of lone " - f"surrogates ({_type(target)})") + yield from _target(value["target"]) + + +def _target(target: Any) -> Iterator[Violation]: + if not isinstance(target, str) or not target or _text_problem(target): + yield Violation("locator-target-is-short-text", _AT_TARGET, + f"is not text of 1 to {STRING_MAX} characters free of lone " + f"surrogates ({_type(target)})") + elif _NUL in target: + yield Violation(NO_NUL, _AT_TARGET, _NUL_DETAIL) def _one_of(value: Any, allowed: tuple[str, ...], where: str, rule: str) -> Iterator[Violation]: @@ -854,9 +881,9 @@ def engine_finding(category: str, entry: str = "", *, pack_version: str, severit """An install-level or pre-run finding, as the engine raises it (the holder's `6069024023` item 1). Its `kind` is its category, its `path` is empty, its identity is `{category, entry}`, and it is human-only. Its - `pack_id` is the one its category fixes: for `ENTRY_CATEGORIES`, `entry`, - the manifest entry's id, which is never empty and never `opendox`; for - every other category, `opendox`. `pack_version` is that pack's: the + `pack_id` is the one its category fixes (`6072086385` item 1): for + `ENTRY_CATEGORIES`, `entry`, the manifest entry's id, which is never empty + and never `opendox`; for every other category, `opendox`. `pack_version` is that pack's: the entry's version, or the installed version for `opendox`. The reasons ride in `evidence`, so a run raises one such finding per category and entry, and it keeps one id across runs.""" @@ -865,7 +892,7 @@ def engine_finding(category: str, entry: str = "", *, pack_version: str, severit if category in ENTRY_CATEGORIES: if entry in ("", OPENDOX): raise FindingRefused(Violation( - "pathless-identity-is-category-and-entry", "/identity/entry", + "pathless-identity-is-category-and-entry", _AT_ENTRY, f"a {category} finding is against the pack its manifest entry " f"launched, so its entry is that pack's id, never empty and never " f"{OPENDOX}")) diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 504dd721..94348ff8 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -340,6 +340,11 @@ def test_the_serialized_identity_holds_the_schemas_bound(name: str) -> None: "notes/plan.md", "identity-collision", "collision-identity-is-the-collided-id"), ({"collided_id": "not an id"}, "notes/plan.md", "identity-collision", "collision-identity-is-the-collided-id"), + # U+0000, which jsonb cannot store (the holder's 6072086385 item 4) + ({"target": "../old/brief\x00.md"}, "notes/plan.md", "broken-link", "text-holds-no-nul"), + ({"tar\x00get": "../old/brief.md"}, "notes/plan.md", "broken-link", "text-holds-no-nul"), + ({"a": ["b", "\x00"]}, "notes/plan.md", "broken-link", "text-holds-no-nul"), + ({"a": {"\x00": "b"}}, "notes/plan.md", "broken-link", "text-holds-no-nul"), ({"category": "no-sandbox", "entry": ""}, "", "identity-collision", "collision-identity-is-the-collided-id"), ] @@ -381,7 +386,7 @@ def test_the_vocabulary_is_the_packaged_copys() -> None: defs = schema["$defs"] assert hc.RESOLUTION_CLASSES == tuple(props["resolution_class"]["enum"]) == ( "auto-fix", "assisted", "human-only") - assert (hc.AUTO_FIX, hc.ASSISTED, hc.HUMAN_ONLY) == hc.RESOLUTION_CLASSES + assert hc.RESOLUTION_CLASSES == (hc.AUTO_FIX, hc.ASSISTED, hc.HUMAN_ONLY) assert hc.SEVERITIES == tuple(props["severity"]["enum"]) == ("error", "warning", "info") assert hc.BASELINE_CLASSES == tuple(props["baseline_class"]["enum"]) == ( "new", "pack-upgrade", "persistent") @@ -413,7 +418,8 @@ def test_the_rule_catalog_is_the_copys_plus_the_engines_own() -> None: assert len(hc.SHAPE_RULES) == len(set(hc.SHAPE_RULES)) == 24 assert set(hc.ENGINE_RULES) == {"name-is-at-most-40-characters", "identity-is-within-the-size-cap", - "nesting-is-within-the-cap", "finding-is-json"} + "nesting-is-within-the-cap", "finding-is-json", + "text-holds-no-nul"} assert not set(hc.ENGINE_RULES) & set(catalog) @@ -754,6 +760,9 @@ def test_the_message_is_one_bounded_line(message: Any, admitted: bool) -> None: ({"a": b"bytes"}, "/evidence/a", "finding-is-json"), ({1: "a"}, "/evidence", "finding-is-json"), ({"a": 10 ** 5000}, "/evidence/a", "finding-is-json"), + ({"a": "b\x00"}, "/evidence/a", "text-holds-no-nul"), + ({"a\x00": "b"}, "/evidence", "text-holds-no-nul"), + ({"a": [{"b": "\x00"}]}, "/evidence/a/0/b", "text-holds-no-nul"), ]) def test_evidence_holds_locators_only(evidence: Any, where: str | None, rule: str | None) -> None: @@ -766,6 +775,45 @@ def test_evidence_holds_locators_only(evidence: Any, where: str | None, assert [(v.rule, v.where) for v in found] == [(rule, where)] +@pytest.mark.parametrize("field, value, rule, where", [ + ("identity", {"target": "../old/brief.md\x00"}, "text-holds-no-nul", "/identity/target"), + ("identity", {"tar\x00get": "../old/brief.md"}, "text-holds-no-nul", "/identity"), + ("identity", {"a": [{"b": "\x00"}]}, "text-holds-no-nul", "/identity/a/0/b"), + ("identity", {"k" * 189: {"x": "\x00"}}, "text-holds-no-nul", "/identity/" + "k" * 189), + ("evidence", {"a": "\x00"}, "text-holds-no-nul", "/evidence/a"), + ("evidence", {"\x00": "a"}, "text-holds-no-nul", "/evidence"), + ("locator", {"target": "\x00"}, "text-holds-no-nul", "/locator/target"), + ("message", "a\x00b", "message-is-one-bounded-line", "/message"), + ("path", "notes/pl\x00an.md", "path-is-corpus-relative", "/path"), + ("pack_version", "0.2.0\x00", "pack-version-is-text", "/pack_version"), +], ids=["identity value", "identity key", "identity, deep", "identity, past the bound", + "evidence value", "evidence key", "locator target", "message", "path", + "pack_version"]) +def test_u0000_is_refused_in_every_string_a_finding_carries(field: str, value: Any, + rule: str, where: str) -> None: + """The holder's `6072086385` item 4: Postgres `jsonb` cannot store U+0000 + (SQLSTATE 22P05), so no string a finding carries holds it, keys and values + alike. The schema admits it in `identity`, `evidence` and `locator.target`, + where the engine's rule refuses it; in `message`, `path` and + `pack_version`, the schema's own rules do. Each refusal is a + `FindingRefused` with a bounded `where`, never a raw error, and quotes + nothing it refused.""" + finding = {**_example("broken-link"), field: value} + assert [(v.rule, v.where) for v in hc.violations(finding)] == [(rule, where)] + assert len(where) <= STRING_MAX + with pytest.raises(hc.FindingRefused) as refused: + hc.check_finding(finding) + assert refused.value.rule == rule + assert refused.value.where == where + assert "\x00" not in str(refused.value) + with pytest.raises(hc.FindingRefused): + hc.to_json(finding) + fields = {**_fields(_example("broken-link")), field: value} + with pytest.raises(hc.FindingRefused) as made: + hc.make_finding(**fields) + assert made.value.rule == rule + + def test_evidence_is_an_empty_object_when_a_family_supplies_none() -> None: """14.5, FR-011: every emitted finding carries `evidence`.""" fields = _fields(_example("orphan")) @@ -810,6 +858,8 @@ def test_an_identity_that_is_not_text_utf8_can_encode_is_refused(identity: Any, ({"target": ""}, ["locator-target-is-short-text"]), ({"target": "x" * 201}, ["locator-target-is-short-text"]), ({"target": "\ud800"}, ["locator-target-is-short-text"]), + ({"target": "../old/\x00brief.md"}, ["text-holds-no-nul"]), + ({"line_start": 1, "line_end": 1, "target": "\x00"}, ["text-holds-no-nul"]), ]) def test_the_locator_is_display_only_and_bounded(locator: Any, rules: list[str]) -> None: finding = {**_example("broken-link"), "locator": locator} @@ -968,6 +1018,7 @@ def _with(name: str, **changes: Any) -> dict[str, Any]: _identity_of(IDENTITY_MAX_BYTES + 1))), "nesting-is-within-the-cap": _with("orphan", evidence=_nested(17)), "finding-is-json": _with("orphan", evidence={"a": ("b",)}), + "text-holds-no-nul": _with("orphan", evidence={"a": "b\x00c"}), } From c620dacd4c37edb181c40f54404b9925cb08e171 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 01:20:41 +0000 Subject: [PATCH 09/17] T041: the holder's NAMES rulings (b), (d) and (e), and lane 3's REVIEW-W1 items at f9cc4b0c (plan 038) The holder's openxFactory#656 6072197564, on lane 3's NAMES points: - (b) a pathless finding's identity.category is one of a CLOSED set, PATHLESS_CATEGORIES: the engine's categories and identity-collision. - (d) ENGINE_FIELDS is split by behaviour: IGNORED_FIELDS (a pack's id is ignored) and STAMPED_FIELDS (pack_id, pack_version and baseline_class are the engine's). No behaviour moves. - (e) INSTALL_CATEGORIES (no-sandbox, manifest-refused, dispositions-refused) take entry "": a run raises ONE install-level finding (data-model.md:382), so a non-empty entry is refused by pathless_identity(), engine_finding() and check_finding() alike. This is also lane 3's MINOR at health_contract.py:865. Lane 3's REVIEW-W1 at f9cc4b0c: three rows for the list-index branch of the where bound (201 and 207 characters, and a stopped pointer that must not resume to /identity/0/a); a pathless original whose kind is not its category, whose disappearance takes the original's kind (and, under (b), admits no re-raise of its own). Tests: 433 passed over the two files; 14 of the new cases fail against 0e24062's module. Mutants: 93 of 93 killed, eight of them new. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 63 ++++++++++++++++++---- tests/test_health_contract.py | 96 ++++++++++++++++++++++++++++++++-- 2 files changed, 143 insertions(+), 16 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index eb68edc7..493535a0 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -102,9 +102,13 @@ per category (`ENTRY_CATEGORIES`; the value is `6072086385` item 1's), and its identity is `{category, entry}`, so the engine raises ONE such finding per category and entry in a run, with the reasons in `evidence`, and it - keeps one id across runs. - `pathless_identity()` builds the identity and `engine_finding()` the whole - finding. + keeps one id across runs. The install-level categories + (`INSTALL_CATEGORIES`) take entry "": a run raises ONE such finding + (`6072197564` (e)). `pathless_identity()` builds the identity and + `engine_finding()` the whole finding. +* A pathless finding's `identity.category` is one of `PATHLESS_CATEGORIES`, + the engine's categories and `identity-collision`, and nothing else + (`6072197564` (b)): only the engine raises a pathless finding. * `collision_identity()` builds a collision's `{collided_id}`. * `disappearance_identity()` builds the identity of the re-raise of an uncited disappearance, in the form its original's path calls for: a pathed @@ -113,6 +117,9 @@ or "">}` (option (D)). T046 raises it. * `ENGINE_KINDS` are the kinds only the engine raises, which T045 refuses in a pack's declaration. +* `IGNORED_FIELDS` and `STAMPED_FIELDS` split the fields a pack does not own + by what happens to a pack's value (`6072197564` (d)): its `id` is ignored, + and `pack_id`, `pack_version` and `baseline_class` are the engine's. T045 APPENDS the pack protocol to this module (the static declaration, the stdout document and the patch type) at T041's landing, a cross-lane hand-off @@ -138,7 +145,6 @@ "AUTO_FIX", "BASELINE_CLASSES", "CATEGORIES", - "ENGINE_FIELDS", "ENGINE_KINDS", "ENGINE_RULES", "ENTRY_CATEGORIES", @@ -150,15 +156,19 @@ "IDENTITY_COLLISION", "IDENTITY_MAX_BYTES", "ID_MAX", + "IGNORED_FIELDS", + "INSTALL_CATEGORIES", "NAME_MAX", "NESTING_MAX", "OPENDOX", "OPTIONAL_FIELDS", + "PATHLESS_CATEGORIES", "REFERENCE_RULES", "REQUIRED_FIELDS", "RESOLUTION_CLASSES", "SEVERITIES", "SHAPE_RULES", + "STAMPED_FIELDS", "STRING_MAX", "UNCITED_DISAPPEARANCE", "Violation", @@ -216,8 +226,15 @@ "id", "kind", "pack_id", "pack_version", "path", "identity", "locator", "severity", "resolution_class", "message", "evidence", "baseline_class") -#: The fields the ENGINE sets; a family or pack is never asked for them (15.7). -ENGINE_FIELDS: tuple[str, ...] = ("id", "pack_id", "pack_version", "baseline_class") +#: The fields a pack does not own, split by what happens to its value (the +#: holder's `6072197564` (d)). A pack's own `id` is IGNORED: the engine computes +#: every id by the id rule (`finding_id()`), so `make_finding()` takes none and +#: a pack's value never reaches a finding (`6065680005` item 3). +IGNORED_FIELDS: tuple[str, ...] = ("id",) +#: The fields that are the ENGINE's: it stamps `pack_id` and `pack_version` +#: (15.7) and sets `baseline_class`; a pack's output that sets one is refused +#: (T045, `6065680005` item 3). +STAMPED_FIELDS: tuple[str, ...] = ("pack_id", "pack_version", "baseline_class") #: No key of `identity` or `evidence`, at any depth, is named one of these #: (R2Q25 (a)). @@ -291,6 +308,20 @@ ENGINE_KINDS: frozenset[str] = frozenset(CATEGORIES) | {IDENTITY_COLLISION, UNCITED_DISAPPEARANCE} +#: The install-level categories: a run raises ONE such finding, never one per +#: entry (data-model.md:382, "ONE install-level finding"; R2Q16 (a)), so its +#: entry is always "" and a non-empty one is refused (the holder's `6069024023` +#: item 1 and `6072197564` (e)). Only `ENTRY_CATEGORIES` and `entry-refused` +#: take an entry. +INSTALL_CATEGORIES: frozenset[str] = frozenset({ + "no-sandbox", "manifest-refused", "dispositions-refused"}) + +#: The categories a pathless finding's identity may name, a CLOSED set (the +#: holder's `6072197564` (b)): the engine's own, and `identity-collision`, the +#: kind of a pathless collision whose disappearance is re-raised (option (D)). +#: A pack raises no pathless finding (T045 refuses one; `6072086385` item 3). +PATHLESS_CATEGORIES: frozenset[str] = frozenset(CATEGORIES) | {IDENTITY_COLLISION} + #: The copy's catalog: the rules its own keywords state, and the three #: cross-field rules no JSON Schema keyword can state. SHAPE_RULES: tuple[str, ...] = ( @@ -335,6 +366,8 @@ _NOT_ONE_LINE = re.compile("[\x00-\x1f\x7f-\x9f\u2028\u2029\ud800-\udfff]") _NUL = "\x00" _NUL_DETAIL = "holds U+0000, which a stored finding cannot carry (jsonb refuses it)" +_INSTALL_DETAIL = ("is not empty, though a run raises ONE install-level finding of its " + "category, never one per entry") class Violation(NamedTuple): @@ -431,6 +464,8 @@ def pathless_identity(category: str, entry: str = "") -> dict[str, str]: rule, _AT_ENTRY, "the entry is a manifest entry's id, of lowercase letters, digits and " f"hyphens and at most {STRING_MAX} characters, or the empty string")) + if entry and category in INSTALL_CATEGORIES: + raise FindingRefused(Violation(rule, _AT_ENTRY, _INSTALL_DETAIL)) return {"category": category, "entry": entry} @@ -647,12 +682,16 @@ def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: yield Violation(rule, _AT_IDENTITY, "a pathless finding's identity is exactly " "{category, entry}, the engine's own") return - if not isinstance(identity["category"], str) or not _NAME.fullmatch(identity["category"]): - yield Violation(rule, "/identity/category", "is not a name of lowercase letters, " - "digits and hyphens") - if not isinstance(identity["entry"], str) or not _ENTRY.fullmatch(identity["entry"]): + category, entry = identity["category"], identity["entry"] + known = isinstance(category, str) and category in PATHLESS_CATEGORIES + if not known: + yield Violation(rule, "/identity/category", "is not one of the engine's categories " + "or identity-collision") + if not isinstance(entry, str) or not _ENTRY.fullmatch(entry): yield Violation(rule, _AT_ENTRY, "is neither a manifest entry's id nor the " "empty string") + elif entry and known and category in INSTALL_CATEGORIES: + yield Violation(rule, _AT_ENTRY, _INSTALL_DETAIL) def _collision_form(identity: dict[Any, Any]) -> Iterator[Violation]: @@ -883,7 +922,9 @@ def engine_finding(category: str, entry: str = "", *, pack_version: str, severit empty, its identity is `{category, entry}`, and it is human-only. Its `pack_id` is the one its category fixes (`6072086385` item 1): for `ENTRY_CATEGORIES`, `entry`, the manifest entry's id, which is never empty - and never `opendox`; for every other category, `opendox`. `pack_version` is that pack's: the + and never `opendox`; for every other category, `opendox`. An + install-level category (`INSTALL_CATEGORIES`) takes entry "", and a + non-empty one is refused (`6072197564` (e)). `pack_version` is that pack's: the entry's version, or the installed version for `opendox`. The reasons ride in `evidence`, so a run raises one such finding per category and entry, and it keeps one id across runs.""" diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 94348ff8..3ec13e51 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -332,6 +332,12 @@ def test_the_serialized_identity_holds_the_schemas_bound(name: str) -> None: "pathless-identity-is-category-and-entry"), ({"category": "no-sandbox", "entry": "House"}, "", "no-sandbox", "pathless-identity-is-category-and-entry"), + # the holder's 6072197564: (b) a closed set of categories; (e) one + # install-level finding per run, so its entry is "" + ({"category": "orphan", "entry": ""}, "", "no-sandbox", + "pathless-identity-is-category-and-entry"), + ({"category": "no-sandbox", "entry": "house-style"}, "", "no-sandbox", + "pathless-identity-is-category-and-entry"), ({"target": "../old/brief.md"}, "", "broken-link", "pathless-identity-is-category-and-entry"), ({"id": "house-style.heading-case.0f1e2d3c4b5a6978"}, "notes/plan.md", @@ -395,7 +401,10 @@ def test_the_vocabulary_is_the_packaged_copys() -> None: field for field in props if field in schema["required"]) + tuple( field for field in props if field not in schema["required"]) assert hc.FIELDS == tuple(props) - assert set(hc.ENGINE_FIELDS) == {"id", "pack_id", "pack_version", "baseline_class"} + # the holder's 6072197564 (d): a pack's id is ignored; the rest are the engine's + assert hc.IGNORED_FIELDS == ("id",) + assert hc.STAMPED_FIELDS == ("pack_id", "pack_version", "baseline_class") + assert set(hc.IGNORED_FIELDS + hc.STAMPED_FIELDS) < set(hc.FIELDS) for name in ("identity_key", "evidence_key"): assert tuple(defs[name]["allOf"][0]["not"]["enum"]) == hc.FORBIDDEN_KEYS == FORBIDDEN assert defs[name]["allOf"][1]["maxLength"] == hc.STRING_MAX == STRING_MAX @@ -922,7 +931,7 @@ def test_the_path_is_corpus_relative(path: Any, admitted: bool) -> None: finding = _example("orphan") finding["path"] = path if path == "": - finding["identity"] = {"category": "x", "entry": ""} + finding["identity"] = {"category": "no-sandbox", "entry": ""} rules = [r for r in _rules(finding) if r != "id-is-the-hash-of-its-key"] assert rules == ([] if admitted else ["path-is-corpus-relative"]) @@ -1142,6 +1151,10 @@ def test_to_json_is_the_canonical_serialization() -> None: #: against the pack their manifest entry launched. AGAINST_OPENDOX = {"no-sandbox", "entry-refused", "manifest-refused", "dispositions-refused"} +#: The install-level categories: a run raises ONE such finding, so its entry +#: is "" (the holder's `6072197564` (e); data-model.md:382). +INSTALL_LEVEL = {"no-sandbox", "manifest-refused", "dispositions-refused"} + def test_the_categories_are_the_ruled_ones_and_names() -> None: assert hc.CATEGORIES == RULED_CATEGORIES @@ -1152,9 +1165,11 @@ def test_the_categories_are_the_ruled_ones_and_names() -> None: assert hc.ENTRY_CATEGORIES < set(hc.CATEGORIES) assert hc.ENGINE_KINDS == set(RULED_CATEGORIES) | {"identity-collision", "uncited-disappearance"} + assert hc.INSTALL_CATEGORIES == INSTALL_LEVEL + assert hc.PATHLESS_CATEGORIES == set(RULED_CATEGORIES) | {"identity-collision"} -@pytest.mark.parametrize("category", RULED_CATEGORIES) +@pytest.mark.parametrize("category", [c for c in RULED_CATEGORIES if c not in INSTALL_LEVEL]) def test_each_category_makes_one_engine_finding_per_entry_with_one_id(category: str) -> None: """A category is its finding's kind, the path is empty, it is human-only, one `pack_id` per category, and one id per category and entry across runs @@ -1193,6 +1208,49 @@ def test_a_finding_against_a_pack_names_that_packs_entry(category: str, entry: s assert refused.value.rule == "pathless-identity-is-category-and-entry" +@pytest.mark.parametrize("category", sorted(INSTALL_LEVEL)) +def test_an_install_level_finding_takes_no_entry(category: str) -> None: + """Lane 3's REVIEW-W1 MINOR at `f9cc4b0c` (`health_contract.py:865`) and the + holder's `6072197564` (e): a run raises ONE install-level finding, never one + per entry (data-model.md:382), so its entry is "", and a non-empty one is + refused by `engine_finding()`, `pathless_identity()` and `check_finding()` + alike. Otherwise one whole-file refusal could split per entry.""" + with pytest.raises(hc.FindingRefused) as refused: + hc.engine_finding(category, "house-style", pack_version="0.2.0", severity="warning", + message="the run could not use it") + assert refused.value.rule == "pathless-identity-is-category-and-entry" + assert refused.value.where == "/identity/entry" + with pytest.raises(hc.FindingRefused) as built: + hc.pathless_identity(category, "house-style") + assert built.value.where == "/identity/entry" + identity = {"category": category, "entry": "house-style"} + finding = {**_example("no-sandbox"), "kind": category, "identity": identity, + "id": _independent_id("opendox", category, "", identity)} + assert [(v.rule, v.where) for v in hc.violations(finding)] == [ + ("pathless-identity-is-category-and-entry", "/identity/entry")] + + +@pytest.mark.parametrize("category, admitted", [ + *[(category, True) for category in RULED_CATEGORIES], + ("identity-collision", True), + ("uncited-disappearance", False), ("broken-link", False), ("orphan", False), +]) +def test_a_pathless_identity_names_an_engine_category_or_a_collision(category: str, + admitted: bool) -> None: + """The holder's `6072197564` (b): the set is CLOSED. Only the engine raises + a pathless finding, so its category is one of the engine's, or + `identity-collision` in the re-raise of a pathless collision's + disappearance (option (D)).""" + identity = {"category": category, "entry": ""} + finding = {**_example("no-sandbox"), "kind": "uncited-disappearance", "identity": identity, + "id": _independent_id("opendox", "uncited-disappearance", "", identity)} + found = [(v.rule, v.where) for v in hc.violations(finding)] + if admitted: + assert found == [] + else: + assert found == [("pathless-identity-is-category-and-entry", "/identity/category")] + + def test_an_engine_finding_against_a_41_character_pack_is_refused() -> None: hc.engine_finding("pack-crashed", "p" * 40, pack_version="1", severity="error", message="the pack crashed") @@ -1217,7 +1275,9 @@ def test_the_two_added_categories_are_admitted(category: str) -> None: @pytest.mark.parametrize("category, entry", [ ("not-a-category", ""), ("No-Sandbox", ""), ("", ""), (None, ""), - ("no-sandbox", "House"), ("no-sandbox", "a" * 201), ("no-sandbox", None)]) + ("entry-refused", "House"), ("entry-refused", "a" * 201), ("entry-refused", None), + ("no-sandbox", "house-style"), ("manifest-refused", "house-style"), + ("dispositions-refused", "house-style")]) def test_the_pathless_identity_refuses_what_is_not_the_engines(category: Any, entry: Any) -> None: with pytest.raises(hc.FindingRefused) as refused: @@ -1284,6 +1344,27 @@ def test_the_disappearance_takes_the_form_its_originals_path_calls_for(name: str assert reraise["path"] == original["path"] +def test_a_pathless_reraise_takes_its_originals_kind_not_its_category() -> None: + """Lane 3's REVIEW-W1 NIT at `f9cc4b0c`: an original whose kind is not its + identity's category, the engine's own re-raise of a pathless + disappearance. Its disappearance's identity names the original's KIND + (option (D)). Under the closed set (`6072197564` (b)) no pathless finding + names that kind, so the contract admits no re-raise of a pathless + re-raise's disappearance.""" + original = hc.make_finding( + kind="uncited-disappearance", pack_id="opendox", pack_version="0.2.0", path="", + identity={"category": "no-sandbox", "entry": ""}, severity="warning", + resolution_class="human-only", + message="a finding disappeared with no landed repair citing it") + identity = hc.disappearance_identity(original) + assert identity == {"category": "uncited-disappearance", "entry": ""} + with pytest.raises(hc.FindingRefused) as refused: + hc.make_finding(kind="uncited-disappearance", pack_id="opendox", pack_version="0.2.0", + path="", identity=identity, severity="warning", + resolution_class="human-only", message="it disappeared again") + assert refused.value.where == "/identity/category" + + def test_the_disappeared_id_alone_is_refused_for_a_pathless_original() -> None: """Why the pathless form exists: `{disappeared_id}` on an empty path breaks the schema's `pathless-identity-is-category-and-entry`.""" @@ -1456,10 +1537,15 @@ def _nested_keys(levels: int, key: str, leaf: Any) -> dict[str, Any]: ({"~" * 95: 5}, "/identity/" + "~0" * 95), ({"~" * 96: 5}, "/identity"), ({"a": [["x"], [True, 1]]}, "/identity/a/1/1"), + ({"k" * 189: [5]}, "/identity/" + "k" * 189), + ({"k" * 189: [[[[5]]]]}, "/identity/" + "k" * 189), + ({"k" * 195: [{"a": 5}]}, "/identity"), ], ids=["a 171-character key, named", "the bound, 200 characters", "one past it, the ancestor", "lane 3's fifteen nested 200-character keys", "a stopped pointer stays stopped", "escapes count, at the bound", - "escapes count, one past it", "array indices"]) + "escapes count, one past it", "array indices", + "an index one past the bound, 201 characters", "indices to 207 characters", + "a stopped pointer stays stopped through an index"]) def test_where_is_bounded_at_the_nearest_ancestor_that_fits(identity: Any, where: str) -> None: """Lane 3's REVIEW-W1 MINOR, bounded as the holder preferred: `where` is at most 200 characters, the place's own pointer or its nearest ancestor's that From 829b7c24f159ac737e7a0d91bccb8c88f69216f6 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:37:51 +0000 Subject: [PATCH 10/17] T041: a pathless entry takes its category's form (lane 3's REVIEW-W1 MINOR at c620dacd, as the holder ruled it) (plan 038) check_finding() refused a non-empty entry only for INSTALL_CATEGORIES, so it admitted {identity-collision, } and {pack-crashed, ""}, two forms the engine never builds. The holder's ruling, refining 6072197564 (e): a category in ENTRY_CATEGORIES names its pack's entry, never ""; INSTALL_CATEGORIES and identity-collision take entry ""; entry-refused takes either. One helper, _entry_breaks_form(), holds the rule for check_finding() and pathless_identity() alike. Tests: 459 passed over the two files; 10 of the new cases fail against c620dacd's module. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 26 ++++++++++++++++++------ tests/test_health_contract.py | 37 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 493535a0..46518696 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -366,8 +366,9 @@ _NOT_ONE_LINE = re.compile("[\x00-\x1f\x7f-\x9f\u2028\u2029\ud800-\udfff]") _NUL = "\x00" _NUL_DETAIL = "holds U+0000, which a stored finding cannot carry (jsonb refuses it)" -_INSTALL_DETAIL = ("is not empty, though a run raises ONE install-level finding of its " - "category, never one per entry") +_ENTRY_FORM_DETAIL = ("does not take its category's form: a pack's id for a category " + "against a pack, and empty for an install-level category or a " + "collision") class Violation(NamedTuple): @@ -464,8 +465,8 @@ def pathless_identity(category: str, entry: str = "") -> dict[str, str]: rule, _AT_ENTRY, "the entry is a manifest entry's id, of lowercase letters, digits and " f"hyphens and at most {STRING_MAX} characters, or the empty string")) - if entry and category in INSTALL_CATEGORIES: - raise FindingRefused(Violation(rule, _AT_ENTRY, _INSTALL_DETAIL)) + if _entry_breaks_form(category, entry): + raise FindingRefused(Violation(rule, _AT_ENTRY, _ENTRY_FORM_DETAIL)) return {"category": category, "entry": entry} @@ -690,8 +691,21 @@ def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: if not isinstance(entry, str) or not _ENTRY.fullmatch(entry): yield Violation(rule, _AT_ENTRY, "is neither a manifest entry's id nor the " "empty string") - elif entry and known and category in INSTALL_CATEGORIES: - yield Violation(rule, _AT_ENTRY, _INSTALL_DETAIL) + elif known and _entry_breaks_form(category, entry): + yield Violation(rule, _AT_ENTRY, _ENTRY_FORM_DETAIL) + + +def _entry_breaks_form(category: str, entry: str) -> bool: + """Whether a pathless identity's entry breaks the form its category calls + for (the holder's ruling on lane 3's MINOR at `c620dacd`, refining + `6072197564` (e)). A category against a pack (`ENTRY_CATEGORIES`) names + that pack's entry, so its entry is never empty; an install-level category + and a collision's re-raise take entry ""; `entry-refused` takes either.""" + if category in ENTRY_CATEGORIES: + return not entry + if category in INSTALL_CATEGORIES or category == IDENTITY_COLLISION: + return entry != "" + return False def _collision_form(identity: dict[Any, Any]) -> Iterator[Violation]: diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 3ec13e51..01e9f1b3 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -1155,6 +1155,10 @@ def test_to_json_is_the_canonical_serialization() -> None: #: is "" (the holder's `6072197564` (e); data-model.md:382). INSTALL_LEVEL = {"no-sandbox", "manifest-refused", "dispositions-refused"} +#: The categories against the pack a valid manifest entry launched, whose +#: entry is that pack's id, never "" (`6072086385` item 1). +AGAINST_A_PACK = set(RULED_CATEGORIES) - AGAINST_OPENDOX + def test_the_categories_are_the_ruled_ones_and_names() -> None: assert hc.CATEGORIES == RULED_CATEGORIES @@ -1241,7 +1245,8 @@ def test_a_pathless_identity_names_an_engine_category_or_a_collision(category: s a pathless finding, so its category is one of the engine's, or `identity-collision` in the re-raise of a pathless collision's disappearance (option (D)).""" - identity = {"category": category, "entry": ""} + entry = "house-style" if category in AGAINST_A_PACK else "" + identity = {"category": category, "entry": entry} finding = {**_example("no-sandbox"), "kind": "uncited-disappearance", "identity": identity, "id": _independent_id("opendox", "uncited-disappearance", "", identity)} found = [(v.rule, v.where) for v in hc.violations(finding)] @@ -1251,6 +1256,34 @@ def test_a_pathless_identity_names_an_engine_category_or_a_collision(category: s assert found == [("pathless-identity-is-category-and-entry", "/identity/category")] +@pytest.mark.parametrize("category, entry, admitted", [ + *[(category, "", False) for category in sorted(AGAINST_A_PACK)], + *[(category, "house-style", True) for category in sorted(AGAINST_A_PACK)], + ("identity-collision", "house-style", False), + ("identity-collision", "", True), + *[(category, "house-style", False) for category in sorted(INSTALL_LEVEL)], + *[(category, "", True) for category in sorted(INSTALL_LEVEL)], + ("entry-refused", "", True), + ("entry-refused", "house-style", True), +]) +def test_a_pathless_entry_takes_its_categorys_form(category: str, entry: str, + admitted: bool) -> None: + """Lane 3's REVIEW-W1 MINOR at `c620dacd` (`health_contract.py:693`), as the + holder ruled it: a category against a pack names that pack's entry, never + "", and an install-level category and a collision's re-raise take ""; + `entry-refused` takes either. Before, `check_finding()` admitted + `{identity-collision, }` and `{pack-crashed, ""}`, two forms + the engine never builds.""" + identity = {"category": category, "entry": entry} + finding = {**_example("no-sandbox"), "kind": "uncited-disappearance", "identity": identity, + "id": _independent_id("opendox", "uncited-disappearance", "", identity)} + found = [(v.rule, v.where) for v in hc.violations(finding)] + if admitted: + assert found == [] + else: + assert found == [("pathless-identity-is-category-and-entry", "/identity/entry")] + + def test_an_engine_finding_against_a_41_character_pack_is_refused() -> None: hc.engine_finding("pack-crashed", "p" * 40, pack_version="1", severity="error", message="the pack crashed") @@ -1277,7 +1310,7 @@ def test_the_two_added_categories_are_admitted(category: str) -> None: ("not-a-category", ""), ("No-Sandbox", ""), ("", ""), (None, ""), ("entry-refused", "House"), ("entry-refused", "a" * 201), ("entry-refused", None), ("no-sandbox", "house-style"), ("manifest-refused", "house-style"), - ("dispositions-refused", "house-style")]) + ("dispositions-refused", "house-style"), ("pack-crashed", ""), ("fetch-failed", "")]) def test_the_pathless_identity_refuses_what_is_not_the_engines(category: Any, entry: Any) -> None: with pytest.raises(hc.FindingRefused) as refused: From bf0f08240db45205d709b2ca5c1a7067d2214e56 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:43:08 +0000 Subject: [PATCH 11/17] T041: the test overview counts five engine rules, and names the pack_id ruling (Copilot r4226093840 on openDox-code#97) (plan 038) Docstrings only: the overview of tests/test_health_contract.py said four engine rules after text-holds-no-nul made five, and two docstrings still said "one pack_id per category", the phrase 6072086385 item 1 replaced. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_health_contract.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 01e9f1b3..573c57d9 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -43,12 +43,13 @@ 6. THE BOUNDS ON `message` AND `evidence` (R2Q25 (a); ADV-27). 7. EVERY RULE THE COPY CATALOGUES IS ENFORCED. Each of its 27 rules has one negative case, openDox-spec's own negative example rebuilt, refused for - that rule and no other; so are the module's four engine rules. + that rule and no other; so are the module's five engine rules. 8. THE ENGINE'S OWN FINDINGS (the holder's `6069024023`): the eleven ruled categories, each one finding per category and entry with one id across - runs and one `pack_id` per category (`engine_finding()`); the disappearance - re-raise in both forms, option (D); and openDox-spec's five examples - rebuilt through `make_finding()` and `engine_finding()`, id included. + runs and the `pack_id` its category takes (`engine_finding()`; `6072086385` + item 1); the disappearance re-raise in both forms, option (D); and + openDox-spec's five examples rebuilt through `make_finding()` and + `engine_finding()`, id included. 9. THE CLOSURE: the module imports the standard library and nothing else. Its imports are read from its source, it is imported in a fresh interpreter that blocks openXdox and openxFactory, and it runs from a directory that @@ -1176,8 +1177,8 @@ def test_the_categories_are_the_ruled_ones_and_names() -> None: @pytest.mark.parametrize("category", [c for c in RULED_CATEGORIES if c not in INSTALL_LEVEL]) def test_each_category_makes_one_engine_finding_per_entry_with_one_id(category: str) -> None: """A category is its finding's kind, the path is empty, it is human-only, - one `pack_id` per category, and one id per category and entry across runs - (the holder's `6069024023` item 1).""" + its `pack_id` is the one its category takes (`6072086385` item 1), and one + id per category and entry across runs (the holder's `6069024023` item 1).""" entry = "house-style" def build(entry: str = entry, message: str = "the pack did not run") -> dict[str, Any]: From 4cc065ad0da80e4476216e654a58dd379fd66567 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:53:18 +0000 Subject: [PATCH 12/17] T041: _entry_breaks_form() cites the holder's ruling 6073087924 (plan 038) Docstring only: the entry-form rule is the holder's openxFactory#656 6073087924, on lane 3's MINOR at c620dacd, so check_finding() admits exactly what engine_finding() builds. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 46518696..b694be2a 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -697,10 +697,12 @@ def _pathless_form(identity: dict[Any, Any]) -> Iterator[Violation]: def _entry_breaks_form(category: str, entry: str) -> bool: """Whether a pathless identity's entry breaks the form its category calls - for (the holder's ruling on lane 3's MINOR at `c620dacd`, refining - `6072197564` (e)). A category against a pack (`ENTRY_CATEGORIES`) names - that pack's entry, so its entry is never empty; an install-level category - and a collision's re-raise take entry ""; `entry-refused` takes either.""" + for (the holder's `6073087924`, on lane 3's MINOR at `c620dacd`, refining + `6072197564` (e)), so `check_finding()` admits exactly what + `engine_finding()` builds. A category against a pack (`ENTRY_CATEGORIES`) + names that pack's entry, so its entry is never empty; an install-level + category and a collision's re-raise take entry ""; `entry-refused` takes + either.""" if category in ENTRY_CATEGORIES: return not entry if category in INSTALL_CATEGORIES or category == IDENTITY_COLLISION: From 85d618f1b02a96266a65e46a7805e55f645eea88 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:17:21 +0000 Subject: [PATCH 13/17] T041: the per-entry entry is a pack id; refused-patch is an engine kind; refused_entry(); n6 and e3 (plan 038) The holder's openxFactory#656 6082100803: for ENTRY_CATEGORIES, check_finding() refuses an entry that is empty, the reserved opendox, or longer than NAME_MAX, so it admits exactly what engine_finding() builds. Lane 3's exactness probe (every category against eleven entries) found 21 mismatches at 4cc065ad and finds 0 here; test_check_finding_admits_ exactly_what_engine_finding_builds holds it. 6086098003 item 1: REFUSED_PATCH joins ENGINE_KINDS, a PATHED engine kind, in neither CATEGORIES nor PATHLESS_CATEGORIES. Item 3 (b): a U+0000 row for path. Item 4 and 6088484643: refused_entry() builds entry-refused's identity.entry: "" for no id, the id as written when it is [a-z0-9-]+ of at most 200 characters, and otherwise sha256- and the SHA-256 of its canonical text (json.dumps with sorted keys, no whitespace, ASCII escapes, allow_nan, default=str; repr() when that raises). It never raises. Lane 3's surviving mutants at 4cc065ad were equivalent ones on dead guards, so the guards go: n6, the bool guard before _writable() in _locator (a bool is always writable, and _line() refuses it next); e3, engine_finding()'s own entry check, which pathless_identity() already made dead. Tests: 502 passed over the two files; 35 of the new cases fail against 4cc065ad's module. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 80 ++++++++++++++++---- tests/test_health_contract.py | 133 ++++++++++++++++++++++++++++++++- 2 files changed, 194 insertions(+), 19 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index b694be2a..85fd9248 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -116,7 +116,13 @@ the schema requires, `{category: , entry: }` (option (D)). T046 raises it. * `ENGINE_KINDS` are the kinds only the engine raises, which T045 refuses in a - pack's declaration. + pack's declaration: the categories, `identity-collision`, and the PATHED + `uncited-disappearance` and `refused-patch` (`6086098003` item 1). +* `refused_entry()` builds `entry-refused`'s `identity.entry` from a refused + entry's id (`6086098003` item 4, `6088484643`): "" for no id, the id as + written when it is a pack-id-shaped name of at most 200 characters, and + otherwise `sha256-` and the digest of its canonical text. That digest is no + finding id: every id is still `finding_id()`'s. * `IGNORED_FIELDS` and `STAMPED_FIELDS` split the fields a pack does not own by what happens to a pack's value (`6072197564` (d)): its `id` is ignored, and `pack_id`, `pack_version` and `baseline_class` are the engine's. @@ -164,6 +170,7 @@ "OPTIONAL_FIELDS", "PATHLESS_CATEGORIES", "REFERENCE_RULES", + "REFUSED_PATCH", "REQUIRED_FIELDS", "RESOLUTION_CLASSES", "SEVERITIES", @@ -182,6 +189,7 @@ "identity_json", "make_finding", "pathless_identity", + "refused_entry", "to_json", "violations", ] @@ -217,6 +225,12 @@ #: (data-model.md § Baseline classes; T046 raises it). UNCITED_DISAPPEARANCE = "uncited-disappearance" +#: The kind of the engine's finding against a pack whose patch it refused, +#: naming `refused_patch` and `reason` (T049; data-model.md:209, :223, :369). +#: A PATHED engine kind, like `uncited-disappearance`, and no category (the +#: holder's `6086098003` item 1). +REFUSED_PATCH = "refused-patch" + #: A finding's fields, in the schema's order. REQUIRED_FIELDS: tuple[str, ...] = ( "id", "kind", "pack_id", "pack_version", "path", "identity", "severity", @@ -306,7 +320,8 @@ #: re-raise of a disappearance. A pack declares none of them (T045 refuses #: one; the holder's `6069024023` item 1). ENGINE_KINDS: frozenset[str] = frozenset(CATEGORIES) | {IDENTITY_COLLISION, - UNCITED_DISAPPEARANCE} + UNCITED_DISAPPEARANCE, + REFUSED_PATCH} #: The install-level categories: a run raises ONE such finding, never one per #: entry (data-model.md:382, "ONE install-level finding"; R2Q16 (a)), so its @@ -479,6 +494,44 @@ def collision_identity(collided_id: str) -> dict[str, str]: return {"collided_id": collided_id} +def refused_entry(entry_id: Any) -> str: + """`entry-refused`'s `identity.entry` for a refused manifest entry's id + (the holder's `6086098003` item 4 and `6088484643`). Deterministic, total + over every value the manifest's YAML parser yields, and it never raises. + + * No id (`None`, as the parser gives an absent or null `id`): "". + * A string matching `[a-z0-9-]+` of at most `STRING_MAX` characters: the id + as written. + * Anything else (a malformed or over-long string, one carrying U+0000, a + non-string): `sha256-` and the lowercase SHA-256 hex of the ASCII bytes + of its CANONICAL TEXT, 71 characters. The canonical text is + `json.dumps(entry_id, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=True, default=str)`; when that raises (a + mapping whose keys JSON cannot spell or sort, a self-referencing value), + it is `repr(entry_id)`, with any non-ASCII character backslash-escaped. + A value nested past Python's recursion limit, which neither spelling + reaches, takes the fixed text ``. + + The digest is no finding id (every id is `finding_id()`'s); it keeps a + refused id out of the stored identity while keeping two distinct ids + apart.""" + if entry_id is None: + return "" + if (isinstance(entry_id, str) and len(entry_id) <= STRING_MAX + and _NAME.fullmatch(entry_id)): + return entry_id + try: + text = json.dumps(entry_id, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=True, default=str) + except (TypeError, ValueError, RecursionError): + try: + text = repr(entry_id) + except RecursionError: + text = "" + digest = hashlib.sha256(text.encode("ascii", "backslashreplace")).hexdigest() + return f"sha256-{digest}" + + def disappearance_identity(original: dict[str, Any]) -> dict[str, str]: """The identity of the engine's re-raise of an uncited disappearance, in the form the ORIGINAL's path calls for (option (D), the holder's @@ -700,11 +753,13 @@ def _entry_breaks_form(category: str, entry: str) -> bool: for (the holder's `6073087924`, on lane 3's MINOR at `c620dacd`, refining `6072197564` (e)), so `check_finding()` admits exactly what `engine_finding()` builds. A category against a pack (`ENTRY_CATEGORIES`) - names that pack's entry, so its entry is never empty; an install-level - category and a collision's re-raise take entry ""; `entry-refused` takes - either.""" + names that pack's entry, which is that pack's id: never empty, never the + reserved `opendox` (contracts/health-packs-manifest.md:24) and a name of 1 + to `NAME_MAX` characters (the holder's `6082100803`; the alphabet is + checked before this). An install-level category and a collision's re-raise + take entry ""; `entry-refused` takes either (`refused_entry()`).""" if category in ENTRY_CATEGORIES: - return not entry + return entry in ("", OPENDOX) or len(entry) > NAME_MAX if category in INSTALL_CATEGORIES or category == IDENTITY_COLLISION: return entry != "" return False @@ -756,7 +811,8 @@ def _locator(value: Any) -> Iterator[Violation]: if key not in value: continue line = value[key] - if isinstance(line, int) and not isinstance(line, bool) and not _writable(line): + # a bool is an int Python can always write; `_line()` refuses it next + if isinstance(line, int) and not _writable(line): yield Violation(NOT_JSON, f"/locator/{key}", "is an integer too long to write " "as text") elif not _line(line): @@ -945,15 +1001,7 @@ def engine_finding(category: str, entry: str = "", *, pack_version: str, severit in `evidence`, so a run raises one such finding per category and entry, and it keeps one id across runs.""" identity = pathless_identity(category, entry) - pack_id = OPENDOX - if category in ENTRY_CATEGORIES: - if entry in ("", OPENDOX): - raise FindingRefused(Violation( - "pathless-identity-is-category-and-entry", _AT_ENTRY, - f"a {category} finding is against the pack its manifest entry " - f"launched, so its entry is that pack's id, never empty and never " - f"{OPENDOX}")) - pack_id = entry + pack_id = entry if category in ENTRY_CATEGORIES else OPENDOX return make_finding(kind=category, pack_id=pack_id, pack_version=pack_version, path="", identity=identity, severity=severity, resolution_class=HUMAN_ONLY, message=message, evidence=evidence) diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 573c57d9..5f5baa35 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -65,6 +65,7 @@ import ast import copy +import datetime import hashlib import json import math @@ -926,7 +927,7 @@ def test_evidence_of_any_size_is_judged_never_raised() -> None: ("/etc/passwd", False), ("C:notes.md", False), ("c:/notes.md", False), ("notes\\plan.md", False), ("../plan.md", False), ("notes/../plan.md", False), ("notes/..", False), ("..", False), ("a\nb", False), ("a\x7fb", False), - ("a\udc80b", False), (None, False), (3, False), + ("a\udc80b", False), ("a\x00b", False), (None, False), (3, False), ]) def test_the_path_is_corpus_relative(path: Any, admitted: bool) -> None: finding = _example("orphan") @@ -1169,7 +1170,8 @@ def test_the_categories_are_the_ruled_ones_and_names() -> None: assert set(hc.CATEGORIES) - hc.ENTRY_CATEGORIES == AGAINST_OPENDOX assert hc.ENTRY_CATEGORIES < set(hc.CATEGORIES) assert hc.ENGINE_KINDS == set(RULED_CATEGORIES) | {"identity-collision", - "uncited-disappearance"} + "uncited-disappearance", + "refused-patch"} assert hc.INSTALL_CATEGORIES == INSTALL_LEVEL assert hc.PATHLESS_CATEGORIES == set(RULED_CATEGORIES) | {"identity-collision"} @@ -1260,6 +1262,12 @@ def test_a_pathless_identity_names_an_engine_category_or_a_collision(category: s @pytest.mark.parametrize("category, entry, admitted", [ *[(category, "", False) for category in sorted(AGAINST_A_PACK)], *[(category, "house-style", True) for category in sorted(AGAINST_A_PACK)], + # the holder's 6082100803: the reserved `opendox`, and the 40-character name + *[(category, "opendox", False) for category in sorted(AGAINST_A_PACK)], + *[(category, "p" * 41, False) for category in sorted(AGAINST_A_PACK)], + *[(category, "p" * 40, True) for category in sorted(AGAINST_A_PACK)], + ("entry-refused", "opendox", True), + ("entry-refused", "p" * 200, True), ("identity-collision", "house-style", False), ("identity-collision", "", True), *[(category, "house-style", False) for category in sorted(INSTALL_LEVEL)], @@ -1285,13 +1293,132 @@ def test_a_pathless_entry_takes_its_categorys_form(category: str, entry: str, assert found == [("pathless-identity-is-category-and-entry", "/identity/entry")] +#: Entries the exactness probe tries for every category. +_PROBE_ENTRIES = ["", "opendox", "house-style", "p" * 40, "p" * 41, "p" * 200, "p" * 201, + "House", "house style", "-", "a\x00b"] + + +def test_check_finding_admits_exactly_what_engine_finding_builds() -> None: + """Lane 3's exactness probe (REVIEW-W1 MINOR at `4cc065ad`; the holder's + `6082100803`): for every category and every probed entry, `engine_finding()` + builds the finding exactly when `check_finding()` admits its identity in a + re-raise, whose `pack_id` is `opendox`. It found 21 mismatches at + `4cc065ad`; it reads 0 now.""" + mismatches = [] + for category in hc.CATEGORIES: + for entry in _PROBE_ENTRIES: + try: + hc.engine_finding(category, entry, pack_version="1.0.0", severity="error", + message="the probe") + built = True + except hc.FindingRefused: + built = False + identity = {"category": category, "entry": entry} + reraise = {**_example("no-sandbox"), "kind": "uncited-disappearance", + "identity": identity, + "id": _independent_id("opendox", "uncited-disappearance", "", identity)} + admitted = not hc.violations(reraise) + if built != admitted: + mismatches.append((category, entry[:12], built, admitted)) + assert mismatches == [] + + +def test_refused_patch_is_a_pathed_engine_kind() -> None: + """The holder's `6086098003` item 1: only the engine raises `refused-patch`, + so T045 refuses a pack declaring it. It is PATHED, like + `uncited-disappearance`: no category, and no pathless identity names it.""" + assert hc.REFUSED_PATCH == "refused-patch" + assert hc.REFUSED_PATCH in hc.ENGINE_KINDS + assert hc.REFUSED_PATCH not in hc.CATEGORIES + assert hc.REFUSED_PATCH not in hc.PATHLESS_CATEGORIES + finding = hc.make_finding( + kind=hc.REFUSED_PATCH, pack_id="house-style", pack_version="1.4.0", + path="notes/plan.md", identity={"refused_patch": "house-style.heading-case.1"}, + severity="error", resolution_class="human-only", + message="the engine refused the pack's patch", + evidence={"reason": "it reaches outside its path"}) + assert hc.violations(finding) == [] + identity = {"category": "refused-patch", "entry": ""} + pathless = {**_example("no-sandbox"), "kind": "uncited-disappearance", + "identity": identity, + "id": _independent_id("opendox", "uncited-disappearance", "", identity)} + assert [(v.rule, v.where) for v in hc.violations(pathless)] == [ + ("pathless-identity-is-category-and-entry", "/identity/category")] + + +def _canonical_digest(entry_id: Any) -> str: + """The holder's `6088484643`, spelled independently of the module.""" + try: + text = json.dumps(entry_id, sort_keys=True, separators=(",", ":"), ensure_ascii=True, + allow_nan=True, default=str) + except (TypeError, ValueError): + text = repr(entry_id) + return "sha256-" + hashlib.sha256(text.encode("ascii", "backslashreplace")).hexdigest() + + +_SELF: list[Any] = [] +_SELF.append(_SELF) + +#: Each refused id, and the entry `refused_entry()` gives it. +_REFUSED_IDS = { + "an absent id": (None, ""), + "a conforming id": ("house-style", "house-style"), + "a conforming id of 200 characters": ("p" * 200, "p" * 200), + "an uppercase id": ("House-Style", _canonical_digest("House-Style")), + "a spaced id": ("house style", _canonical_digest("house style")), + "an id with U+0000": ("house\x00style", _canonical_digest("house\x00style")), + "an empty string": ("", _canonical_digest("")), + "a non-string id": (5, _canonical_digest(5)), + "a boolean id": (True, _canonical_digest(True)), + "a float id, NaN": (float("nan"), "sha256-" + hashlib.sha256(b"NaN").hexdigest()), + "a date id": (datetime.date(2026, 10, 9), _canonical_digest("2026-10-09")), + "a list id": (["a", 1], _canonical_digest(["a", 1])), + "a 201-character id": ("p" * 201, _canonical_digest("p" * 201)), + "a mapping with mixed-type keys": ( + {1: "a", "b": 2}, "sha256-" + hashlib.sha256(repr({1: "a", "b": 2}).encode()).hexdigest()), + "mixed-type keys with non-ASCII text": ( + {1: "caf\u00e9", "b": 2}, + "sha256-" + hashlib.sha256(b"{1: 'caf\\xe9', 'b': 2}").hexdigest()), + "a self-referencing list": (_SELF, "sha256-" + hashlib.sha256(b"[[...]]").hexdigest()), +} + + +@pytest.mark.parametrize("name", list(_REFUSED_IDS)) +def test_refused_entry_builds_entry_refuseds_entry(name: str) -> None: + """The holder's `6086098003` item 4 and `6088484643`: "" for no id, the id + as written when it is `[a-z0-9-]+` of at most 200 characters, and otherwise + `sha256-` and the SHA-256 of its canonical text, its JSON with sorted keys, + no whitespace and ASCII escapes (`repr()` when that raises). The entry is + one `pathless_identity()` admits, never truncated, and never a refusal.""" + entry_id, entry = _REFUSED_IDS[name] + assert hc.refused_entry(entry_id) == entry + assert hc.refused_entry(entry_id) == entry # deterministic + assert hc.pathless_identity("entry-refused", entry) == {"category": "entry-refused", + "entry": entry} + + +def test_refused_entry_keeps_distinct_ids_apart_and_never_raises() -> None: + entries = [hc.refused_entry(entry_id) for entry_id, _ in _REFUSED_IDS.values()] + assert len(set(entries)) == len(entries) + deep: Any = "x" + for _ in range(sys.getrecursionlimit() * 3): + deep = [deep] + deep_entry = hc.refused_entry(deep) + assert re.fullmatch(r"sha256-[0-9a-f]{64}", deep_entry) + assert hc.refused_entry({"b": 1, "a": [2, 1]}) == hc.refused_entry({"a": [2, 1], "b": 1}) + + def test_an_engine_finding_against_a_41_character_pack_is_refused() -> None: + """The holder's `6082100803`: a per-entry category's entry is a pack id of + 1 to 40 characters, judged at the identity, so `check_finding()` and + `engine_finding()` refuse the same entries.""" hc.engine_finding("pack-crashed", "p" * 40, pack_version="1", severity="error", message="the pack crashed") with pytest.raises(hc.FindingRefused) as refused: hc.engine_finding("pack-crashed", "p" * 41, pack_version="1", severity="error", message="the pack crashed") - assert refused.value.rule == "name-is-at-most-40-characters" + assert refused.value.rule == "pathless-identity-is-category-and-entry" + assert refused.value.where == "/identity/entry" def test_engine_finding_rebuilds_openDox_specs_no_sandbox_example() -> None: From 4ffc411be517caa72aec8987ddad75fcab3efcc4 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:22:04 +0000 Subject: [PATCH 14/17] T041: refused_entry()'s recursion fallback is tested past the C limit (plan 038) Python 3.12's json.dumps and repr() both reach a 3000-level value, so the test of the fallback text now nests 50,000 levels, past the C recursion limit both share; the mutant that stops catching RecursionError there is killed. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_health_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 5f5baa35..d9110713 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -1401,7 +1401,7 @@ def test_refused_entry_keeps_distinct_ids_apart_and_never_raises() -> None: entries = [hc.refused_entry(entry_id) for entry_id, _ in _REFUSED_IDS.values()] assert len(set(entries)) == len(entries) deep: Any = "x" - for _ in range(sys.getrecursionlimit() * 3): + for _ in range(50_000): # past the C recursion limit json.dumps and repr() share deep = [deep] deep_entry = hc.refused_entry(deep) assert re.fullmatch(r"sha256-[0-9a-f]{64}", deep_entry) From 471b7231c6890b2e73985f853eaa3f6e013602fa Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:13:56 +0000 Subject: [PATCH 15/17] T041: refused_entry()'s fallback spelling is ascii() (#656 6088732352 (b)) (plan 038) The holder's ruling on Copilot thread 4234200849 (openxFactory#1283): the canonical text's fallback is ascii(id), repr() with every non-ASCII character escaped, so the text is always ASCII and is encoded strictly. The digests do not move: the previous fallback, repr() encoded with backslashreplace, spelled every value the same way, so the new row for a non-ASCII key in a mixed-type mapping passes against 4ffc411b's module too. The mutant that puts repr() back is killed. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 8 +++++--- tests/test_health_contract.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 85fd9248..075a4842 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -508,7 +508,9 @@ def refused_entry(entry_id: Any) -> str: `json.dumps(entry_id, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=True, default=str)`; when that raises (a mapping whose keys JSON cannot spell or sort, a self-referencing value), - it is `repr(entry_id)`, with any non-ASCII character backslash-escaped. + it is `ascii(entry_id)`, Python's `repr()` with every non-ASCII + character escaped (the holder's `6088732352` (b)), so the text is always + ASCII and the digest never raises. A value nested past Python's recursion limit, which neither spelling reaches, takes the fixed text ``. @@ -525,10 +527,10 @@ def refused_entry(entry_id: Any) -> str: ensure_ascii=True, allow_nan=True, default=str) except (TypeError, ValueError, RecursionError): try: - text = repr(entry_id) + text = ascii(entry_id) except RecursionError: text = "" - digest = hashlib.sha256(text.encode("ascii", "backslashreplace")).hexdigest() + digest = hashlib.sha256(text.encode("ascii")).hexdigest() return f"sha256-{digest}" diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index d9110713..9680d981 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -1352,8 +1352,8 @@ def _canonical_digest(entry_id: Any) -> str: text = json.dumps(entry_id, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=True, default=str) except (TypeError, ValueError): - text = repr(entry_id) - return "sha256-" + hashlib.sha256(text.encode("ascii", "backslashreplace")).hexdigest() + text = ascii(entry_id) + return "sha256-" + hashlib.sha256(text.encode("ascii")).hexdigest() _SELF: list[Any] = [] @@ -1380,6 +1380,11 @@ def _canonical_digest(entry_id: Any) -> str: {1: "caf\u00e9", "b": 2}, "sha256-" + hashlib.sha256(b"{1: 'caf\\xe9', 'b': 2}").hexdigest()), "a self-referencing list": (_SELF, "sha256-" + hashlib.sha256(b"[[...]]").hexdigest()), + # the holder's 6088732352 (b): the fallback is ascii(), so a non-ASCII key + # in a mixed-type mapping digests its escaped spelling + "a non-ASCII key among mixed-type keys": ( + {"cl\u00e9": 1, 2: "b"}, + "sha256-" + hashlib.sha256(b"{'cl\\xe9': 1, 2: 'b'}").hexdigest()), } @@ -1388,7 +1393,8 @@ def test_refused_entry_builds_entry_refuseds_entry(name: str) -> None: """The holder's `6086098003` item 4 and `6088484643`: "" for no id, the id as written when it is `[a-z0-9-]+` of at most 200 characters, and otherwise `sha256-` and the SHA-256 of its canonical text, its JSON with sorted keys, - no whitespace and ASCII escapes (`repr()` when that raises). The entry is + no whitespace and ASCII escapes (`ascii()` when that raises, `6088732352` + (b)). The entry is one `pathless_identity()` admits, never truncated, and never a refusal.""" entry_id, entry = _REFUSED_IDS[name] assert hc.refused_entry(entry_id) == entry From 1c6d0c49543320aebefcf3e6b6c11e3142823efd Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:52:38 +0000 Subject: [PATCH 16/17] WIP T041: item 8 (#656 6090537166) in refused_entry(), tests to follow (plan 038) Module only, not for #97 yet: the ascii() fallback also catches ValueError (an integer past the 4300-digit limit) and takes ONE fixed text, ; the default hook spells a set or frozenset as its members sorted by ascii() text. The item 8 test rows and mutants are next. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/opendox/health_contract.py | 43 ++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/opendox/health_contract.py b/src/opendox/health_contract.py index 075a4842..7b7b2db3 100644 --- a/src/opendox/health_contract.py +++ b/src/opendox/health_contract.py @@ -494,6 +494,15 @@ def collision_identity(collided_id: str) -> dict[str, str]: return {"collided_id": collided_id} +def _spelling(value: Any) -> Any: + """`refused_entry()`'s `default` hook for a value JSON cannot represent: a + set's members in the order of their `ascii()` text, and anything else by + its `str()` (the holder's `6090537166` item 8 (b)).""" + if isinstance(value, (set, frozenset)): + return sorted(value, key=ascii) + return str(value) + + def refused_entry(entry_id: Any) -> str: """`entry-refused`'s `identity.entry` for a refused manifest entry's id (the holder's `6086098003` item 4 and `6088484643`). Deterministic, total @@ -506,13 +515,27 @@ def refused_entry(entry_id: Any) -> str: non-string): `sha256-` and the lowercase SHA-256 hex of the ASCII bytes of its CANONICAL TEXT, 71 characters. The canonical text is `json.dumps(entry_id, sort_keys=True, separators=(",", ":"), - ensure_ascii=True, allow_nan=True, default=str)`; when that raises (a - mapping whose keys JSON cannot spell or sort, a self-referencing value), - it is `ascii(entry_id)`, Python's `repr()` with every non-ASCII - character escaped (the holder's `6088732352` (b)), so the text is always - ASCII and the digest never raises. - A value nested past Python's recursion limit, which neither spelling - reaches, takes the fixed text ``. + ensure_ascii=True, allow_nan=True, default=...)`, whose `default` hook + spells a `set` or `frozenset` as the list of its members sorted by each + member's `ascii()` text, so a YAML `!!set` is spelled one way under every + `PYTHONHASHSEED`, and spells every other value JSON cannot represent (a + YAML date, timestamp or binary) by its `str()` (the holder's `6090537166` + item 8 (b)). When that raises (a mapping whose keys JSON cannot spell or + sort, a self-referencing value), the text is `ascii(entry_id)`, + Python's `repr()` with every non-ASCII character escaped (`6088732352` + (b)), so it is always ASCII. + A value neither spelling can write, an integer past Python's 4300-digit + conversion limit (which YAML yields from a long `0x` or `0b` literal) or + a value nested past the recursion limit, takes ONE fixed text, `` (item 8 (a)). So the digest never + raises. + + ACCEPTED LIMITS (`6088732352` (a), `6089449884`, `6090537166`): the text is + not injective across YAML value types (a date and its `str()`, a + non-string mapping key and its JSON spelling), two ids past the limits + share the fixed text, and a set reached only through the `ascii()` + fallback keeps Python's order. `identity-collision` reports any + collision. The digest is no finding id (every id is `finding_id()`'s); it keeps a refused id out of the stored identity while keeping two distinct ids @@ -524,12 +547,12 @@ def refused_entry(entry_id: Any) -> str: return entry_id try: text = json.dumps(entry_id, sort_keys=True, separators=(",", ":"), - ensure_ascii=True, allow_nan=True, default=str) + ensure_ascii=True, allow_nan=True, default=_spelling) except (TypeError, ValueError, RecursionError): try: text = ascii(entry_id) - except RecursionError: - text = "" + except (ValueError, RecursionError): + text = "" digest = hashlib.sha256(text.encode("ascii")).hexdigest() return f"sha256-{digest}" From f924db0dbd92fd6ae0977736e6a31d7f301a3986 Mon Sep 17 00:00:00 2001 From: Brett Heap <1513478+brettheap@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:23:01 +0000 Subject: [PATCH 17/17] T041: item 8's test rows, red first (#656 6090537166) (plan 038) Lane 3's MAJOR and MINOR at 471b7231, as the holder ruled them: - (a) the four ids lane 3 reproduced, integers YAML yields past Python's 4300-digit limit (a long 0x literal, a long 0b literal, one in a list, one in a mixed-key mapping), each a sha256- entry of the one fixed text and never a ValueError; the 50,000-level value takes the same text. - (b) a YAML !!set id in _REFUSED_IDS, and one set id digested in four interpreters under four PYTHONHASHSEED values: one entry, while its str() is not one spelling there, so the row bites. All 7 new cases fail against 471b7231's module. 509 passed over the two files. Lane: openxfactory-4 Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_health_contract.py | 70 +++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/tests/test_health_contract.py b/tests/test_health_contract.py index 9680d981..fef92d94 100644 --- a/tests/test_health_contract.py +++ b/tests/test_health_contract.py @@ -79,6 +79,7 @@ from typing import Any, Iterator import pytest +import yaml from opendox import contracts from opendox import health_contract as hc @@ -1346,16 +1347,26 @@ def test_refused_patch_is_a_pathed_engine_kind() -> None: ("pathless-identity-is-category-and-entry", "/identity/category")] +def _default(value: Any) -> Any: + """`6090537166` item 8 (b): a set by its members in `ascii()` order.""" + return sorted(value, key=ascii) if isinstance(value, (set, frozenset)) else str(value) + + def _canonical_digest(entry_id: Any) -> str: """The holder's `6088484643`, spelled independently of the module.""" try: text = json.dumps(entry_id, sort_keys=True, separators=(",", ":"), ensure_ascii=True, - allow_nan=True, default=str) + allow_nan=True, default=_default) except (TypeError, ValueError): text = ascii(entry_id) return "sha256-" + hashlib.sha256(text.encode("ascii")).hexdigest() +#: The one text an id takes when neither spelling can write it (item 8 (a)). +_PAST_THE_LIMITS = "sha256-" + hashlib.sha256( + b"").hexdigest() + + _SELF: list[Any] = [] _SELF.append(_SELF) @@ -1380,6 +1391,9 @@ def _canonical_digest(entry_id: Any) -> str: {1: "caf\u00e9", "b": 2}, "sha256-" + hashlib.sha256(b"{1: 'caf\\xe9', 'b': 2}").hexdigest()), "a self-referencing list": (_SELF, "sha256-" + hashlib.sha256(b"[[...]]").hexdigest()), + # 6090537166 item 8 (b): a YAML !!set, its members in ascii() order + "a set id": (yaml.safe_load("id: !!set {beta: null, alpha: null}")["id"], + "sha256-" + hashlib.sha256(b'["alpha","beta"]').hexdigest()), # the holder's 6088732352 (b): the fallback is ascii(), so a non-ASCII key # in a mixed-type mapping digests its escaped spelling "a non-ASCII key among mixed-type keys": ( @@ -1409,11 +1423,61 @@ def test_refused_entry_keeps_distinct_ids_apart_and_never_raises() -> None: deep: Any = "x" for _ in range(50_000): # past the C recursion limit json.dumps and repr() share deep = [deep] - deep_entry = hc.refused_entry(deep) - assert re.fullmatch(r"sha256-[0-9a-f]{64}", deep_entry) + assert hc.refused_entry(deep) == _PAST_THE_LIMITS assert hc.refused_entry({"b": 1, "a": [2, 1]}) == hc.refused_entry({"a": [2, 1], "b": 1}) +#: The four ids lane 3 reproduced at `471b7231`: integers YAML yields past +#: Python's 4300-digit conversion limit, which neither `json.dumps` nor +#: `ascii()` can write. +_PAST_THE_DIGIT_LIMIT = { + "a long 0x literal": "id: 0x" + "f" * 4000, + "a long 0b literal": "id: 0b" + "1" * 15000, + "a long 0x literal in a list": "id: [0x" + "f" * 4000 + "]", + "a long 0x literal in a mixed-key mapping": "id: {1: 0x" + "f" * 4000 + ", b: 2}", +} + + +@pytest.mark.parametrize("name", list(_PAST_THE_DIGIT_LIMIT)) +def test_refused_entry_never_raises_past_the_int_digit_limit(name: str) -> None: + """Lane 3's MAJOR at `471b7231`, as the holder ruled it (`6090537166` item + 8 (a)): an id neither spelling can write takes ONE fixed text, so it is a + `sha256-` entry and never a `ValueError`. Two such ids share it, an + accepted limit that `identity-collision` reports.""" + entry_id = yaml.safe_load(_PAST_THE_DIGIT_LIMIT[name])["id"] + assert hc.refused_entry(entry_id) == _PAST_THE_LIMITS + + +_SET_UNDER_A_SEED = """ +import json +from opendox import health_contract as hc +members = {members!r} +print(json.dumps([hc.refused_entry(set(members)), str(set(members))])) +""" + + +def test_a_set_id_is_one_entry_under_every_hash_seed() -> None: + """Lane 3's MINOR at `471b7231`, as the holder ruled it (`6090537166` item + 8 (b)): a set's `str()` follows hash order, which `PYTHONHASHSEED` moves, + so the default hook spells a set by its members in `ascii()` order. The + same set id, digested in four interpreters with four seeds, is one entry; + its `str()` there is not one spelling, so the row bites.""" + members = ["delta", "alpha", "kilo", "echo", "bravo", "juliet", "golf", "india", + "charlie", "hotel", "foxtrot", "lima"] + entries, spellings = set(), set() + for seed in ("0", "1", "2", "3"): + env = {**os.environ, "PYTHONHASHSEED": seed, + "PYTHONPATH": os.pathsep.join([str(SRC), os.environ.get("PYTHONPATH", "")])} + done = subprocess.run( + [sys.executable, "-c", _SET_UNDER_A_SEED.format(members=members)], + env=env, capture_output=True, text=True, timeout=60, check=True) + entry, spelling = json.loads(done.stdout) + entries.add(entry) + spellings.add(spelling) + assert len(spellings) > 1 + assert entries == {_canonical_digest(set(members))} + + def test_an_engine_finding_against_a_41_character_pack_is_refused() -> None: """The holder's `6082100803`: a per-entry category's entry is a pack id of 1 to 40 characters, judged at the identity, so `check_finding()` and