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.
263 lines
16 KiB
Markdown
263 lines
16 KiB
Markdown
# Phase 3 split table — the proving consumer's 25 gates
|
|
|
|
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: 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
|
|
`9ee5a8e` (repository HEAD `20bcdd6`). Their `validate.py` is the single
|
|
entrypoint for every per-run gate, so this table is exhaustive over the gate
|
|
set as it stands at that commit — assumption C2 is satisfied only as long as
|
|
that remains true, and a gate added there after this date is not in this table.
|
|
|
|
## The three columns
|
|
|
|
| Column | Meaning | Goes where |
|
|
|---|---|---|
|
|
| Profile | Structural bundle contract: types, frontmatter keys, paths, reserved files, index shape | This phase, `BundleProfile` |
|
|
| Guard | Content safety: carriers, secrets, injection, active content | `llm-ingestion-guard` calls — never reimplemented here |
|
|
| Consumer | Domain logic: source pipelines, LLM contract, promotion, run accounting | Stays in the consumer's repository |
|
|
|
|
## Stage A — detection/acquisition
|
|
|
|
| # | Gate | Default class | Column | Note |
|
|
|---|---|---|---|---|
|
|
| 1 | `INGEST_LEDGER_DIVERGENCE` | ALERT (halts) | Consumer | Upstream re-poll reconciliation; no bundle structure involved. |
|
|
| 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. 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
|
|
|
|
| # | Gate | Default class | Column | Note |
|
|
|---|---|---|---|---|
|
|
| 6 | `LLM_STOP_REASON` | ERROR | Consumer | |
|
|
| 7 | `LLM_SCHEMA` | ERROR | Consumer | Frozen run-record schema plus summary length caps. |
|
|
| 8 | `LLM_OUTPUT_LINT` | ERROR | mixed | Decomposed below. |
|
|
| 9 | `LLM_GROUNDING_ANCHOR` | ERROR | Consumer | Anchor resolution against their concepts layer. |
|
|
| 10 | `LLM_SOURCE_TRACE` | ERROR | Consumer | Verbatim-quote and dropped-citation detectors. |
|
|
| 11 | `LLM_SEAM_LINKS` | ERROR | Consumer | Proposed links restricted to existing concept IDs. |
|
|
| 12 | `LLM_BUDGET` | ERROR | Consumer | Per-run call/token/cost caps. |
|
|
|
|
## Stage C — bundle conformance and publish
|
|
|
|
| # | Gate | Default class | Column | Note |
|
|
|---|---|---|---|---|
|
|
| 13 | `BUNDLE_FRONTMATTER_PARSEABLE` | ERROR | Profile | Every non-reserved `.md` opens with a complete fence. |
|
|
| 14 | `BUNDLE_TYPE_ENUM` | ERROR | Profile | Closed enum `Concept`/`Guide`/`Reference`/`Release`. |
|
|
| 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. 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. |
|
|
| 22 | `BUNDLE_HASH_REGISTRY` | ERROR | Consumer | Their page-hash snapshotter must cover the concepts layer exactly. |
|
|
| 23 | `BUNDLE_SANITIZATION` | ERROR | Guard | Carrier-freedom of published text. |
|
|
| 24 | `BUNDLE_BLAST_RADIUS` | ERROR | Consumer | Per-version diff caps from their run manifest. |
|
|
| 25 | `BUNDLE_SECRET_SCAN` | ERROR | Guard | Credential patterns over bundle text and run artifacts. |
|
|
|
|
## The two mixed gates, decomposed
|
|
|
|
The plan asks for exactly one column per gate. Two gates do not honour that as
|
|
written, because each bundles checks belonging to different columns. Splitting
|
|
them is the honest mapping; the alternative — assigning the whole gate to its
|
|
dominant column — would quietly move the residue to the wrong side.
|
|
|
|
**Gate 8, `LLM_OUTPUT_LINT`** runs `_lint_text` over enrichment summaries and
|
|
verbatim release bodies:
|
|
|
|
| Check | Column |
|
|
|---|---|
|
|
| Off-allowlist URL | Consumer (their editorial pinning; see gate 20) |
|
|
| HTML tag in prose | Guard — active content is squarely the guard's territory at v0.3.0 |
|
|
| base64-like blob | Guard |
|
|
| Credential pattern | Guard |
|
|
| Consumer-directed imperative | Guard — this is the injection posture, expressed as their house rule |
|
|
| Injection lexicon and hex blob | Guard |
|
|
|
|
**Gate 20, `BUNDLE_URL_ALLOWLIST`** bundles two unrelated rules:
|
|
|
|
| Check | Column |
|
|
|---|---|
|
|
| URL host/prefix on the pinned allowlist | Consumer |
|
|
| Domain is new versus the previous bundle state | Consumer (drift detection over their own retained state) |
|
|
|
|
Neither half is Profile: a URL allowlist is editorial policy about which
|
|
sources a bundle may cite, not a statement about bundle structure. It is listed
|
|
as mixed only because the checks have different natures, and both land in the
|
|
same column — noted rather than merged, so a later reviewer can see it was
|
|
considered.
|
|
|
|
## What STRICT_V1 must express
|
|
|
|
The phase-3 plan sketched this profile before the validator was read. Two of
|
|
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 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
|
|
listed in a generated index, and is still subject to the link, URL,
|
|
sanitization and secret gates. A two-value reserved-file policy
|
|
(required-present / required-absent) cannot express it; the third value is
|
|
"present-and-exempt-from-document-gates".
|
|
|
|
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
|
|
spell these `bundle/concepts` and `bundle/releases` because their validator
|
|
walks a repository whose bundle lives in `bundle/`. That prefix is where
|
|
*they* keep the bundle, not part of the contract — see "No placement field"
|
|
below.
|
|
- **Index policy**, which differs from this library's default in four ways:
|
|
one index per *directory* rather than one per bundle; entries formatted
|
|
`* [Title](link) - description` rather than `- [label](target)`; index
|
|
contents must match the directory exactly, both directions; and the root
|
|
index carries frontmatter pinning `okf_version`, `bundle_profile`,
|
|
`okf_spec_commit`.
|
|
## No placement field — a field the profile must NOT grow
|
|
|
|
The catalog reported on 2026-07-25 (their commit `3ccb5f6`,
|
|
`docs/okf-second-brain/spec.md` §1 and §8) that bundle placement is
|
|
**unmandated**, measured against four consumers rather than taken on report:
|
|
exactly one installs a bundle anywhere; for the other three the bundle
|
|
directory is a caller-supplied argument per run, with no default, no discovery
|
|
and no `$HOME` expansion. Discovery, where it happens, keys on the §3 root
|
|
marker (`index.md` plus `okf_version`), never on a path.
|
|
|
|
Three consequences for `BundleProfile`, recorded here because this phase's
|
|
non-goal is precisely "do not make the profile too narrow for the Node-world
|
|
conventions in phase 4":
|
|
|
|
- No root-location or install-path field. There is no path form to express.
|
|
- "No installation root" is a conforming consumer shape, so a profile that
|
|
assumes every consumer has one would be too narrow in exactly the warned way.
|
|
- Installing and writing are separate axes: a consumer may write into a bundle
|
|
it never placed, so write policy must not be tied to a placement concept.
|
|
|
|
Every path in this document is therefore bundle-relative, and the bundle
|
|
directory stays what it is today in both doors — an argument.
|
|
|
|
The index-policy divergence is the largest single item. Under the default
|
|
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.
|
|
|
|
## Answered by the proving consumer
|
|
|
|
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
|
|
|
|
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.
|