docs(phase-3): fold in the proving consumer's review of the split table

Their answer crossed with the draft rather than responding to it, so the
two readings of the same validator are independent. They agree on every
substantive point -- four required keys, three reserved basenames,
summary as the only nullable key, the index structure, the closed sets --
and on the column assignments. That is corroboration, not confirmation
bias, and it is worth more than either reading alone.

Four refinements folded in:

- Emission order is an ordered prefix followed by remaining keys SORTED,
  not just an ordered list. This is the commons D1 outcome and it is what
  keeps regeneration byte-identical; normalising key order any other way
  breaks their BUNDLE_HASH_REGISTRY gate, which is why D1 mattered.
- Values may be a non-empty LIST of strings, not only scalars. Same block
  -list shape Door C already refuses to round-trip, which is why imported
  concepts are written verbatim -- a scalars-only parser would destroy
  their Release pages, and a scalars-only profile would reject them.
- Non-required allowlisted keys are validated against closed sets WHEN
  PRESENT, not required to be present.
- Internal-link resolution generalises only because path_policy owns the
  layer layout; noted on the gate row as their caveat, accepted.

Gate 5's row now distinguishes the Guard COLUMN (functionality that is
the guard's and is never reimplemented) from the call SITE (theirs, in
their acquisition stage) -- the one place our wording and theirs differed.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 12:34:03 +02:00
commit 08ca68fee2

View file

@ -1,9 +1,17 @@
# Phase 3 split table — the proving consumer's 25 gates
Status: **draft, awaiting review.** Phase 3's plan requires this mapping before
any porting, reviewed with the operator and with the proving consumer
(`claude-code-llm-wiki`). Nothing here is implemented yet, and nothing in the
consumer's repository is modified by this repository.
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.
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.
Read from the consumer's working tree on 2026-07-25:
`tools/wiki_ingest/validate.py` and `tools/wiki_ingest/bundle.py` at commit
@ -28,7 +36,7 @@ that remains true, and a gate added there after this date is not in this table.
| 2 | `INGEST_CHANGELOG_PARSE` | ERROR | Consumer | Source-format parsing for one specific upstream. |
| 3 | `INGEST_EXPECTED_SET` | ALERT | Consumer | npm/changelog reconciliation against known deltas. |
| 4 | `INGEST_DOCS_CORPUS_SANITY` | ERROR | Consumer | Corpus size/shape thresholds for one named corpus. Its second half — "a concept must not derive from the changelog page" — is a layer-provenance rule expressed as a URL test; see open question Q3. |
| 5 | `INGEST_INPUT_INJECTION_SCAN` | WARN (ERROR when verbatim) | Guard | Carrier scan plus semantic lexicon on fetched input. The disposition-aware escalation is Consumer policy over a Guard verdict; see open question Q1. |
| 5 | `INGEST_INPUT_INJECTION_SCAN` | WARN (ERROR when verbatim) | Guard | Carrier scan plus semantic lexicon on fetched input. Guard *column* means the functionality is the guard's and is never reimplemented — not that the call site moves here. The consumer keeps the call, as they state, since it sits in their acquisition stage. The disposition-aware escalation is Consumer policy over a Guard verdict; see Q1. |
## Stage B — LLM-output contract
@ -51,7 +59,7 @@ that remains true, and a gate added there after this date is not in this table.
| 15 | `BUNDLE_RESERVED_FILES` | ERROR | Profile | `log.md` required-absent; `index.md` structure. Three reserved basenames, not two — see "What STRICT_V1 must express". |
| 16 | `BUNDLE_YAML_STRICT` | ERROR | Profile | Key allowlist, required keys, and the StrictYAML scalar subset. |
| 17 | `BUNDLE_LAYER_PATH_TYPE` | ERROR | Profile | Path prefix ⇄ `layer``source_tier``type` agreement. |
| 18 | `BUNDLE_LINKS_INTERNAL` | ERROR | Profile | Every relative cross-link resolves inside the bundle. |
| 18 | `BUNDLE_LINKS_INTERNAL` | ERROR | Profile | Every relative cross-link resolves inside the bundle. Consumer's caveat, accepted: link resolution assumes their layer layout, so it generalises only because `path_policy` owns the layout too. |
| 19 | `BUNDLE_LINKS_EXTERNAL` | WARN (never blocks) | Consumer | Liveness polling; needs network state this library does not collect. |
| 20 | `BUNDLE_URL_ALLOWLIST` | ERROR | mixed | Decomposed below. |
| 21 | `BUNDLE_INDEX_COMPLETE` | ERROR | Profile | Per-directory index exactness plus root-index metadata keys. |
@ -104,7 +112,10 @@ corrected in the same commit.
*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.
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.
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
@ -118,8 +129,20 @@ Beyond the frontmatter set, `BundleProfile` must carry:
- **Type policy:** closed enum, four values.
- **Nullable keys:** `summary` alone may be `null` — the single deliberate
escape from strings-only frontmatter.
- **Value shapes:** a string scalar, **or a non-empty list of strings**. This
is the same block-list shape Door C already refuses to round-trip through
this library's line-oriented parser, which is why an imported concept is
written verbatim. A profile that assumes scalars-only rejects the consumer's
own `Release` pages, and a parser that assumes scalars-only would silently
destroy them.
- **Key-name grammar:** `^[a-z_]+$`.
- **Scalar encoding:** the StrictYAML subset (JSON-double-quoted scalars), so
a regeneration over the same data is byte-identical.
- **Emission order — carry this over intact:** the ordered prefix above,
followed by any remaining keys **sorted**. This is the door-A model settled
by commons decision D1, and it is what makes regeneration byte-identical.
A profile that normalises key order any other way breaks the consumer's
`BUNDLE_HASH_REGISTRY` gate, which is what made that decision matter.
- **Layer/path policy:** `concepts/` ⇄ layer `concepts`, source_tier
`official-docs`, any type except `Release`; `releases/` ⇄ layer `releases`,
source_tier `changelog-derived`, type `Release` exactly. Their constants