docs(phase-3): the four open questions are answered; required set is eight

The proving consumer answered Q1-Q4 on 2026-07-25, each measured or read out
of their code that day. No row in the mapping moved. Three of the four answers
carry a sharper reason than this table drafted, and one reverses a correction
this table made a day earlier.

Q2 is the substantive change. This table had corrected the plan's eight-key
required set down to four, against the consumer's REQUIRED_KEYS constant.
Their operator ratified eight: the constant is their emit-path minimum, not
their contract, and all eight keys are present on 522/522 of their documents.
STRICT_V1 is therefore deliberately stricter than the consumer's own
validator; if they ever ship a class lacking source_sha they bump the profile
rather than bend the bundle, and they say so before shipping it. The eleven-key
allowlist is unchanged and still a different thing from the required set:
version/date are layer-specific and summary is the one nullable key, so all
three stay optional-when-absent.

Q1, Q3 and Q4 keep their Consumer/Guard assignments, now with the reasons that
justify them: gate 5 escalates on run state (disposition == verbatim) rather
than document class, so a profile structurally cannot see it; gate 4's
changelog rule is a vendor slug list that is not scoped to concepts/ at all;
gates 19 and 22 need external state, one of them by construction never
blocking. Assumption C2 is confirmed at 25 gates and the index shape is
confirmed non-negotiable.

Both reviews - the consumer's and the operator's - are now in, so the port may
begin. The Guard column is unaffected by all of it.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 15:21:59 +02:00
commit cf1f7984c4
2 changed files with 108 additions and 57 deletions

View file

