"""The bundle contract as configuration (Phase 3, step 1). What Phases 1 and 2 hard-coded about a valid bundle — the index name and its managed-link shape, the three filename namespaces, the reserved layer, and the frontmatter key order — becomes a profile object here. `DEFAULT` must express exactly the existing contract: the Phase 1/2 suite and the golden fixtures are the byte-level proof (assumption C1), and these tests pin the profile's own behavior. """ from __future__ import annotations import dataclasses import pytest from llm_ingestion_okf.profiles import ( DEFAULT, BundleProfile, FrontmatterSchema, TypePolicy, ) def test_the_profile_and_every_policy_on_it_are_frozen() -> None: """A profile is a value, not a mutable registry. A consumer holds DEFAULT while a run is in flight; a profile that could be mutated mid-run would make the contract a moving target and the golden determinism claim unprovable. """ for value in (DEFAULT, DEFAULT.types, DEFAULT.frontmatter, DEFAULT.paths, DEFAULT.index): with pytest.raises(dataclasses.FrozenInstanceError): value.name = "mutated" # type: ignore[misc] def test_default_profile_pins_the_phase_1_2_names() -> None: """The names the two doors already write, now stated in one place.""" assert DEFAULT.index.name == "index.md" assert DEFAULT.paths.concept_suffix == ".md" assert DEFAULT.paths.ingest_prefix == "ingest-" assert DEFAULT.paths.inbox_prefix == "inbox-" assert DEFAULT.paths.import_prefix == "import-" # --- the reserved layer (assumption C3) ----------------------------------- @pytest.mark.parametrize("reserved", ["verdict", "Verdict", "VERDICT"]) def test_no_type_policy_can_admit_the_reserved_verdict_layer(reserved: str) -> None: """C3: the verdict reservation is a spec invariant, not profile config. Refused at CONSTRUCTION, not at use: a profile that admits `verdict` must not exist at all, or the promotion gate stops being the only path into that layer the moment someone passes the profile to a door. """ with pytest.raises(ValueError, match="verdict"): TypePolicy(allowed=frozenset({"Concept", reserved})) def test_a_closed_type_policy_without_the_reserved_layer_constructs() -> None: policy = TypePolicy(allowed=frozenset({"Concept", "Guide", "Reference", "Release"})) assert policy.rejection("Concept") is None def test_default_type_policy_is_open_apart_from_the_reserved_layer() -> None: """Phase 1/2 accept any okf_type but `verdict`; DEFAULT must not narrow that.""" assert DEFAULT.types.allowed is None for admitted in ("Concept", "note", "whatever-the-operator-wants"): assert DEFAULT.types.rejection(admitted) is None for reserved in ("verdict", "Verdict", "VERDICT"): assert DEFAULT.types.rejection(reserved) is not None def test_the_reserved_rejection_reproduces_both_doors_message_and_code() -> None: """The doors keep their own typed errors; the policy supplies the wording. Door A raises ManifestError and Door B MaterializationError for the same refusal, so the policy cannot raise — it hands back the reason and the stable code, and each door frames it. These are the exact strings the Phase 1/2 tests already assert on. """ rejection = DEFAULT.types.rejection("verdict") assert rejection is not None assert rejection.code == "okf_type_reserved" assert f"okf_type {rejection.reason}" == "okf_type must not be 'verdict' (reserved layer)" def test_a_closed_type_policy_rejects_an_off_enum_type_distinctly() -> None: """An off-enum refusal is not the reserved-layer refusal wearing its code.""" policy = TypePolicy(allowed=frozenset({"Concept", "Release"})) rejection = policy.rejection("Guide") assert rejection is not None assert rejection.code == "okf_type_not_allowed" assert "Concept, Release" in rejection.reason # --- index policy --------------------------------------------------------- def test_index_link_template_and_pattern_describe_the_same_shape() -> None: """Two fields that must agree, proven to agree rather than trusted to. Rendering and parsing managed index links are separate operations (§6 appends, and maintenance rewrites), so the profile carries both a template and a pattern. A round-trip is what keeps them from drifting apart. """ for label, target in [("Title", "ingest-x.md"), ("", "inbox-y.md")]: line = DEFAULT.index.render_link(label, target) match = DEFAULT.index.link_pattern.match(line) assert match is not None, f"pattern does not match its own template output: {line!r}" assert match.group("label") == label assert match.group("target") == target def test_default_index_link_is_the_phase_1_shape() -> None: assert DEFAULT.index.render_link("Sales", "ingest-sales.md") == "- [Sales](ingest-sales.md)" def test_index_link_pattern_is_anchored_to_the_whole_line() -> None: """§6 removal keys on the exact shape, never a bare substring — a curated line that merely mentions a link must survive verbatim.""" assert DEFAULT.index.link_pattern.match("see - [Sales](ingest-sales.md) below") is None # --- frontmatter emission ------------------------------------------------- def test_emission_is_the_ordered_prefix_then_the_sorted_tail() -> None: """Commons decision D1, carried over intact. Keys named in the profile's order come first in that order; anything else follows sorted. That is what makes a regeneration over the same data byte-identical, which is what the proving consumer's hash registry needs. """ schema = FrontmatterSchema(order=("type", "title")) emitted = schema.emit({"zeta": "3", "title": "T", "alpha": "1", "type": "Concept"}) assert emitted == "type: Concept\ntitle: T\nalpha: 1\nzeta: 3" def test_emission_skips_ordered_keys_that_are_absent() -> None: schema = FrontmatterSchema(order=("type", "title", "generated")) assert schema.emit({"generated": "true", "type": "Concept"}) == "type: Concept\ngenerated: true" def test_default_emission_reproduces_door_a_key_order() -> None: """Byte-identity with `_render_concept_file`'s §5 frontmatter.""" emitted = DEFAULT.frontmatter.emit( { "type": "Concept", "title": "Sales", "source_system": "crm", "source_query": "sales.csv", "ingested_at": "2026-07-03T12:00:00Z", "ingest_manifest": "m@0123456789abcdef", "generated": "true", } ) assert emitted.splitlines() == [ "type: Concept", "title: Sales", "source_system: crm", "source_query: sales.csv", "ingested_at: 2026-07-03T12:00:00Z", "ingest_manifest: m@0123456789abcdef", "generated: true", ] def test_default_emission_reproduces_door_b_key_order() -> None: """Byte-identity with `render_inbox_concept`'s provenance layer. Door B's six keys are a different subset of the same schema, and the canonical order must reproduce BOTH doors — a single order that reordered either one would change bytes already frozen in the fixtures. """ emitted = DEFAULT.frontmatter.emit( { "type": "Concept", "title": "Notes", "source_file": "notes.md", "source_sha256": "abc", "ingested_at": "2026-07-03T12:00:00Z", "generated": "true", } ) assert emitted.splitlines() == [ "type: Concept", "title: Notes", "source_file: notes.md", "source_sha256: abc", "ingested_at: 2026-07-03T12:00:00Z", "generated: true", ] def test_source_query_is_the_only_collapsed_key() -> None: """§5 collapses whitespace runs for `source_query` alone (a multi-line SELECT must render on one line); every other value is validated single-line at load and emitted verbatim, so an operator's internal double space survives.""" assert DEFAULT.frontmatter.collapsed_keys == frozenset({"source_query"}) emitted = DEFAULT.frontmatter.emit( {"title": "two spaces", "source_query": "SELECT a\n FROM t"} ) assert emitted == "title: two spaces\nsource_query: SELECT a FROM t" def test_a_profile_is_assembled_from_its_policies() -> None: """The profile is its policies and nothing else — no behavior branches outside what the object expresses. `ownership` joined the original four at D2, and it is the same rule rather than an exception to it: the ingest stamp differs per profile, so the alternative was a version branch inside the collision gate. A policy on the object is what keeps the emitter and the predicate changeable only together. """ assert {field.name for field in dataclasses.fields(BundleProfile)} == { "types", "frontmatter", "paths", "index", "ownership", }