U4 escalated tag requirement 1: real v0.2 frontmatter is block YAML, and "emit
accepts a block-list value" had been scoped against a list of strings when
upstream's canonical sources is a list of five-key mappings. Re-sized against
the code rather than against its tests, and the answer runs the other way.
Measured: emit (profiles.py:197) takes Mapping[str, str] and renders one
"key: value" line per key through _render (:211), verbatim but for
source_query's whitespace collapse; _is_legal_value (:97) admits a string or a
non-empty list of strings, never mappings; the only production call sites are
materialize.py:166 and inbox.py:129, both passing dict[str, str].
The earlier "discharged" note read the validator correctly and drew too much
from it. But the gap it left is not U4's either: A-E4 owes at least a resource,
not upstream's five keys, and a manifest source has no author, no last_modified,
and no bundle-internal resource in upstream's sense. A field with no reader is
not written. So the question was never how large a YAML emitter we need -- it
was which form sources takes.
Chosen: sources: [{ id: <id>, resource: <ref> }], the same inline flow form A-E3
already chose for generated. It keeps parse_frontmatter line-oriented, which
matters because a block list pollutes that parser with false keys (- id,
resource) -- the failure mode documented for po-claude -- and _is_ingest_owned,
the one piece of this tag we called risky, reads THROUGH that parser. The block
form would force the collision gate and the parser to be hardened in the same
session. It also satisfies commons' section 5 single-line MUST, so the DEFAULT
seam U4 opened stays closed, and section 11 asks for parseable YAML, not block
YAML. The deviation from upstream's emitted shape is real and is recorded as a
decision, like U1, not as compliance we may claim.
Consequences: requirement 1 is smaller than both earlier sizings -- zero emitter
work -- and D1a drops off the critical path to v0.5.0a1. A-E4 is corrected from
"block list" to the flow form before the pilot receives it; a test spec that
outlives its decision is the A-E6 defect class.
Swept the same defect class while here: the TDD order and three other passages
wrote okf_version: "0.2" QUOTED, contradicting A-E6, which is precisely the form
catalog measured as exit 1. All five now unquoted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Measured, after asserting the opposite to the security repo an hour earlier: no
door in this library can create, import, or persist an executable file. Door C
skips every suffix that is not .md (importer.py:226-231); Door B has no extractor
for .py in either the core or the optional set (extract.py:113,131-142); Door A
writes only ingest-*.md. We told the guard a third-party attester .py could reach
a consumer's tree through our door. It cannot. Reasoned, not measured, corrected
the same hour.
The real exposure is the inverse. Door C writes a merged concept verbatim,
frontmatter included, and under v0.2 that frontmatter can carry
executor.resource and attester.resource. So we import the reference to executable
code while never importing the code. Either the pointer dangles -- noisy, safe --
or it resolves to something else already in, or later added to, the destination
tree, and the imported concept references code it never shipped. That second one
is a substituted reference, not a broken one, and it looks valid.
Also recorded: the guard is a PERSIST gate, and executable code carries its risk
at EXECUTION. A .py entirely safe to store may be dangerous to run, so no persist
gate covers the class section 10 introduces, and the execution boundary has no
owner in this stack -- upstream deferred the attester ABI and sandboxing, and we
declared runtime out of scope on that same deferral. Written down because "the
guard covers it" is a reasonable conclusion to draw and a hard one to catch later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Operator directive: when the v0.2 implementation is ready, every OKF-consuming
repo is told through the coord mailbox what to do or test. That was a step title
in the runbook and is now a table with content -- nine repos, each with the
action asked of them and, more importantly, what happens if they do nothing.
For most the honest answer is "nothing changes", and saying that plainly is more
useful than an open invitation to review. That is the black-box commitment made
checkable rather than asserted.
Two new GA exit criteria: every repo in the table has been sent its message and
the table was re-derived rather than reused; and the security repo's section 10
boundary questions are either answered or their being open is recorded as an
accepted, stated limit. The second matters because Door C is the path an
external bundle's executable attester would take into a consumer's tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
The verification criterion said the release checklist carries an
upstream-version re-check. It now names the procedure that check runs, which is
the part that makes it more than a reminder.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
The standing always-latest policy had no procedure, and v0.1 -> v0.2 showed what
that costs: the spec was read, the shipped example bundles were not, and five
findings a spec reading had settled wrongly surfaced only after someone asked
whether we had studied the examples. One of them re-sized the release itself.
docs/upstream-okf-upgrade-runbook.md is that procedure. Every step names the
concrete failure it prevents, and each is a failure that actually happened:
- Pin before reading. Two repos read main independently, which felt like two
confirmations and was one unstable reference read twice -- and SPEC.md was
edited after the migration commit, so neither alone was the whole answer.
- Enumerate the whole okf/ tree. bundles/ held the real examples and was on
nobody's list until the tree was listed.
- Read the shipped examples, not only the normative text. A directory named
samples/ turned out to be a runner config, not samples of the format. And
markdown-converting fetch tooling strips exactly the byte-level properties
(BOM, trailing newline, quoting) that break downstream gates.
- Name fixtures by repo AND path. Two repos had examples/ingest-golden-file/
holding different fixtures with zero content overlap, and a pilot baseline was
agreed by a name that pointed at two files.
The black-box commitment is now an invariant in CLAUDE.md rather than an
emergent property: a consumer's cost for an upstream release is a re-run.
Additive profiles, byte-stable existing profiles, keyword-only new parameters so
positional call sites survive, no churn in consumer fixtures. The boundary is
stated rather than glossed -- shape changes are absorbed, upstream changes to
consumer-authored content are not, and those get a measured exposure report per
consumer instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Two corrections to claims this repo made, both measured rather than argued.
1. `examples/ingest-golden-file/` names TWO different fixtures. Ours holds
ingest-orders.md + ingest-products.md; po-claude's holds ingest-costs.md +
ingest-edge.md; the manifests differ at line 3 char 41; content-file overlap
is zero and the only shared filename, index.md, is byte-different. We agreed
a pilot baseline by name, and the name pointed at two files. A-E1 is the stop
condition, so a mismatched baseline would have surfaced as a profile defect.
The pilot runs THEIR fixture -- A-E1 asserts byte-identity with what their
pinned version produces, which is a claim about their baseline, not ours --
and the report must state repo and path.
2. "The shared fixture, ratified by two independent implementations" was false.
We took it from an incoming message and passed it on the same day without
measuring, while holding a standing rule that incoming messages are premises,
not facts, and applying that rule only to spec readings. Commons caught it.
The :29 argument in V1 survives in commons' stronger form: the clause requires
reproducing "the shared golden extractions", the definite article presumes one
set where there are two, so it has no referent today. (b) therefore does not
break a shared fixture we have -- it cements that one can never arise, since a
producer-bearing field inside the byte-compared prefix makes convergence
impossible by construction. The :280 half stays observable and fires on our next
release regardless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
V-A1 is discharged: everything is now read at pinned commit 3fcbb9f, not off
main. v0.2 itself landed in 780fe9d; SPEC.md was edited after it, so "the v0.2
commit" and "the current spec text" are different objects.
Session 19 read the spec. It never read the four bundles the tooling migrated
to v0.2, and reading them corrected things a spec reading had settled wrongly.
okf/samples/ga4_merch_store is not a bundle -- it is a runner config that
generates one, so it says nothing about output shape.
- U1 No reference bundle declares okf_version. All four root index.md files
have no frontmatter at all. A-E6 is therefore a stricter posture than
upstream's own implementation -- defensible, but a decision rather than an
assumption. po-claude's "deviation" #2 was conformance. catalog's gate reports
MISSING on every official Google bundle.
- U2 Root index.md is the ONLY index permitted to carry frontmatter (12).
Constrains the fixture and the index policy.
- U3 `generated` is used for HUMAN-authored content upstream, so it cannot be a
machine-generated or ownership predicate. Empirical confirmation of commons'
axis finding, stronger than their spec-text argument.
- U4 Real v0.2 frontmatter is multi-line block YAML: flow sequences, block lists
of flow mappings, nested mappings, block lists of multi-key mappings, booleans,
dates. Requirement 1 was scoped against a list of strings; the canonical shape
is a list of five-key mappings. The one-session sizing must be redone.
- U5 7's actor convention gives `<producer>/<version>` for tools, and upstream
uses it. That is real evidence for (b), against the (d) we recommended to
commons today. Sent to them as a correction.
- U6 Conformance and the consumer-tolerance MUSTs quoted verbatim, including the
bare-`verified`-as-one-element-list requirement on D1's reader.
- U7 An undocumented `not:` family observed in a bundle, unresolved, recorded so
it is not mistaken for checked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
We carried (b) -- generated.by naming this library and its version. Commons
found it collides with ingest-spec's conformance clause: :29 requires a
conformant implementation to reproduce the shared goldens byte for byte, and
`generated` sits inside the mandatory ordered prefix. A producer's name and
version inside a shared fixture means no sibling implementation can reproduce
it, and Golden regression fires on every version bump with no contract change.
They measured it as normative-only for them, since examples/ingest-golden-*
does not exist in commons. Measured here, it is observable today: four fixtures
carry `generated:` on line 8 inside the byte-compared prefix, and
ingest-golden-file is both the shared fixture against the sibling
implementation and the baseline po-claude runs the pilot against.
So we back (d) -- `process:<fixed id>` per v0.2 section 7. Second reason,
visible only from this side: it keeps _is_ingest_owned an equality check.
Under (b) the collision gate must either prefix-match and ignore the version --
parsing a value we had just made structured -- or stop recognizing files an
earlier version wrote, which fails safe but breaks re-running a manifest across
a version bump.
The cost of (d) is recorded rather than minimized: producer identity leaves the
bundle, and a log does not travel through Door C. The decision stays commons'
to ratify.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
All three pilots accepted. Their measurements land in the plan now rather than
as failed runs later, which is the entire return on asking before building.
Six corrections, each measured by the consumer rather than reasoned by us:
- A-E6 specified `okf_version: "0.2"` quoted. Catalog measured the quoted form
failing their shape regex with exit 1, and a UTF-8 BOM making the marker
invisible while still exiting 0. Both now stated as raw-byte expectations.
- A-E2 said the diff lies only *inside* the frontmatter block. No bundle whose
root index has no frontmatter block can satisfy that together with A-E6.
- B-E3 covers one of catalog's three gates; the other two cannot see our
fixture. Recorded so the report is not read as broader than it is.
- Our claim that no gate keys on the version value was wrong as formulated:
check-okf-parity.mjs puts the raw value in its signature. It compares without
judging -- verdict-invariant, not value-blind.
- C-E1's ground truth is 524 non-reserved documents, not 522; 522 was correct
at d2c12d2.
- `materialize_bundle` gains keyword-only `profile=DEFAULT` on po-claude's
request. The signature already has a `*`, so their three-positional call path
stays source-compatible and additivity becomes a property of the signature
instead of something a consumer measures.
Also recorded: po-claude closed their index-parser defect themselves (f41264f),
so do not design around it; the assumption under it -- "a generated index has no
frontmatter" -- is not peculiar to them. Dropping the superseded `timestamp`
draws one catalog warning per concept file, decided after the pilot, not before.
The estimate's open caveat is discharged by reading profiles.py:197: emit is one
formatting path, the validator already admits list values, so only the emitter
side is missing and the sizing stands.
CLAUDE.md gains a standing constraint: consumer content stays at form level in
public files. The wiki's bundle is private pending an Anthropic ToS assessment,
their report goes back through coord, and a push is not reversible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Walking each pilot test back to what it actually exercises produces a result
the deliverable ordering had obscured: none of the three needs D1's
structured reader.
Test A emits v0.2 -- `generated` is an inline flow mapping, a formatted
string, and `sources` needs the emitter to accept a block-list value. Neither
requires reading one back. Test B runs catalog's gate on Test A's output.
Test C validates the wiki's v0.1-shaped documents against a v0.2 variant
profile; their documents carry no v0.2 families, so block-list reading is
never reached.
The structured reader exists to consume third-party v0.2 bundles -- D3, Door
C -- and no pilot repo sends us one. So D1 splits: the emitter half is in the
tag, and the reader half moves behind the pilot where the feedback can inform
it. Two consequences worth stating plainly.
V2 is off the critical path. It chooses the reader's design, and the reader
now lands after the pilot; the tag is not waiting on a decision.
The tag is roughly a session of work, not a phase: an emitter change, a
profile constant following the existing STRICT_V1 pattern, a profile-aware
ownership predicate, `sources` derivation, one generated fixture.
Risk concentrates rather than spreads. `_is_ingest_owned` is the pre-mutation
collision gate, so a defect there is expensive and quiet -- its
characterization test is written before it is touched. The emitter change
runs through DEFAULT's path, so V-A6 is proven by the golden suite rather
than assumed.
The sizing itself carries a caveat in the doc: it treats
FrontmatterSchema.emit as a single formatting function, inferred from its
tests rather than read. That is a premise like any other and gets verified
before the estimate is relied on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
The pilot invitations asked for feedback without specifying what to run or
what we expect to see. That returns a description, not a measurement: only a
stated expectation lets someone else's run falsify our model instead of
merely confirming it.
Each of the three tests now carries a numbered procedure, numbered expected
outcomes, and an explicit list of what would surprise us. Naming the
surprises is the load-bearing part -- a consumer who sees something odd but
passing otherwise has no reason to mention it.
Test A (producer, portfolio-optimiser-claude) runs one already-working
manifest twice with the same explicit ingested_at, once under DEFAULT as
baseline and once under OKF_V0_2, and diffs. A-E1 is the stop condition: if
the DEFAULT run is not byte-identical to their current pinned output, we have
broken a v0.1 consumer and the pilot ends there. The named surprises include
`at` differing from the ingested_at they passed, which would mean a
wall-clock crept in, and a collision refusal on a file that IS theirs, which
is the inverse of the fail-safe we predicted -- we expect foreign files to be
refused, so a false refusal of their own is the defect.
Test B (gate, catalog) runs their unmodified gate on the v0.2 fixture and on
a 0.1 control. B-E3 -- that no gate other than the version gate behaves
differently between the two -- is what converts our reading of their form
regex into a measurement. A membership list or equality comparison anywhere
in their chain is exactly what this is paid for.
Test C (expressiveness, wiki) is run by us, read-only at a recorded commit,
and the report goes to them. What we ask them to check is the part we cannot
measure from outside: whether a field we called an optional addition is in
fact load-bearing in their pipeline, and whether "no change required" holds
operationally rather than only formally.
Feedback gains a per-expectation verdict line so three independent runs are
comparable and a disagreement is located rather than merely reported. The
request also says outright that a wrong expectation is a better result than a
clean run, since a clean run only confirms what we already believed.
Inputs come from this repo at the pre-release tag; nothing is transported
through the mailbox except the specification.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Operator directive 2026-07-26: consumers are better served by getting the
latest version early and reporting back than by us holding it until we judge
it finished.
This closes a real gap. Every test in the plan -- including V-A8 against
upstream's reference implementation -- asks whether output is conformant.
None asks whether it is usable: whether a bundle is awkward to construct,
whether a rejection message is actionable, whether the profile can express
what a consumer's actual data needs. Only real data surfaces that.
Pilot set is three repos, one axis each, chosen for signal:
portfolio-optimiser-claude for the producer path (one real manifest run),
catalog for gate acceptance (their gate on our fixture, measured rather than
inferred from the regex), and claude-code-llm-wiki for expressiveness across
522 real documents.
The load-bearing design detail is that most of the pilot is read-only on our
side: we run the v0.2 reader over real consumer bundles and send the report.
That needs no adoption, no writes into their trees (O2 holds), and no change
to a contract their operator ratified. Only the producer axis asks a
consumer to do anything, and it asks for one run.
Excluded with reasons rather than silently: okr (Node side not yet lifted),
linkedin-studio (v0.2's provenance families would put implicit pressure on
the ingest/published carve-out we agreed not to normalize), commons (they
are deciding V1 -- a participant, not a test site).
Shipping a provisional surface without owing stability rests on three rules,
not on saying "provisional": OKF_LATEST does not point at v0.2 until GA, so
flipping it is the GA event rather than a merge side effect; the docstring
and CHANGELOG name the pilot repos; and breaking changes during the pilot
get no deprecation cycle. Stating that last one up front is what buys the
freedom to act on feedback -- discovering it later is what turns a pilot
into a de-facto release.
Feedback is requested in five named parts, because unstructured reports are
not comparable across three repos. The one that matters most is "what was
awkward but worked": workarounds are the highest-value signal and never
appear as a failure.
GA exit criteria are testable, and carry one honest limit: a three-repo
pilot exercises only what those three use, so `sources` with usage_window,
multi-verifier `verified`, and Attested Computation will likely go
unexercised. Those stay marked provisional at GA instead of being silently
promoted -- claiming otherwise would be the same unearned-claim pattern that
"conform first, claim after" exists to prevent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Operator directive 2026-07-26: the library always supports the current
latest version of Google OKF. v0.2 shipped 2026-07-25, so v0.2 support is
committed work rather than something a consumer has to request. This
overrides the previous default answer to open question V3 ("no until a
named consumer asks"), which is kept in the plan marked superseded so the
override reads as deliberate.
Support is additive: a new profile, never a migration of the existing two.
That single design choice is what makes an always-latest policy sustainable,
and it resolves the tension the directive would otherwise create with three
constraints that do not yield to it:
- DEFAULT states commons' ingest-spec section 5 layer, so its `generated`
shape is commons' call. Under the additive design this stops blocking us,
which takes commons off the critical path.
- STRICT_V1 mirrors the proving consumer's ratified contract; changing
another repo's contract from here would violate O2.
- v0.2 defers the attestation receipt and verdict wire formats upstream, so
the format is supportable and the unspecified runtime is not. It re-enters
scope when upstream specifies it.
This is also the first time the phase-3 profile abstraction is forced by
something outside this repo rather than by a second consumer, which is the
better test of whether the seam was cut in the right place.
Deliverables D1-D6 replace the earlier decision-round framing: a frontmatter
model that can carry block lists (`sources`, multi-verifier `verified`), an
OKF_V0_2 profile plus an OKF_LATEST alias whose moving-target tradeoff is
documented rather than hidden, Door C conformance against the consumer
tolerance rules, `Attested Computation` round-trip, v0.2 golden fixtures, and
a release-checklist re-check so the standing policy cannot decay silently.
Two new assumptions carry the weight. V-A7 forbids any profile from emitting
`timestamp` together with a malformed `generated`, since that combination
would have neither a valid `generated.at` nor an eligible section 13.1
fallback. V-A8 validates our own v0.2 fixture against upstream's reference
implementation, because every other test in the suite only asks whether we
agree with ourselves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
Everything built so far targets OKF v0.1. Upstream published v0.2 on
2026-07-25, so the plan records how the library relates to it and, more
importantly, who owns each decision.
Read from the spec itself rather than secondhand, which corrected two
readings that a summary had gotten wrong:
- STRICT_V1's `timestamp` is NOT a defect. Section 13.1 grants consumers a
documented fallback to legacy `timestamp` precisely when `generated` is
absent, and STRICT_V1 emits no `generated`. Nothing is asked of the wiki.
- The one measured shape problem is DEFAULT's `generated: "true"`, because
v0.2 requires `generated.by` within `generated`. That key was not reserved
in v0.1, so it was legal when written; v0.2 claimed the name. DEFAULT
states commons' ingest-spec section 5 layer, so the fix is commons' call
and is raised there as open question V1 rather than patched locally.
Two findings shrink the work. The canonical form for `generated` and a
single `verified` is an inline flow mapping, which the existing scalar
parser already round-trips as an opaque string, so block-list support is
only needed for `sources` and multi-verifier `verified` -- and only if a
named consumer asks. And the collision degrades safely: `_is_ingest_owned`
returns False for a v0.2 mapping, so a foreign concept is refused rather
than overwritten.
The track sits between Phases 3 and 4 because Phase 4 freezes the
cross-runtime contract. Freezing a v0.1 shape into two runtimes would let
the shared fixture suite certify the drift instead of catching it.
Self-imposed rule, since the spec does not require it: conform first, claim
after. Declaring `okf_version: "0.2"` is a MAY with no conformance
checkpoint, so claiming it early would be permitted -- and would be the same
class of true-sounding misleading claim as reporting a 0.2.0 measurement
under a 0.3.1 heading.
Also moves the guard 0.3.1 measurement procedure out of session state and
into execution-order.md, where it belongs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
They named both on a live channel today, having withdrawn the half of their
original question that turned out not to bind them. Recorded here rather than in
a reply so the scope freeze has to answer them explicitly.
`write_index` ownership was genuinely ambiguous in this plan — they read
deliverable 4's "write" and asked which line owns it. It is deliverable 3, and
the plan now says so.
Their vocabulary requirement is already the Python half's shape:
`TypePolicy(allowed=None)` is the default and rewrites nothing, so there is no
DEFAULT_TYPE to snap to. The reference checker's collapse is a known deviation to
resolve with the convention owner under step 1, not a behaviour to lift.
`IndexPolicy` grew five judging fields and `IndexPolicy.violations`, closing the
gap a test has been pinning since `848e395`. `STRICT_V1.index` is now the proving
consumer's shape rather than DEFAULT's.
The design was settled by a conflict rather than by a preference. The convention
owner reported that an index is an authored count of a directory's children and
that a validator enumerating the directory has implemented the wrong contract.
Checked against the proving consumer before adopting it, the two turn out to be
directly opposed: gate BUNDLE_INDEX_COMPLETE (validate.py:1081-1120, ERROR) builds
its expected set by enumerating the directory and demands an exact bidirectional
match, and bundle.py:498-567 writes every index from a tree walk. Neither is
incoherent inside its own spec, so authored-versus-derived is a policy field in
both directions and a library invariant in neither.
Consequences encoded rather than documented: nothing here enumerates a directory
— the caller supplies the listing, `violations` refuses one when the profile's
index is authored and refuses to run without one when it is derived, so code
written to the wrong contract fails at the call instead of passing every test one
would think to write. Root and nested indexes are asymmetric (confirmed in both
consumers, different key sets). A per-entry description is template-level, so
`render_link` refuses both a missing description and an unwanted one.
DEFAULT keeps upstream's root-only index and judges nothing, for the same measured
reason it carries no required frontmatter key set: upstream binds `index.md` to
the bundle root alone, so a judging default would condemn conforming bundles.
25 new tests, 493 green. C1 re-proven: `git diff --stat examples/` empty.
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.
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.
Both arrived within minutes of the v0.4.0 announcement and both change
what we would otherwise have built or claimed.
CATALOG -- a profile field we must NOT add. Their steg 5 measured bundle
placement across four consumers: exactly one installs a bundle anywhere,
the other three take the bundle directory as a per-run argument with no
default, no discovery and no $HOME expansion. Placement is unmandated, so
BundleProfile gets no root-location or install-path field, "no
installation root" is a conforming consumer shape, and write policy must
not be coupled to placement. Recorded in the split table, which also now
spells the proving consumer's layer prefixes bundle-relative
(concepts/, releases/) rather than repeating the bundle/ prefix their own
repository layout carries -- that prefix was placement leaking into a
contract.
COMMONS -- our "Fixed" framing outruns their frozen text. Removing every
ingest-stamped file is verbatim what ingest-spec v1 section 5 mandates:
v1 assumes one manifest per bundle and defers the multi-manifest case as
a named extension point. So v0.4.0 implements that extension point rather
than fixing a defect against the spec. The mechanic survives ratification
either way -- stem matching follows from their own {stem}@{h} stamp
definition, since {h} changes on every content edit -- but the prose is
queued for amendment and is not ratified, so the changelog now says so
instead of claiming conformance it does not have.
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.
Both were wrong, and the convention owner corrected them: this library is not
a marketplace plugin, so there is no catalog ref to re-pin -- the integration
is a registry entry in the catalog's parity gate. And the catalog's vendored
okf-check.mjs is not byte-identical to okr's (~141 differing lines of code, an
intentional layer-1 floor), so it cannot serve as a conformance oracle. Parity
is behavioural over the spec section 3 floor in default read-mode, per file,
measured by that gate over an adversarial corpus.
Adds llm_ingestion_guard>=0.2,<0.3 as this library's first and only runtime
dependency, and guard_adapter.py -- the one module that imports it. The flows
themselves are unchanged: they still take an injected gate, and importing the
package still does not import the guard, so a Door A consumer is unaffected by
the dependency's state.
Door B screens the exact bytes it persists. The guard's §6 bookends
(prepare_input -> model -> screen_output) assume a model call in between; this
library makes none, and prepare_input returns prompt-shaped text (sanitized
AND spotlight-fenced with a per-call nonce) that must never reach disk. So the
adapter calls screen_output alone, on the extracted text as it stands, and
hands that same text back -- the verdict is then a statement about the bytes
actually written. This supersedes the plan's "bookends" wording, recorded
there under "Settled during implementation (step 4)".
It follows that the gate refuses rather than repairs: a file carrying an
invisible carrier is rejected, not stripped and persisted. Sanitizing first
would write a document differing invisibly from the operator's file while
source_sha256 still points at the original bytes. Operator decision; the
policy is PRESET_USER_UPLOAD, so any finding at all is held back.
Door C hands the bundle over whole to okf.import_bundle, which resolves the
cross-link graph across concepts. Per-concept reasons are derived from the
scan findings (severity:label) because stamp_concept keeps the disposition and
drops the reason strings behind it.
Assumption B1 closes as a signature smoke test over what the adapters actually
call -- screen_output and okf.import_bundle signatures, the Disposition values
both doors compare by value, the Origin/Channel vocabularies Door C validates,
the result fields read, and the upload preset's shape. prepare_input is not
pinned: drift there cannot reach this library. Behaviour is pinned against the
real scanner too, including the persist-gate proof that a fail-secure fixture
leaves the bundle byte-identical.
B2 closes with it: git+https tag install over anonymously readable HTTPS, no
credential. A direct reference is an install-time channel, not the pin -- the
range stays in pyproject, is satisfied by the tag install today, and resolves
normally once the package index exists. A packaging test enforces that the
guard remains the only runtime dependency (verified by hand-mutation).
Reads an external OKF bundle as {bundle-relative path -> document text},
hands it WHOLE to an injected gate over the guard's okf.import_bundle (a
bundle-level call: it resolves the cross-link graph across concepts), and
merges only concepts clearing the non-blocking floor. Same injection pattern
as Door B, so the core stays dependency-free while the CI channel for the
real guard is settled.
Two constraints shaped the design and are pinned by tests:
- A merged concept is written VERBATIM. Stamping provenance into it would
require round-tripping its frontmatter through this library's line-oriented
parser, which cannot represent the block lists the guard's parser accepts --
silent data loss -- and would persist bytes the guard never screened.
- Ownership is therefore proven by content identity: identical bytes at the
target name are a no-op re-merge (re-import of an unchanged bundle is
idempotent), and anything else at the name is refused. Curated content and
an updated concept are refused alike; refusing is what never destroys.
The floor is fail-closed beyond the plan's "no error" wording: an error, an
unrecognised disposition, and a concept the gate returned no verdict for are
all refusals. quarantine_review stays its own bucket, as at Door B.
origin/channel are validated against the guard's pinned vocabulary -- it
derives trust from origin by enum identity, so an unrecognised string would be
silently downgraded rather than caught.
Three primitives promoted for reuse rather than duplicated:
reduce_to_id_grammar and check_filename_length to materialize.py, and
extract.decode_text. Door C slugs the WHOLE concept path, so tables/users.md
and views/users.md stay distinct. Concept discovery folds case explicitly
rather than globbing *.md, whose case-sensitivity follows the filesystem and
would import the same bundle differently on APFS and ext4.
README's "what is gated today" section corrected: it claimed nothing is gated,
which is no longer true, but the honest statement is narrower than "the doors
are gated" -- the library cannot verify that an injected adapter is a real
guard, and a permissive stub is believed.
405 tests green; ruff, ruff format and mypy --strict clean.
Stage 0 guard-readiness check (this session) verified the real
`llm-ingestion-guard` v0.2.0 surface against ground truth: the Door C plan
assumed `okf.import_bundle(bundle, origin=…, channel=…, allow_reserved=False)`,
but the shipped signature is `import_bundle(bundle, *, origin, channel)` — no
`allow_reserved` parameter. Reserved-name rejection of `index.md`/`log.md` is
unconditional in the guard (OKFPathError, no toggle), so the intended default
semantics are unchanged; only the documented call was wrong and would have
raised TypeError if written verbatim. Correct the plan line before Phase 2
code is written against it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBbjgS5A55RVavoyjJC4FX
Sequences the remaining roadmap (Stage 0 guard gate -> Phase 2 -> Phase 3
-> Phase 4) with the quality rationale for the order: guard readiness
verified before any Phase 2 code, Phase 2 split guard-independent-first,
Phase 3's configurability extracted only after two concrete consumers
exist, Phase 4 code last on a frozen contract with its coordination
started early. Connective tissue over the four per-phase plans; owns the
order and reasons, not the phase detail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBbjgS5A55RVavoyjJC4FX
Phase 1 (Door A): ingest-spec v1 implementation + §11 golden fixtures,
pinned to the current spec revision, with TDD order and byte-exact
verification criteria. Phase 2 (Doors B/C): guard-gated inbox and
external bundle import. Phase 3: configurable bundle contract via a
profile object, default profile locked to the golden suite. Phase 4:
zero-dep Node half, coordination-first with owner sign-offs.
Each plan carries explicit key assumptions with tests and a
verification section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeqhJpYQyghASjiJo5EhGg