Measure first, widen after. The 19-fixture guard-surface suite was re-run against v0.3.4 in a scratch venv before the range moved, and reproduced the three deltas measured against v0.3.3 exactly, with none added. v0.3.4 is the tag pinned rather than v0.3.3 because it shipped first and repairs a quadratic regex (okf._MD_LINK_RE) that sits on Door C's own call path. Door C now passes allow_reserved=False explicitly. The guard added the keyword in the 0.3 line and defaults it True for received bundles, which would merge a sender's index.md / log.md instead of rejecting them. The override keeps the unconditional reserved-name refusal committed to before the keyword existed, and the reason is structural rather than a second opinion on the guard's scan: Door C generates the merged bundle's index.md from what it merged and writes every merged concept verbatim, so a sender's index.md would be a second and irreconcilable claim on one path. This is not a behaviour change for anyone on the previous pin: under v0.2.0 the keyword did not exist and reserved names were refused by construction. The floor is >=0.3 and not >=0.2 for a measured reason. allow_reserved is absent in v0.2.0 and present from v0.3.0 onward, checked across all five tags: a >=0.2 floor would admit a version that raises TypeError on every Door C import. That measurement also corrects a recorded premise -- the plan said the keyword "shipped in v0.3.3", which read the first version we ran the suite against as the version it was introduced in. The conclusion held; the reason did not, and the reason is what a future bump would have relied on. test_door_c_pins_allow_reserved_false_against_the_guards_default locks both halves: that the guard still defaults True, without which the override is a no-op that would pass forever over nothing, and that Door C overrides it. 586 tests, mypy --strict clean, goldens byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V2v1hrDhrff2H3y2TNJHkF
16 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Changed
-
The guard pin moved to
>=0.3,<0.4, resolved againstv0.3.4. The window is widened only after measurement, never before: the 19-fixture guard-surface suite was run againstv0.3.4in a scratch venv first, and reproduced exactly the three deltas measured againstv0.3.3— no new ones.v0.3.4's own fixes are regex-complexity repairs, one of them (okf._MD_LINK_RE) on Door C's call path, with no disposition changes. -
Door C now passes
allow_reserved=Falsetookf.import_bundle. The guard added the keyword in the0.3.xline and defaults itTruefor the received-bundle path, which would merge a sender'sindex.md/log.mdinstead of rejecting them. Door C overrides it, keeping the unconditional reserved-name refusal committed to before the keyword existed. The reason is structural rather than a second opinion on the guard's scan: Door C generates the merged bundle'sindex.mdfrom what it merged and writes every merged concept verbatim, so a sender'sindex.mdwould be a second, irreconcilable claim on one path.This is not a behaviour change for anyone on the previous pin. Under
v0.2.0the keyword did not exist and reserved names were refused by construction; the explicit argument preserves that outcome across the bump. A consumer sees the same rejections, with the same reasons, before and after.
0.5.0a2 — 2026-07-31
This is the pre-release the pilots pin. v0.5.0a1 was tagged and abandoned
unused — do not pin it. It carried a generated.by actor that the spec owner
had already excluded, and it was caught before any pilot was notified.
Fixed
-
OKF_V0_2'sgenerated.byactor isprocess:okf-ingest, notprocess:llm-ingestion-okf. Commons decided the fixed id on this repo's own proposal 2026-07-31, superseding option (d) chosen here 2026-07-27. The exclusion isingest-spec.md:7-8, frozen on the spec being framework-neutral: normalising our repo name into the id would force every other conformant implementation to write it into its own output.Nothing in the wild carried the excluded value —
OKF_V0_2did not exist atv0.4.0, and no pilot had been notified — so this costs a tag rather than a migration. It is recorded rather than quietly folded in because the failure it avoids is specific:actoris both the stamp written and the value owned back (OwnershipPolicy), and recognition is one-way, so a pilot holding bundles stamped with the excluded id would have hitcollision_unstampedon its own files the moment the id was corrected.
0.5.0a1 — 2026-07-31 — ABANDONED, do not pin
Pre-release. PROVISIONAL surface. Shipped to a named pilot set —
portfolio-optimiser-claude, the plugin marketplace catalog, and
claude-code-llm-wiki — so that real data can find what fixtures cannot. The
v0.2 surface may change on their feedback without a deprecation cycle.
Saying so up front is what buys the freedom to act on the feedback; discovering
it later is what would make a pilot a de-facto release. Do not pin this tag
outside the pilot set.
Support for a new upstream version is additive — a new profile, never a
migration. OKF_LATEST still points at DEFAULT; flipping it is the GA
event, not a side effect of this tag.
Added
OKF_V0_2profile. Google OKF v0.2 as a profile alongsideDEFAULTandSTRICT_V1. It closes nothing: §14 forbids a conformant consumer to reject on an unknowntypevalue or on unknown additional keys, sotypeis the only required key. It NAMESokf_versionbut never carries its value — that value tracks the upstream Google version and belongs to catalog (decision E1), so the caller supplies it viamaterialize_bundle(..., root_frontmatter_values=…)and the profile fixes only the key and its position.root_frontmatter_valuesonmaterialize_bundle, keyword-only: the mechanism behind "a profile names a key, a caller owns its value".sourcesemitted as an inline flow sequence, populated from the manifest.- The v0.2 golden fixture, compared byte-for-byte like the others.
Changed
profileonmaterialize_bundleis keyword-only, so existing positional call sites stay source-compatible._is_ingest_ownedis profile-aware. Ownership is a policy on the profile, not a literal; recognition stays one-way.
Fixed
root_frontmatterpermitted a key without requiring it — the two had been the same profile field. Upstream says MAY where the field said MUST, so a root index that legally omitted an optional key was rejected.root_frontmatternow permits and orders; the newroot_frontmatter_requiredrequires.STRICT_V1keeps requiring its three (unchanged for its consumer),OKF_V0_2requires none,DEFAULTis untouched. Measured against the eight root indexes available locally: 7 of 8 failed before, 0 of 8 after.
Notes
DEFAULTandSTRICT_V1are byte-stable across this release. The emit path is byte-identical; the golden suite is the check.- Known, deliberately unfixed:
TypePolicy's closed branch does not strip quotes from a declared type. All three call sites passDEFAULT.types, and bothDEFAULTandOKF_V0_2setallowed=None, so the branch is unreachable in shipped code — it fires only for a caller constructingSTRICT_V1and callingrejectiondirectly. Fixing it would repair a write path that never sees quotes, and would fix the meaning of a quote character without a value model that can express one, in a line-oriented format.
0.4.0 — 2026-07-25
Phase 2. The bundle inbox (Door B) and external-bundle import (Door C) ship, and with them this library's first — and only ever — runtime dependency.
Added
- Door B — bundle inbox (
process_inbox). Per dropped file: bytes → extraction → persist gate → render → collision gate → write → index link. Returns anInboxResultwhose four buckets (persisted,quarantined,rejected,failed) are disjoint and complete, so a file that vanished mid-run surfaces as a missing entry rather than as nothing. One bad file never aborts the run: only an invalidingested_at, a reservedokf_type, and a missing inbox directory fail the whole run, each being wrong for every file at once. Concept files are namedinbox-{slug}.md, disjoint fromindex.md, Door A'singest-*, andpromoted-verdict-*;source_sha256is taken over the original dropped bytes, so provenance stays re-verifiable against the operator's file. - Door C — external bundle import (
import_bundle). Reads an external OKF bundle and hands it whole to an injected gate over the guard'sokf.import_bundle— a bundle-level call, because it resolves the cross-link graph across concepts — merging only concepts that clear the non-blocking floor. Two invariants, both load-bearing: a merged concept is written verbatim (stamping it would require round-tripping frontmatter through this library's line-oriented parser, which cannot represent the block lists the guard's parser accepts, and would persist bytes the guard never screened), and ownership is therefore proven by content identity — an occupied target name is re-used only when the bytes there are already identical, never overwritten. Re-importing an unchanged bundle is a no-op. extract_text— the Door B extraction registry. All file-type → text extraction lives in this library, because the guard is text-only.md/txtpass through,csvrenders the markdown table,jsonis fenced verbatim, andhtml/htmreduce to text withhtml.parser— stdlib throughout.pdf,docx, andxlsxare gated behind the[extract]extra, which ships no parser yet: those types fail fast with a typed error naming the extra, never a silent skip.llm_ingestion_okf.guard_adapter— the shipped gate over the real guard (inbox_gate,import_gate), and the only module here that imports it. Door B screens the exact bytes it persists: the guard'sprepare_inputbookend prepares text for a model call this library never makes, soscreen_outputalone is used and the screened string is the written string. It follows that the gate refuses rather than repairs — a file carrying an invisible carrier is rejected, not stripped and persisted. The policy isPRESET_USER_UPLOAD, so any finding at all is held back.- 15 new stable error codes, each registered in the
errors.pydocstrings (the stability contract) and pinned by the error-code conformance suite:extractor_decode_error,extractor_empty_csv,extractor_extra_missing,extractor_unknown,import_label_invalid,import_path_empty,import_path_too_long,import_provenance_invalid,import_slug_collision,inbox_slug_collision,inbox_slug_empty,inbox_slug_too_long,inbox_source_file_invalid,inbox_title_invalid,okf_type_reserved.
Changed
llm-ingestion-guard>=0.2,<0.3is now a mandatory runtime dependency. Installing this package installs the guard. Importing it does not: onlyguard_adapterimports the guard, so a Door A consumer keeps working whatever state the dependency is in, and the doors themselves still take an injected gate. Until the guard is published to a package index, install it from its tag — see the README. A packaging test enforces that this stays the only runtime dependency.- An extraction
titlecontaining[or]is rejected at manifest load (ingest-spec §4, ratified D1). Previously only single-line was validated. A manifest that loaded before and carries a bracket in a title now fails fast with codemanifest_schema: the title renders verbatim into the index link label- [title](target), where a bracket breaks index-link and navigation parsing downstream.
Fixed
- Materializing one manifest no longer deletes the files another manifest stamped into the same bundle. The §3 ownership scan classified every ingest-stamped file as replaceable, so a second manifest sharing a bundle removed the first one's concept files and their index links. Ownership is now narrowed to files whose stamp names the running manifest by stem — the stem is stable across content edits, so an edited manifest still reclaims what a prior run of itself wrote. The operator-copy restriction (a generated file copied into curated content) remains documented, not enforced.
Notes
- Phase 2's binary extraction is not in this release: the
[extract]extra is declared but empty, andpdf/docx/xlsxtherefore fail fast. That is the one outstanding item from the phase, and it ships separately. - The ownership change above is an extension point, not a spec fix. Filed
under "Fixed" because it stops silent data loss, but the spec owner
(
portfolio-optimiser-commons) has since pointed out that removing every ingest-stamped file is verbatim what ingest-spec v1 §5 mandates: v1 assumes one manifest per bundle and defers multiple manifests feeding one bundle as a named extension point. So this release implements that extension point and outruns the frozen text rather than conforming to it. The mechanic itself is not at risk — matching by manifest stem follows from the spec's own{stem}@{h}stamp definition, since{h}changes on every content edit — but the surrounding prose is queued for amendment in commons and is not ratified. Treat multi-manifest bundles as ahead of the spec until it is. - Exception
__cause__preservation is now pinned by a conformance suite, one test per fail-fast wrap site, alongside the existing.codesuite.
0.3.2 — 2026-07-23
Fixed
- Frontmatter and index-label values are emitted verbatim; only
source_queryis whitespace-collapsed. Earlier releases collapsed every whitespace run in every frontmatter value and index link label to a single space. §5 ofingest-spec.mdmandates that collapse forsource_queryalone — where a multi-line SQLSELECTmust render on one line — while every other value is validated single-line at manifest load and passed through unchanged: validation, not repair. Atitlecarrying an internal whitespace run now survives byte-for-byte at both thetitlefrontmatter and the index link label, instead of being silently altered. Output bytes change only for values that contained a collapsible whitespace run; the shipped golden fixtures and both consumers are unaffected.
0.3.1 — 2026-07-19
Fixed
-
Documentation corrected a security claim that did not hold. The module docstring and README stated that this library "calls the guard at every persist gate". That described the intended end state in the present tense. Door A — the only door shipped — is ungated: the package has zero runtime dependencies and calls no guard function before writing to disk. Both places now say so plainly, and state that gating external or untrusted content is the caller's responsibility (
okf.import_bundle, orprepare_input/screen_output) until the persist gates land with Doors B and C.No behavior changed in this release. The correction is published because a consumer read the earlier wording as safe-by-default and would have persisted ungated content on that basis.
0.3.0 — 2026-07-17
Added
- Stable machine-readable error codes.
IngestErrorgained a keyword-onlycodeattribute (default"unspecified"). Roughly 24 codes are documented in theerrors.pydocstrings, and those docstrings are the stability contract. Consumers should assert onexc.value.code, not on message text.
Changed
- Exception message text is explicitly declared unstable. It may change in any
release. Tests matching on message strings (
pytest.raises(match=...)) should migrate to code comparisons.
Notes
- Generic schema shape violations deliberately share the single code
manifest_schema. One code per validation rule would have frozen an unnecessarily large surface. Finer resolution is a separate decision, not an assumed requirement.
0.2.0 — 2026-07-17
Added
- PEP 561 support. The
py.typedmarker ships with the package, so consumers get the inline type hints without a mypy override. IngestResult.stampexposes the spec §5 provenance stamp on the result object.
0.1.0 — 2026-07-16
Phase 1 (Door A) implemented against the normative ingest-spec.md owned by
portfolio-optimiser-commons. Never tagged; consumers pinned the commit
dae0bd1a directly.
Added
- Fail-fast manifest validation (spec §3–§4).
- Spec §5 body renderers as pure functions.
- The
fileconnector (CSV, fail-closed path boundary), thesqlconnector (read-only sqlite, env-resolved credentials), and thehttpconnector behind an explicit per-run network opt-in. - Spec §5 materialization with an in-memory staging collision gate.
- Index maintenance on re-materialization (spec §6).
- The spec §11 golden fixtures, compared byte-for-byte.
- The Door A public surface:
materialize_bundleplus the typed error hierarchy rooted inIngestError.