@ -1,17 +1,19 @@
# Phase 3 split table — the proving consumer's 25 gates
Status: **drafted and independently corroborated; operator review outstanding.**
Phase 3's plan requires this mapping before any porting. Nothing here is
implemented yet, and nothing in the consumer's repository is modified by this
repository.
Status: **reviewed by the proving consumer and by the operator (2026-07-25);
the port may begin.** Phase 3's plan requires this mapping before any porting.
Nothing in the consumer's repository is modified by this repository.
The proving consumer (`claude-code-llm-wiki`) answered the same questions from
their own code on 2026-07-25, crossing with this draft rather than responding
to it. Two independent readings of the same source agreed on every substantive
point: four required keys, three reserved basenames, `summary` as the only
nullable key, the index structure, and the closed sets. Their answer added four
refinements, each folded in below and marked. Their column assignments match
this table's, with one wording difference resolved in gate 5's row.
point: the required-key set as their code spells it, three reserved basenames,
`summary` as the only nullable key, the index structure, and the closed sets.
Their answer added four refinements, each folded in below and marked. Their
column assignments match this table's, with one wording difference resolved in
gate 5's row. A later round answered the four open questions outright — see
"Answered by the proving consumer" — and moved the required set from what their
code enforces to what their operator ratified.
Read from the consumer's working tree on 2026-07-25:
`tools/wiki_ingest/validate.py` and `tools/wiki_ingest/bundle.py` at commit
@ -103,19 +105,37 @@ considered.
## What STRICT_V1 must express
The phase-3 plan sketched this profile before the validator was read. Two of
its claims do not survive contact and are corrected here; the plan file is
corrected in the same commit.
its claims did not survive contact and are corrected here; the plan file
carries the same corrections. One of the two was then corrected a second time
by the consumer's operator — the sequence is recorded rather than smoothed
over, because the end state is deliberately neither what the plan drafted nor
what their code says.
1. **Required keys are four, not eight.** `REQUIRED_KEYS = (type, title,
description, timestamp)`. The eight-key list in the plan was a truncation of
`FRONTMATTER_KEY_ORDER`, which is a different thing: an eleven-key
*allowlist* that doubles as canonical emission order — `type`, `title`,
`description`, `timestamp`, `layer`, `source_tier`, `source_url`,
`source_sha`, `version`, `date`, `summary`. So `frontmatter_schema` needs
three fields, not one: required, allowlisted, and ordered. The non-required
keys are validated against closed sets **when present**, not required to be
present — a profile that requires all of them rejects pages the consumer
ships today.
1. **Required keys are eight — settled by the consumer, not read off their
code.** Their `REQUIRED_KEYS` constant is four (`type`, `title`,
`description`, `timestamp`), and this table first corrected the plan down to
it. Asked whether four was the contract or an artefact of unfinished triage
(Q2), they measured the whole bundle and their operator ratified eight:
`type`, `title`, `description`, `timestamp`, `layer`, `source_tier`,
`source_url`, `source_sha`, present on 522/522 documents. The four-key
constant is their *emit-path* minimum, not their contract.
Two consequences are carried deliberately:
- **STRICT_V1 is stricter than the consumer's own validator.** Should they
ever ship a document class that legitimately lacks `source_sha` — a
hand-written Guide, say — this profile rejects what their validator
passes. The agreed resolution is to bump the profile, not to bend the
bundle, and they send the requirement before shipping such a class rather
than after.
- **The allowlist remains a different thing from the required set.**
`FRONTMATTER_KEY_ORDER` is eleven keys — the eight above plus `version`,
`date`, `summary` — and doubles as canonical emission order. `version` and
`date` are layer-specific (present on 351/522) and `summary` on 349/522,
so all three stay optional-when-absent and are validated against closed
sets **only when present**; a profile that required them would reject
pages the consumer ships today. So `frontmatter_schema` needs three
fields, not one: required, allowlisted, and ordered.
2. **Three reserved basenames, not two.** `index.md`, `log.md`, and
`README.md`. README is bundle documentation, reserved like `index.md`: it
carries no frontmatter, is skipped by the per-document gates, is never
@ -184,28 +204,52 @@ profile the observable output must stay byte-identical (assumption C1, proven
by the golden suite), so `index_policy` has to express both shapes rather than
generalising one into the other.
## Open questions for the proving consumer
## Answered by the proving consumer
- **Q1.** Gate 5 escalates the injection scan from WARN to ERROR on a
`verbatim` disposition, because no LLM rewrite layer sits in front of the
published text. That is a policy tier chosen per document, over a guard
verdict. Should the profile be able to express "this document class gets a
stricter gate tier", or does that stay entirely in the consumer's hands?
This repository's position is the latter — the profile describes structure,
not security tiers — but it is the consumer's call, since it decides where
their gate lives after the port.
- **Q2.** Is the four-key required set final, or is it four only because seed
triage has not finished? `layer`, `source_tier`, `source_url` and
`source_sha` are allowlisted and, in practice, present on every generated
document — if they are meant to be required, STRICT_V1 should require them.
- **Q3.** Gate 4's second half asserts that no `concepts/` page may derive from
the changelog page. That reads as a layer-provenance rule, which is Profile
territory, but it is expressed as a URL predicate over one specific upstream,
which is Consumer territory. Mapped to Consumer here. Correct?
- **Q4.** Gates 19 and 22 are mapped to Consumer because they need state this
library never collects (link liveness, page hashes). If either is expected to
move, the profile would have to grow a notion of external state, which is a
larger change than phase 3 scopes.
All four questions were answered on 2026-07-25, each measured or read out of
their code that day rather than recalled. **No row in the mapping moved**; the
reasons under three of the four are sharper than the ones this table drafted.
- **Q1 — gate tiers stay Consumer/Guard.** This repository's position was
accepted, and the escalation turns out not to be a document-class rule at
all: gate 5 escalates WARN to ERROR when the per-version *record* carries
`disposition == "verbatim"` (`validate.py:528-540`), i.e. when the LLM
rewrite layer was skipped and the input lint is the only filter left. That is
run state, not bundle shape — a profile could not see it even if it wanted
to. A profile describes structure; it does not set security tiers.
- **Q2 — eight, ratified by their operator.** Folded into "What STRICT_V1 must
express" above, together with the consequence both sides accepted.
- **Q3 — Consumer, for a sharper reason than "URL predicate".** The check is
`is_changelog_url()` (`docs_corpus.py:181-184`): it lowercases the last URL
segment and tests membership in `("changelog", "release-notes")` — a
vendor-specific slug list for one upstream. Two details worth carrying: the
gate is **not** scoped to `concepts/` at all (it walks every non-reserved
document across both layers, `validate.py:480-492`, despite a rationale
string that says "concepts layer"), and it spares their own release pages
only because their `source_url` ends in `CHANGELOG.md`, whose last segment is
`changelog.md` rather than `changelog`. That is a coupling, not a design. A
structural profile could express "layer X may not carry source_tier Y"; it
could not express this.
- **Q4 — both stay Consumer.** Gate 19 sits in their `NEVER_BLOCKING_GATES`
(`validate.py:86-88`) by construction, precisely because liveness is a flake
magnet: a profile that owned it would inherit a gate that can never block.
Gate 22 is their provenance model, the one commons decision D1 settled in
their favour, and it is why the ordered-prefix emission matters. The profile
gets no notion of external state from either.
Two confirmations arrived unasked and are recorded because later work leans on
them:
- **Assumption C2 holds.** `validate.py` is still the single entrypoint for
every per-run gate, and the registry at lines 50-78 is the whole set — 5
`INGEST_*`, 7 `LLM_*`, 13 `BUNDLE_*` = 25. A change reaches this repository
as a message before it can be read out of their code.
- **The index shape is not negotiable downward**, which makes "express both
shapes rather than generalise one into the other" the right call: one index
per directory, exactly one `# ` heading, entries `* [Title](link) -
description` and nothing else, directory and index matching in both
directions, and a root index carrying `okf_version` / `bundle_profile` /
`okf_spec_commit` frontmatter in that key order.
## What this table does not do
@ -213,3 +257,7 @@ It does not port anything. Per the plan, porting starts only after this
mapping is reviewed, and the Guard column must contain no rows implemented in
this repository — the phase-2 grep gate over guard functionality still applies
unchanged.
Both reviews are in as of 2026-07-25 — the proving consumer's (above) and the
operator's — so the port may begin. The Guard-column constraint is unaffected
by either.

View file

@ -19,10 +19,10 @@ the golden suite is the regression harness for that claim.
- `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
set, the *allowlisted* key set, and the canonical emission *order*.
STRICT_V1 requires eight 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
@ -39,16 +39,19 @@ the golden suite is the regression harness for that claim.
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`).
(`Concept`/`Guide`/`Reference`/`Release`), eight required frontmatter keys
(`type`, `title`, `description`, `timestamp`, `layer`, `source_tier`,
`source_url`, `source_sha`) 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.
This line was drafted as eight, corrected to four against the consumer's
`REQUIRED_KEYS` constant on 2026-07-25, and settled at eight by their
operator the same day: the four-key constant is their emit-path minimum,
not their contract, and all eight are present on 522/522 of their
documents. STRICT_V1 is therefore deliberately stricter than the
consumer's own validator; the split table records the agreed consequence.
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.