"""The bundle contract as configuration (Phase 3). What a valid bundle looks like — which concept types exist, which frontmatter keys are emitted and in which order, which filename namespaces the doors own, and what an index line looks like — is a profile, not a set of constants scattered across the doors. `DEFAULT` is exactly the ingest-spec v1 + Phase 2 contract, so nothing observable changes for a caller that never mentions a profile; the golden fixtures are the byte-level proof. Two things deliberately do NOT live here. Security is the guard's, always: no disposition, origin or channel vocabulary belongs on a profile. And the reserved `verdict` layer is a spec invariant rather than profile config — it is refused at construction, so a profile admitting it cannot be built, let alone passed to a door. The `timestamp`/`generated` pair is refused the same way, and for the same reason: both are things a profile must not be able to express. Profiles are constructed in code. Config-file loading and inheritance chains are extension points, not v1 (settled with the operator at phase start). """ from __future__ import annotations import re from collections.abc import Collection, Iterable, Mapping, Sequence from dataclasses import dataclass, field, replace # The one layer no profile may admit (ingest-spec §3): the promotion gate is # the only path into it. Compared case-insensitively, as both doors already do. RESERVED_OKF_TYPE = "verdict" # The one key PAIR no profile may name (OKF §13.1): `timestamp` is readable as # a legacy stand-in for `generated.at` only while `generated` is ABSENT, so a # schema able to name both can describe a document that has neither. Stated as # key names rather than as a judgement on values because every `generated` a # schema can express today is a scalar (`_is_legal_value`) and therefore # malformed as a v0.2 mapping — naming both IS the hazard here. When a value # model can express a well-formed `generated`, this narrows with it. # "Naming" spans EVERY field that puts a key in the schema's namespace — `order`, # `required`, `allowed` and `nullable` alike. A field left out of that union is a # hole in the gate, not a narrower gate: the namespace is open by default, so a # key named only by `nullable` is admitted just as surely as an emitted one. _TIMESTAMP_FALLBACK_PAIR = frozenset({"timestamp", "generated"}) # --- YAML scalar forms (K3-22) ----------------------------------------------- # # SPEC SS 11 point 1 requires "a parseable YAML frontmatter block" and SS 4 # names no YAML version and no subset, so the reader a consumer has decides; # PyYAML is the common one. The forms are decided by RULES here and never by a # parser -- this package's one runtime dependency is the guard -- and the rules # are validated against PyYAML in `tests/test_yaml_frontmatter.py`, both error # directions counted. # What a YAML reader takes as syntax at the START of a plain scalar. _YAML_INDICATORS = frozenset("-?:,[]{}#&*!|>'\"%@`") # What a plain scalar INSIDE a flow mapping may not carry, for one of the two # readers a `sources` entry has to survive. `,[]{}` end it for both. `?` ends # it for PyYAML, whose scanner stops a flow plain scalar there, so a URL with a # query string fails `safe_load`. A quote anywhere is refused by the pinned # guard, which admits no quoted leaf in a flow mapping (1.3.0, measured). So # quoting cannot rescue a flow value: plain fails one reader, quoted the other. _FLOW_UNSAFE = frozenset(",[]{}?'\"") # A key inside a flow mapping, as the guard's `_KEY_RE` and our readers take it. _FLOW_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$") def yaml_block_plain(value: str) -> bool: """Whether `value` reads back verbatim as a plain scalar in a block mapping. MEASURED ON R761 (K3-19): 217 of 2 024 first spec points carry `": "`, and PyYAML's `safe_load` refused exactly those 217 concepts' frontmatter. K3-22 measured the same rule over every top-level value in eleven trees (the K2 default bundle, the R761 HTML and XML builds, a five-document project, this repository's examples and fixtures, and five consumer bundles): 0 refused that PyYAML returns verbatim, 0 kept that it does not. `" #"` is here although PyYAML does not refuse it: `title: Kap #3` loads, as `Kap`. """ return ( bool(value) and value[0] not in _YAML_INDICATORS and ": " not in value and " #" not in value and not value.endswith(":") and not any(char in value for char in "\t\n\r") ) def yaml_flow_plain(value: str) -> bool: """Whether `value` reads back verbatim as a plain scalar inside a flow mapping, for PyYAML AND the pinned guard. The block rule plus `_FLOW_UNSAFE`, with one exception the block rule does not make: a leading `-` followed by a non-space is an ordinary character in a flow mapping for both readers (`del/-utkast.pdf`), and refusing it would refuse an address both of them read. The block rule keeps refusing it because it also decides which spec points are written at all (K3-19), and in a block mapping a refusal only costs a pair of quotes. """ if not value or any(char in value for char in _FLOW_UNSAFE): return False rest = value[1:] if value[0] == "-" and value[1:2] not in ("", " ", "\t") else value return yaml_block_plain(rest) def yaml_flow_collection(value: str) -> bool: """Whether `value` is SHAPED as a flow collection (`[...]` or `{...}`). Structure a producer built -- `source_offset: [0, 4]`, `generated: { by: x, at: y }` -- is written as it stands; measured over the same eleven trees, only structural keys carry this shape and no `title` does. """ return (value[:1], value[-1:]) in (("[", "]"), ("{", "}")) def yaml_flow_collection_plain(value: str) -> bool: """Whether a flow collection parses, for PyYAML and the guard, into what was written: one `{ key: leaf, ... }` mapping, or a `[...]` sequence of such mappings or of scalars, every leaf `yaml_flow_plain`.""" if value[:1] == "{" and value[-1:] == "}": return _flow_mapping_plain(value) if not (value[:1] == "[" and value[-1:] == "]"): return False items: list[str] = [] depth = 0 current: list[str] = [] for char in value[1:-1]: depth += {"{": 1, "}": -1}.get(char, 0) if depth not in (0, 1): return False if char == "," and depth == 0: items.append("".join(current).strip()) current = [] else: current.append(char) items.append("".join(current).strip()) if depth != 0: return False return all( _flow_mapping_plain(item) if item[:1] == "{" else yaml_flow_plain(item) for item in items ) def _flow_mapping_plain(item: str) -> bool: if not (item[:1] == "{" and item[-1:] == "}"): return False inner = item[1:-1].strip() if not inner: return False for entry in inner.split(","): key, separator, leaf = entry.strip().partition(": ") if not separator or not _FLOW_KEY.match(key) or not yaml_flow_plain(leaf.strip()): return False return True def quote_scalar(value: str) -> str: """`value` as a double-quoted YAML scalar: `\\` and `"` escaped, nothing else. Double and never single: over every bundle measured, 0 values carry a surrounding `"` pair and 11 193 a surrounding `'` pair, so a reader that unquotes `"` changes the meaning of no value already written. """ return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' def unquote_scalar(value: str) -> str: """A surrounding `"` pair removed and `\\"` / `\\\\` decoded; else `value`. The two escapes `quote_scalar` writes are the two decoded. Any other backslash sequence (`\\n`, `\\t`, `\\x..`, `\\u....`) is kept as written: a YAML reader would decode it and this reader does not claim to. A single-quoted value is returned as it stands -- `'1'` stays `'1'`. """ if len(value) < 2 or value[0] != '"' or value[-1] != '"': return value inner = value[1:-1] out: list[str] = [] index = 0 while index < len(inner): char = inner[index] if char == "\\" and inner[index + 1 : index + 2] in ('"', "\\"): out.append(inner[index + 1]) index += 2 continue out.append(char) index += 1 return "".join(out) def block_scalar(value: str) -> str: """`value` as written after `key: ` in a block mapping: plain where a YAML reader returns it verbatim, double-quoted otherwise.""" return value if yaml_block_plain(value) else quote_scalar(value) @dataclass(frozen=True) class TypeRejection: """Why a profile refuses an `okf_type`, for the door to frame and raise. The policy does not raise: Door A refuses with `ManifestError` and Door B with `MaterializationError`, so the refusal has to be reported rather than thrown. `reason` completes the sentence "