PLAN — ID-429 Bundle index subtasks
PLAN — ID-429 Bundle index
Section titled “PLAN — ID-429 Bundle index”Proposed subtasks for DESIGN.md. Decision cites are D1–D9; measurement
cites are §2 row N, both in that document.
Two hard external interfaces, and they block different subtasks:
| Interface | Owner | Blocks |
|---|---|---|
| Directory scheme admits unseen types (DESIGN §5, IA-1…IA-5) | id-427 | {429.7} only — the acceptance of the feature, not its mechanism |
parseBundleNav accepts directory entries + a base dir (DESIGN §6) | id-439 | {429.6} — hard co-requisite, same wave |
Byte-exact v0.2 re-vendor + okf_version: "0.2" stamp | id-426 | {429.4}, {429.9} — sequencing only, to avoid touching the same fixture and stamp line twice |
Five of the nine are free now. The mechanism of per-directory emission (D1) reads no
concept type, so it does not wait on id-427.
{429.1} — Ratify the design · free now · delivered
Section titled “{429.1} — Ratify the design · free now · delivered”DESIGN.md. Closes id-429 AC-1. Needs owner ratification of D2 (root stops listing concepts), D3 (theme strip) and D8 (root-only log) before any code moves — D3 deletes a capability, and D2 changes what a reader sees first.
Test strategy: none (document). The gate is the owner’s read, plus the Coordinator’s reconciliation of DESIGN §5 against id-427’s spec-in-flight.
{429.2} — Reserve index.md/log.md by basename at any depth · free now
Section titled “{429.2} — Reserve index.md/log.md by basename at any depth · free now”_existing_concept_paths (bundle_writer.py:1114-1120) compares the full bundle-relative
path against a set of bare basenames, so every nested index would be reported removed on
every run, forever (§2 row 4). §3.1 makes these two names reserved at any level. Fix the
reservation for index.md/log.md only; README.md/CONFORMANCE.md stay root-only,
matching bundle-graph.ts:81-84’s deliberate rule that a nested guides/README.md is a
concept.
Ships before {429.5}, and is correct on its own — it also protects a client-hand-authored
nested log.md (D8) that §9 permits today.
Test strategy (scripts/tests/test_producer_bundle_writer.py): seed a bundle dir with
certifications/index.md and case-studies/log.md on disk, run write_bundle twice with
an unchanged draft set, assert both are absent from RunSummary.removed on both runs
(the second run is the one that catches a naive fix); assert a nested guides/README.md
is still counted as a concept; assert the root four (ontology.json, README.md,
CONFORMANCE.md, context.jsonld) keep their existing root-only behaviour.
{429.3} — Retire the theme machinery · free now
Section titled “{429.3} — Retire the theme machinery · free now”Delete build_index_themes and the unthemed_heading fallback; drop theme_config from
write_bundle (bundle_writer.py:1149) and from the flow_def run entry (:414, pass
site :555); rename IndexTheme → IndexSection and keep it as the renderer’s structural
node (D3). Delete the phantom index-themes.json convention from the docstrings (§2 row 3)
rather than leaving a named config file that has never existed.
Test strategy: prove the retirement at the callers, not by grep — the flow_def run
entry and write_bundle no longer accept the keyword (a call passing it must fail, which
is what stops a future caller quietly re-supplying it), and pyright/mypy over the
producer package is clean. Rewrite the five index tests (:297-393) against IndexSection.
Grep the specs/id-132-* family and lib/okf/.claude/skills/okf/SKILL.md for BI-5’s
“~17 themes” language and route what is found to {429.9} — do not treat grep absence as
proof of no consumer (§2 row 3 is the measurement that carries this subtask).
{429.4} — Bullet form: ASCII separator · free now, sequence with id-426
Section titled “{429.4} — Bullet form: ASCII separator · free now, sequence with id-426”— → - at bundle_writer.py:424, matching the upstream shown form (§2 row 7). Add D6’s
defensive rule: an empty description emits no trailing separator. Closes AC-3.
No consumer change — CONCEPT_BULLET_RE already accepts both glyphs (§2 row 6), which
corrects the premise that parse-index.ts must move in step. The only artefact that moves
is the byte-exact round-trip fixture __tests__/fixtures/okf/bundle-writer-index.md.
Sequence with id-426, which rewrites the adjacent okf_version stamp line in the same
function and the same fixture; landing them separately regenerates the byte-exact fixture
twice and burns a review cycle on a hash diff.
Test strategy: assert the character, not the shape — a Python assertion that the
rendered bullet contains ") - " and no — anywhere in the output (a
“has a separator” assertion passes on the bug). Regenerate the fixture from the writer, and
keep the existing Vitest round-trip (__tests__/lib/okf/parse-index.test.ts:48) asserting
full structure with no fallback. Confirm .prettierignore:65-70 still covers the
fixture path after any move.
Cross-task note for id-426: the vendored SPEC renders §8’s marker as - where upstream
is * (§2 row 7) — prettier drift in the vendored copy, not an upstream difference. If the
re-vendor is not byte-exact, this AC gets checked against a formatted copy and reads as
already-satisfied.
{429.5} — Emit one index per directory · free now (needs {429.2}, {429.3})
Section titled “{429.5} — Emit one index per directory · free now (needs {429.2}, {429.3})”The core (D1, D7). Group written by directory; declare <dir>/index.md at every level
from root to leaf, in the same declare_file pass, before append_log_entry. The renderer
takes the okf_version stamp as a parameter so a nested index cannot acquire
frontmatter (AC-5 becomes structural, not conventional). Closes AC-2 for the
subdirectory half; the root half is {429.6}.
Test strategy — one assertion per decided rule, over a synthetic multi-level draft set
(root concept + topics/ + case-studies/ + case-studies/won-bid/):
- an index exists at every level, including an intermediate directory holding only a child directory (D7);
- the root index carries
okf_versionfrontmatter and every nested index carries none; - counts are immediate, not recursive (
case-studies/reports its own concepts and1 subdirectory, notwon-bid/’s contents) — the assertion that catches the natural recursive-walk implementation; - entries are ASCII-ordered and two consecutive runs over an unchanged draft set produce byte-identical index files (D6 determinism; a client-repo git diff is the real failure mode);
- links are directory-relative with no leading slash (D5), and a won-bid concept appears in
case-studies/won-bid/index.md, not incase-studies/index.md(§2 row 2 — proves the physical-path keying survived); - a concept in
failed_rel_pathsis absent from its index while its file survives on disk (D7, decided consequence); - a directory whose last concept leaves stops having its index declared (assert the absence
of the
declare_filecall — the physical delete is the engine’s, not ours).
{429.6} — Root index becomes a directory listing · BLOCKED on id-439
Section titled “{429.6} — Root index becomes a directory listing · BLOCKED on id-439”D2. The root emits directory entries plus a ## Concepts section only if bundle-root
concept files exist. Closes AC-2’s root half and restates the root’s job per AC-1.
Blocking, not advisory: parseBundleNav drops non-.md targets, so a root index of
directory entries returns non-empty themes with zero concepts — which suppresses
<BundleNav>’s absent-index fallback and renders an empty nav rail (§2 row 6, DESIGN
§6.1). This must merge in the same wave as id-439’s directory-entry support.
Test strategy: producer side — with every concept in a subdirectory, the root index
contains zero .md bullets and one entry per child directory. Consumer side (id-439’s
tests, referenced here so the interface is not orphaned): parseBundleNav yields directory
entries distinguishable from concepts; <BundleNav> renders them as navigable groups;
row.path === selectedConceptId still selects for a nested index’s entries after base-dir
joining. Add one end-to-end assertion that the nav rail is non-empty for a bundle whose
root index lists only directories — the regression this subtask exists to avoid.
{429.7} — Acceptance over an open-type bundle · BLOCKED on id-427
Section titled “{429.7} — Acceptance over an open-type bundle · BLOCKED on id-427”Prove the design’s central claim — that the directory axis is total over an open type vocabulary — against id-427’s actual output, and prove DESIGN §5’s assumptions were met.
Test strategy: run the producer over a corpus containing id-427’s two measured holes
(an empty-scope_tag published q_a_pair; a source_document that produced no
q_a_pair), then assert: both residual concepts appear in an index (IA-1/IA-2 — if
they land at the bundle root, IA-2 was not met and the root index has regressed toward a
flat dump); an index exists for every directory the run minted, including type directories
that did not exist before the run; and IA-3 — a concept whose title slugifies to
index or log is refused or suffixed by the slug function, never written over a
directory’s index. IA-3 is the one that is newly reachable because id-427 opens the
vocabulary; if id-427’s spec does not carry it, raise it there rather than patching it
here.
{429.8} — Close id-362 F2 · free now
Section titled “{429.8} — Close id-362 F2 · free now”Record F2 closed against id-429 with the answer “not required” (§8 MAY; §11 MUST NOT reject), F1 left open on id-362. Ledger + task-file edit only. Closes AC-6.
Test strategy: none. Verification is that id-362’s file states F1’s remaining scope without re-opening F2, and that neither file justifies the work by conformance (id-429 status_note; DESIGN §0).
{429.9} — Doc and §-cite sweep · after id-426, {429.3}, {429.5}
Section titled “{429.9} — Doc and §-cite sweep · after id-426, {429.3}, {429.5}”bundle_writer’s module docstring and regenerate_indexes’ docstring still narrate the
~17-theme design and the S451 rider’s em-dash preference, and cite v0.1 §-numbers (§6 index,
§7 log, §11 versioning) that id-426’s re-vendor renumbers to §8/§9/§12. Sweep both with the
id-426 §-cite pass so the numbering moves once. Delete the dead
git_sync.INDEX_FILENAME (§2 row 12) or leave it deliberately — state which.
Test strategy: none beyond the existing suite. The check is a read of the two docstrings against DESIGN §§3–4: a docstring that still describes themes will be treated as the contract by the next reader, which is exactly how the theme machinery outlived its supplier.
Dependency order
Section titled “Dependency order”{429.1} ratify ├── {429.2} basename reservation ──┐ ├── {429.3} theme strip ───────────┼── {429.5} per-directory emission ──┐ ├── {429.4} separator (with id-426)┘ │ ├── {429.8} close id-362 F2 │ │ │ │ id-439 parser ──────────────────────────────────────► {429.6} root index │ id-427 directory scheme ────────────────────────────► {429.7} acceptance └────────────────────────────────────────────────────────► {429.9} sweep (after id-426)Merge waves. {429.2}+{429.3}+{429.5} is one coherent producer PR and can land alone — it adds per-subdirectory indexes with the root index unchanged, which is already the majority of the owner’s ruling and carries no consumer risk. {429.4} rides id-426’s wave. {429.6} rides id-439’s. {429.7} closes after id-427.