The code span holding `* [Title](link) - description` wrapped across a line break, so a line-oriented reader sees `[Title](link)` as a real link into docs/ and reports a broken target. The three sibling occurrences in this repo all keep the span intact on one line; this one was incidental wrapping, not a decision. Reflowing is the local fix. It is not the same as widening a checker's code-span stripping to span line breaks, which this repo argued against on 2026-08-03: that would hide the class instead of resolving it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJUFZhHzf9AqLucMsJy6u1
312 lines
19 KiB
Markdown
312 lines
19 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.
|
|
|
|
## The index policy, and the conflict that shaped it
|
|
|
|
The convention owner reported on 2026-07-25 that the second-brain spec's §3
|
|
mandates an `index.md` at every directory level, that `IndexPolicy` as it then
|
|
stood could not express that, and — the load-bearing part — that an index is an
|
|
**authored** count of a directory's children and never a filesystem lookup. On
|
|
their reading, an index reader or validator that enumerates a directory to
|
|
build or check an index has implemented the wrong contract, with a silent
|
|
failure mode: code written to the wrong reading passes every test one would
|
|
think to write.
|
|
|
|
That was checked against the proving consumer before it was adopted, and the
|
|
two consumers turn out to be **directly opposed on exactly this point**:
|
|
|
|
- `validate.py:1081-1120`, gate `BUNDLE_INDEX_COMPLETE` (severity ERROR),
|
|
builds its expected set by enumerating the directory and fails unless the
|
|
index matches it exactly in both directions.
|
|
- `bundle.py:498-567` writes every index from what a tree walk finds, and its
|
|
own docstring calls `index.md` machine-generated.
|
|
|
|
So one consumer mandates the derivation the other calls the wrong contract.
|
|
Neither is incoherent inside its own spec, and this repository is not the venue
|
|
to adjudicate between them. What it settles is narrower and sufficient:
|
|
**authored-versus-derived cannot be a library invariant in either direction.**
|
|
It is a policy field (`entries_match_directory`), as are per-directory scope,
|
|
the heading requirement, whether prose is admitted, and the root-index key set.
|
|
|
|
Three consequences that outlive this phase:
|
|
|
|
1. **Nothing in this library enumerates a directory.** The caller supplies the
|
|
listing; `IndexPolicy.violations` refuses one when the profile's index is
|
|
authored, and refuses to run without one when it is derived. The
|
|
convention owner's construction rule is enforced at the call site rather
|
|
than documented and hoped for.
|
|
2. **Root and nested indexes are asymmetric**, confirmed independently in both
|
|
consumers: the root carries frontmatter (`okf_version`/`bundle_profile`/
|
|
`okf_spec_commit` for the wiki, `okf_version`/`okf_layout` for the catalog)
|
|
and nested indexes carry none. Different key sets, same shape — which is
|
|
why it is a profile field and not a constant.
|
|
3. **A per-entry description is template-level, not a separate requirement.**
|
|
The wiki requires `* [Title](link) - description`; the catalog requires
|
|
prose in the index and no per-entry description. A profile that wants none
|
|
simply does not name `{description}` in its template.
|
|
|
|
`DEFAULT` keeps upstream's root-only index and judges nothing, for the same
|
|
measured reason it carries no required frontmatter key set: upstream OKF binds
|
|
`index.md` to the bundle root alone, so a default demanding one per level would
|
|
declare upstream-conforming bundles invalid.
|
|
|
|
## 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.
|