# Phase 3 plan — Configurable bundle contract Status: approved roadmap phase (see `CLAUDE.md`); details settled here before code. Depends on: Phases 1–2 (their behavior becomes the *default profile* and must not change). Proving consumer: `claude-code-llm-wiki` with a `strict-v1` profile. ## Goal Everything the first two phases hard-code about what a valid bundle looks like — concept types, layers/path prefixes, frontmatter sets and ordering, reserved-file policy — becomes configuration on a profile object instead of constants. The observable behavior under the default profile is byte-identical to Phase 1/2; the golden suite is the regression harness for that claim. ## Design 1. **`BundleProfile`** — a frozen dataclass (stdlib, no config-file DSL in v1; profiles are constructed in code by the consumer): - `type_policy`: closed enum vs. open set; the reserved `verdict` exclusion stays unconditional (it is a spec invariant, not profile config). - `frontmatter_schema`: ordered required keys + optional keys, per concept type where needed (Phase 1's seven-key layer and Phase 2's inbox layer are expressions of this schema). - `path_policy`: layer/path prefixes, filename namespaces, reserved filename patterns (`index.md`, `promoted-verdict-*`, …). - `reserved_files`: per-file required / optional / required-absent policy (e.g. a profile may demand that `log.md` is absent). - `index_policy`: managed-link format and per-level index expectations. 2. **Built-in profiles:** - `DEFAULT` (implicit): exactly the ingest-spec v1 + Phase 2 contract. - `STRICT_V1`: the `claude-code-llm-wiki` contract — closed type enum (`Concept`/`Guide`/`Reference`/`Release`), extended required frontmatter (`type`, `title`, `description`, `timestamp`, `layer`, `source_tier`, `source_url`, `source_sha`), layer path prefixes, `log.md` required-absent, root-index metadata. 3. **Plumbing change:** materialization, inbox, import, and index generation take an optional `profile` argument defaulting to `DEFAULT`. No behavior branches outside what the profile object expresses. ## The split table (settled before code) The wiki's existing validator mixes three kinds of gates. First implementation step is a written mapping of every gate to exactly one column — the mapping is reviewed with the operator before any porting: | Column | Meaning | Goes where | |---|---|---| | Profile | Structural bundle contract (types, keys, paths, reserved files) | This phase, `BundleProfile` | | Guard | Content safety (sanitize, quarantine, fencing, secrets, budgets) | Guard calls — never reimplemented here | | Consumer | Domain logic (source pipelines, promotion, staging) | Stays in the consumer repo | ## TDD order 1. `BundleProfile` type + `DEFAULT` profile; refactor Phase 1/2 constants onto it with the full existing test suite as the safety net. 2. Golden suite re-run — byte-for-byte identical under `DEFAULT` (the phase's load-bearing regression test). 3. `STRICT_V1` profile + fixtures: a wiki-shaped bundle that validates under `STRICT_V1` and is rejected under `DEFAULT` (extra frontmatter keys are fine under preserve-unknown; the *rejection* comes from the missing ingest-spec layer), and an ingest-spec bundle rejected under `STRICT_V1` (missing required keys, `log.md` policy). 4. Reserved-file policy tests (required-absent enforcement). 5. Split-table review artifact committed under `docs/` and cross-checked against the implemented profile fields. ## Key assumptions (each with its test) | # | Assumption | Test | |---|---|---| | C1 | Default-profile refactor is behavior-neutral | Phase 1 golden suite byte-identical; full Phase 1/2 test suite green, unmodified | | C2 | `strict-v1` requirements are fully enumerable from the wiki's validator | The split table is exhaustive: every wiki gate appears in exactly one column; reviewed with the operator | | C3 | Verdict reservation must survive every profile | Named test: no profile construction can permit `type: verdict` | | C4 | Profiles-in-code (no config files) suffice for the proving consumer | Confirmed with the operator at phase start; config-file loading is an extension point | ## Non-goals - Porting the wiki's security stack — those gates map to the Guard column. - Config-file/DSL profile loading, profile inheritance chains — extension points. - Adopting the second-brain (Node-world) conventions — that contract enters in Phase 4; this phase only ensures the profile object is expressive enough not to block it (checked against the catalog's spec during the split-table step). - Any change to commons' ingest-spec; if profile work reveals spec friction, it is raised in commons, not patched locally. ## Verification 1. `pytest` green; `mypy --strict src/`; `ruff check .` and `ruff format --check .` clean. 2. Golden suite byte-for-byte under `DEFAULT` — zero fixture changes in the diff (`git diff --stat examples/` empty for this phase). 3. `STRICT_V1` fixture bundle validates; cross-profile rejection tests pass in both directions (step 3 above). 4. C3 named test: constructing a profile that admits `verdict` raises at construction time. 5. The split table exists under `docs/`, every wiki gate row has exactly one column, and the Guard column contains no rows implemented in this repo (grep-gate from Phase 2 still clean). 6. Public API check: Phase 1/2 call sites compile unchanged (profile argument is optional) — verified by leaving existing tests untouched.