Phase 3 starts with a written mapping of the proving consumer's gates to exactly one of Profile / Guard / Consumer, reviewed before any porting. Drafted here against claude-code-llm-wiki's validate.py and bundle.py at their 9ee5a8e, which is their single gate entrypoint, so the enumeration is exhaustive against that commit and no other. Reading the validator corrected two things this plan asserted before it had been read, both of which would have produced a wrong STRICT_V1: - Required frontmatter keys are FOUR (type, title, description, timestamp), not the eight the plan listed. Those eight were a truncation of FRONTMATTER_KEY_ORDER, which is an eleven-key allowlist doubling as canonical emission order. frontmatter_schema therefore needs three fields -- required, allowlisted, ordered -- not one required/optional pair. - Reserved files are THREE basenames, not two: README.md is reserved like index.md and log.md but in a third way, present and exempt from the document gates while still content-scanned. A two-valued required/required-absent policy cannot express it. Two of the 25 gates bundle checks from different columns (LLM_OUTPUT_LINT, BUNDLE_URL_ALLOWLIST). They are decomposed per check rather than assigned wholesale to a dominant column, which would have moved the residue to the wrong side of the boundary. Also recorded: index policy is the widest gap between the two profiles (per-bundle vs per-directory, two link formats, append-if-absent vs exact match, root index with or without frontmatter), and C4 is confirmed with the operator -- profiles in code, no config-file loading in v1. Four open questions are addressed to the proving consumer in the artifact; no porting until they and the table come back reviewed.
119 lines
7 KiB
Markdown
119 lines
7 KiB
Markdown
# 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`: three distinct fields, not one — the *required* key
|
||
set, the *allowlisted* key set, and the canonical emission *order*. The
|
||
proving consumer keeps four required keys inside an eleven-key allowlist
|
||
(see `docs/phase-3-split-table.md`), so a single ordered required/optional
|
||
pair cannot express it. Also carries nullable keys and the scalar encoding
|
||
subset. 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 policy, THREE values not two — required,
|
||
required-absent, and present-but-exempt-from-document-gates (the proving
|
||
consumer's `README.md`, which carries no frontmatter and is never indexed
|
||
yet is still content-scanned).
|
||
- `index_policy`: managed-link format and per-level index expectations. This
|
||
is the widest gap between the two profiles: one index per bundle versus
|
||
one per directory, `- [label](target)` versus
|
||
`* [Title](link) - description`, append-if-absent versus exact match in
|
||
both directions, and a root index with or without frontmatter.
|
||
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`), four required frontmatter keys
|
||
(`type`, `title`, `description`, `timestamp`) inside an eleven-key
|
||
allowlist that doubles as emission order, layer/path/type/source_tier
|
||
agreement rules, `log.md` required-absent, `README.md` reserved-exempt,
|
||
per-directory indexes, and root-index metadata (`okf_version`,
|
||
`bundle_profile`, `okf_spec_commit`).
|
||
|
||
An earlier draft of this line listed eight required keys. That was the
|
||
allowlist read as the required set; corrected against the validator on
|
||
2026-07-25.
|
||
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. **Drafted 2026-07-25 in
|
||
`docs/phase-3-split-table.md`**, over all 25 gates, and sent to the proving
|
||
consumer for review; two gates turned out to bundle checks from different
|
||
columns and are decomposed there rather than forced into one.
|
||
|
||
| 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. Drafted 2026-07-25 against `validate.py`/`bundle.py` at their `9ee5a8e`, which is the single gate entrypoint — the enumeration holds only against that commit, so re-check before porting. Two gates required decomposition rather than a single column |
|
||
| 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 2026-07-25 at phase start: profiles are constructed in code, config-file loading stays 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.
|