"""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) #: The frontmatter keys whose BLOCK form the line-oriented grammar decodes #: rather than skips (K3-24). One key wide on purpose: `sources` is the key #: `consume.read_sources` already knows how to read, so decoding it here adds #: no second grammar to disagree with the first. Widening this set changes #: what every flat reader reports for keys no measurement covers -- a fixture #: in this tree carries a block `verified:` that still reads as empty, and a #: test pins that state so the next widening is a decision rather than a #: side effect. STRUCTURED_BLOCK_KEYS = frozenset({"sources"}) #: A leaf carrying one of these has no plain form inside a flow mapping THIS #: library's own readers parse back: a comma or a brace would re-split the #: mapping, a leading `"` would open a quoted scalar. A `?` is absent on #: purpose -- it is what `yaml_flow_plain` refuses for PyYAML, and refusing it #: here would refuse exactly the address this decoding exists to carry. _FLOW_RENDER_UNSAFE = frozenset(",[]{}") def read_block_mappings( lines: Sequence[str], position: int ) -> tuple[Mapping[str, str], ...] | None: """The block sequence of mappings opened at `lines[position]`, or `None`. `None` is "this reader cannot decode it", never "there is nothing here": an indented line before any `- ` opens no entry and is refused rather than folded into one, which would invent an entry the document does not have. One grammar, four call sites: the three copies of the line-oriented frontmatter reader and `consume.read_sources`, which is where this loop was written and measured. Two copies of a block grammar would be two answers to one question. """ entries: list[dict[str, str]] = [] for nested in lines[position + 1 :]: if not nested.strip(): continue if nested[:1] not in (" ", "\t"): break item = nested.strip() if item.startswith("- "): entries.append({}) item = item[2:].strip() elif not entries: return None key, separator, raw = item.partition(":") if not separator: return None entries[-1][key.strip()] = unquote_scalar(raw.strip()) if not entries: return None return tuple(entries) def render_flow_mappings(entries: Sequence[Mapping[str, str]]) -> str: """`entries` as the flow sequence the flat readers already round-trip. A READING projection, not an emission: the flat grammar's value type is `str`, and the flow form is the one string shape this library's own readers decode back into the same entries. It is deliberately NOT a claim that the rendering is writable -- `yaml_flow_plain` still refuses a `?` and the guard still refuses a quote inside a flow mapping, so the emission rule is untouched and a value rendered here may have no writable flow form at all. That is the whole reason the producer writes block. """ items = [] for entry in entries: pairs = ", ".join(f"{key}: {_flow_leaf(value)}" for key, value in entry.items()) items.append("{ " + pairs + " }" if pairs else "{}") return "[" + ", ".join(items) + "]" def _flow_leaf(value: str) -> str: if not value or value[0] == '"' or any(char in value for char in _FLOW_RENDER_UNSAFE): return quote_scalar(value) return value def block_mapping_value(lines: Sequence[str], position: int) -> str | None: """The flow rendering of the block sequence at `position`, or `None`.""" entries = read_block_mappings(lines, position) return None if entries is None else render_flow_mappings(entries) @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 "