Skip to content

PLAN — ID-429 Bundle index subtasks

Proposed subtasks for DESIGN.md. Decision cites are D1D9; measurement cites are §2 row N, both in that document.

Two hard external interfaces, and they block different subtasks:

InterfaceOwnerBlocks
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" stampid-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 IndexThemeIndexSection 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 changeCONCEPT_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_version frontmatter and every nested index carries none;
  • counts are immediate, not recursive (case-studies/ reports its own concepts and 1 subdirectory, not won-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 in case-studies/index.md (§2 row 2 — proves the physical-path keying survived);
  • a concept in failed_rel_paths is 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_file call — 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.


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.


{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.