7.9 KiB
Door B concept granularity: one document becomes many concepts
Record of what was decided and why, written after the work landed. The repo's
pattern: cf65635 after structure derivation, d35bcb2 after Door C.
The defect this closes
process_inbox emitted exactly one flat concept per dropped file. OKF v0.2 §2
defines a concept as "a single unit of knowledge within a bundle" and a concept
ID as "the path of the concept's file within the bundle" — neither ties a
concept to a source file, and Appendix A presents v0.1 → v0.2 as a
de-monolithization. Door B therefore implemented the shape the SPEC names as
the one being migrated away from.
No conformance test caught it, and none could. §11 checks that every
non-reserved .md has parsable frontmatter with a non-empty type, so a
bundle of one giant concept is fully conformant. Conformance is the floor,
not the proof. The measurement that found it was a reading of §2's
terminology, not a count of keys.
What was built
A new profile, SEGMENTED_V1, and a caller-owned, human-adjudicated
segmentation plan. DEFAULT, STRICT_V1, STRUCTURED_V1 and OKF_V0_2 are
byte-identical; their goldens did not move, which is asserted against the
baseline commit rather than assumed.
The splitting decision is made once, written down as data, adjudicated by a human, and thereafter replayed deterministically. The run path still makes zero model calls, and a from-scratch rebuild is byte-identical to an incremental update.
| Piece | Where |
|---|---|
| Plan model, parser, cache key, slicing | segmentation.py |
SegmentationPolicy + SEGMENTED_V1 |
profiles.py |
| Root frontmatter, per-concept mirroring, 1→N, collision gate, ownership, per-directory indexes | inbox.py |
| Proposer (outside the package) | tools/okf_propose_segments.py |
| Golden | examples/ingest-golden-segmented/ |
The identity decision, and what it costs
Order …2527032751 settled the form: (c) a root-frontmatter bundle
identifier consumers join on, combined with (a) sources[].resource as an
absolute URL wherever the source has a stable one. (c) carries identity, (a)
carries provenance.
No step implemented form (a), and that is the correct outcome. It is
already emitted, at materialize.py:205,214, from the manifest locator —
Door A's surface. Door B ingests dropped files, which have no stable URL by
construction.
S4b is resolved as ONE branch. The root index is the SOURCE of bundle_id
— the caller supplies it exactly once, so D5 stays intact — and every concept
MIRRORS it. Two bundles built from identical inputs therefore hold concepts
whose paths collide by construction and whose identity values are disjoint.
The collision is the expected behaviour under form (c), not a defect.
Two identity mechanisms, deliberately. Within a bundle, Door C keys on a
content hash; across bundles, identity is this caller-assigned bundle_id. A
bundle is a collection the caller delimits, so hashing it would hand it a new
identity every time a document was added. The cost is stated rather than
hidden: a consumer meets both mechanisms.
Decisions worth their own line
- The capability is
profile.segmentation is not None, neverIndexPolicy.per_directory.STRICT_V1already setsper_directory=Truewhile Door B ignores it, so keying there would have changed a shipped profile's output and broken its byte-stability pin. - Offsets index the CANONICAL EXTRACTED TEXT, never source bytes. A
.csvis re-rendered before it becomes a concept body, and a.pdfhas no sliceable prose at all. The golden and the.csvfixture both pin this; on a.mdfixture bytes and text are identical and the assertion would pass for the wrong reason. - The adjudication cache key is the triple
(source_sha256, extractor_id, extractor_version), not the hash alone (S5b). Source bytes cannot see an extractor swap or a version bump, either of which silently invalidates every stored offset while the hash still matches. Door B observes the hash and the extractor id; the parser version is the caller's to declare, for the same reasonbundle_id's value is — the library names the key, whoever knows the value owns it. - All N segments are gated before any is written. Gating and writing one at a time would leave a half-screened document on disk the moment segment 3 of 5 quarantines. A refusal is reported once, for the document.
- Ownership asks "which concepts are owned by source_file X?", recursively, and only under the capability. Keyed on the concept filename, a round naming fewer segments orphaned the rest: they survived incrementally, were absent from a scratch rebuild, and the two diverged silently.
- A retired directory loses its index too. A scratch rebuild writes an index only where a concept lives, so an orphaned one is a one-sided difference. Found by S7 on 2026-09-01, once its fixture was made to actually retire a segment — with round 2 a superset of round 1, S7 stayed green with retirement disabled entirely.
- Frontmatter values are FLOW form, never block.
source_offset: [12, 48]. This library's parser round-trips a flow value as an opaque string and cannot read a block one at all, so emitting block would produce bundles we cannot read back. - Index ordering has one seam,
_index_sort_key. A consumer-controlled ordering is then a parameter, not a refactor. - The proposer proposes; a human adjudicates. Every entry it emits carries
PROPOSEDand the rule that produced it. Research measured the reason: the OKF reference agent's four semantic gates need a model this run path does not have, and heading derivation is inert on most of the K2 corpus — 23 of 33 PDFs carry no outline, 95 % of outline entries are CAD metadata, and the scoreable denominator is one document.
Answers to the brief's open questions
- Where does the plan live? At a caller-supplied path, parsed by the caller and passed in. The library loads nothing from a fixed location — the same division D5 makes for values.
- Is
source_offsetreadable? Not this round. It is write-only until the structured reader (D1b) lands. Nothing built here has to be torn out when it does. - Does a plan cover a whole run? No — one plan, selected by content hash. Every other dropped file in the same run keeps today's one-concept rule.
Known gaps, stated rather than left to be discovered
derivedmarks a declared title as inferred. A segmented concept'stitleis DECLARED by the adjudicator, but structure derivation runs over the segment body and addstitletoderived. The marker is therefore wrong in the one direction this repo cares about — a consumer that distrusts derived fields will distrust a fact the producer stated. The golden pins today's behaviour; the fix belongs with the code that composes the two.- A plan matching no dropped file is silent. A typo'd
source_sha256segments nothing and reports nothing, which is the silent skip this library refuses elsewhere. status: deprecatedhas no producer.statusis named as a facet key and nothing writes it, so a deprecated parent cannot carry one.- An unchanged round still rewrites.
write_byteswrites unconditionally; the bytes are identical, so no contract is broken, but "an unchanged round writes nothing" is not true today. - Six new error codes live in
tests/test_segmentation.py, nottests/test_error_codes.py. The baseline pin forbade editing the registry file. Migrate once the pin is released.
Relation to the two orders
…2527032751(identity) — executed. Form (c) built, form (a) measured as already present at Door A.…3790625395(index ordering) — untouched by design._index_sort_keyis the single named seat a consumer-controlled ordering would occupy, so that decision stays a parameter change rather than a refactor.