Repository navigation
fix(wiki): one reading of a draft section for every consumer (#589) - #590
Conversation
The Markdown renderer skipped a section with no heading while handlers/wiki_compile.py mirrored it into wiki.pages.sections under an empty key, so a file and its row described the same page differently. core/draft_curator.py called .strip() on whatever the section held, raising AttributeError on a heading that was not text, and on a Section object whose heading was empty. mcp_server/shared/wiki_sections.py states the shape once (is_text, heading_of, body_of, titled_sections) and the renderer, the mirror, the curator's required-section lookup and wiki_refine_draft's validation all read a section through it (ADR-1071). wiki_refine_draft also refuses two sections carrying the same heading, which the mirror's mapping silently deduped. Three of the new cases fail on the previous code, one with the AttributeError. Full suite: 8716 passed, 8 skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
|
ZETETIC-REVIEW: APPROVE SummaryPR #590 introduces 1. Behavior equivalence (
|
| case | old | new | same? |
|---|---|---|---|
well-formed ({"heading":"Overview","body":"text"}) |
[('Overview','text')] |
same | yes |
numeric heading {"heading":7,"body":"x"} |
[('7','x')] |
[] (dropped) |
changed |
numeric body {"heading":"Overview","body":7} |
[('Overview','7')] |
[('Overview','')] |
changed |
whitespace-only body " " |
kept as " " |
emptied to "" |
changed |
heading with padding " Overview " |
rendered unstripped | rendered stripped to "Overview" |
changed |
"", None, 0, False heading |
dropped | dropped | yes |
The 7 to dropped/emptied case is exactly the scenario the task asked me to check for. It is a real behavior change, but it is the intended one: ADR-1071's is_text requires an actual non-blank str, so a numeric heading/body, previously silently coerced via str()/or "", is now treated the same "not usable" way the compiler already treated a blank heading, matching what the curator and the mirror now do. It is covered by tests (tests_py/shared/test_wiki_sections.py::test_titled_sections_drops_what_a_page_cannot_carry, tests_py/handlers/test_wiki_compile_sections_mirror.py). For a genuinely well-formed draft (both fields already non-blank text), only the whitespace-stripping-on-render difference remains, which is a cosmetic tightening, not a functional regression. _missing_required_sections/_section_is_filled behavior for a placeholder body is unchanged. titled_sections still returns (heading, body) and _section_is_filled still applies its own length/placeholder check on body downstream (mcp_server/core/draft_curator.py:58-59, untouched by the diff).
Non-blocking: the PR's "Audit notes" state only that curator behavior for a well-formed draft is unchanged; it doesn't call out that the compiler's malformed heading/body handling changed too. Worth a one-line mention in the ADR's Consequences section for future readers, but the change is correct and tested, not blocking.
2. Other raw consumers of sections
Grepped "sections"/.sections across mcp_server/ (excluding tests):
core/wiki_staleness.py:140(harvest_page_refs_typed) andcore/wiki_symbol_extract.py:140(harvest_page_symbols) readpage.get("sections"). They operate on published pages (dict-shaped, built bywiki_compileviatitled_sections), so they now see the corrected mirror. Both also carry a defensiveelif isinstance(sections, list)fallback that reads onlybody(neverheading), so they were never exposed to the heading bug either way. Non-blocking DRY observation, not a defect.core/wiki_source_backfill.py:82(_from_body), same: reads the published page's dict-shapedsections, consistent with the mirror.core/draft_curator.py:93(evaluate_draft) reads the raw draft list and passes it into_missing_required_sections, which now goes throughtitled_sections. Confirmed viatests_py/core/test_draft_curator_sections.py.handlers/wiki_synthesize.py:217-219buildssectionsfromdraft.sections(aSectiondataclass with guaranteed-text fields at construction time), not affected.handlers/wiki_refine.py:175,226,338:wiki_get_draftechoes the raw stored draft (informational only, no rendering decision);_validate_against_contractis the one that now goes throughheading_of/body_of/titled_sections.handlers/wiki_verify.py:94,handlers/wiki_migrate.py:204build/consume dict-shapedsectionsfrom parsed Markdown or migration frontmatter, unrelated to draft-section reading.
No consumer disagrees with the corrected mirror; no raw reader was missed.
3. Layer placement and stdlib only claim
mcp_server/shared/wiki_sections.py:12-14 imports only __future__ and typing.Any. Matches docs/module-inventory.md § Dependency Rules (shared/ is Python stdlib only, reachable from core/infrastructure/handlers/server). The new inventory line (docs/module-inventory.md diff) accurately describes the module. Confirmed correct.
4. Mutation testing (fix strength)
Ran against mcp_server/shared/wiki_sections.py (restored after each, git status --short clean afterward):
- Mutant A:
titled_sectionskeeps sections whose heading isNone(drop theif heading is not None:guard). Result: 2 tests failed (test_wiki_sections.py::test_titled_sections_drops_what_a_page_cannot_carry,test_wiki_compile_sections_mirror.py::test_the_mirror_holds_only_the_sections_the_page_carries). - Mutant B:
heading_ofskips.strip()(return heading if is_text(heading) else None). Result: 1 test failed (test_wiki_sections.py::test_heading_of[padded]). - Restored via
cp /tmp/wiki_sections_orig.py mcp_server/shared/wiki_sections.py;git status --shortconfirmed clean.
Both mutants killed. Suite is mutation-strong on the changed lines.
5. Gates (all run in the worktree, .venv already synced)
UV_NO_SYNC=1 uv run ruff checkgivesAll checks passed!UV_NO_SYNC=1 uv run ruff format --checkgives1463 files already formattedUV_NO_SYNC=1 uv run python scripts/check_craftsmanship.py --base origin/maingivesCraftsmanship gate: OKUV_NO_SYNC=1 uv run python scripts/check_doc_claims.pygivesdoc claims OK (1 declared not a claim exemption(s)).venv/bin/python -m pyright mcp_server/gives0 errors, 0 warnings, 0 informationsCORTEX_CLAUDE_DIR=<throwaway> UV_NO_SYNC=1 uv run pytest -qgives8716 passed, 8 skipped, 2 warnings, 459 subtests passed in 266.17s. Matches the PR body's claim exactly.
6. Factual claims (PR body / ADR-1071 / CHANGELOG / module-inventory)
- Issue [bug] a headless or non-text section heading still reaches wiki.pages.sections and the curator #589's three points (empty-key mirror,
.strip()AttributeError, silent dedup of duplicate headings) are each addressed: verified inmcp_server/handlers/wiki_compile.py:158,mcp_server/core/draft_curator.pydiff, andmcp_server/handlers/wiki_refine.py's newseen/duplicate check plustests_py/handlers/test_wiki_refine_sections.py::test_duplicate_headings_are_refused. - "Three of those fail on the previous code, one with the
AttributeError": plausible given the diff (the curator's non-string-heading case raisesAttributeErroron.strip()in the old code; the other three new test files exercise behavior the old code didn't have). Not independently re-verified against the pre-fix commit, but consistent with the diff and not load-bearing for the verdict. - Section 4.1/4.2 "new module is 51 lines": confirmed, file is exactly 51 lines including docstring.
- CHANGELOG entry accurately describes the fix;
wiki/manifest.jsonADR-1071 entry present and correctly pathed;docs/module-inventory.mdline accurate (checked above). - "Breaking changes: none outward, duplicate heading now returns a validation error": confirmed via
_validate_against_contractdiff and its test.
Issues
Blocking
None found.
Non-blocking
- ADR-1071 / PR "Audit notes" could note the compiler's malformed heading/body handling changed (numeric input now dropped/emptied) alongside the curator note, for a future reader diffing behavior. See section 1 above.
core/wiki_staleness.py:140-147andcore/wiki_symbol_extract.py:140-147each carry their own ad hocisinstance(sections, list)body-extraction fallback, duplicating a smaller slice of whatshared/wiki_sections.pynow centralizes. Not a defect (they never touchheading), but a candidate for a futurebody_of-based simplification.
Hand-offs
None. No defect requiring engineer/architect/security-auditor escalation.
Verdict
APPROVE
…589) The review of PR #590 measured the delta case by case: the old loop coerced with str(), so a numeric heading rendered as 7 and a numeric body rendered under its heading, while titled_sections drops both. A padded heading now renders stripped and a whitespace-only body renders empty. A well-formed draft renders exactly as before. ADR-1071's consequences say so; no code or test changed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
|
ZETETIC-REVIEW: REQUEST_CHANGES SummaryPR #590 head moved to 505ac5a, a docs-only follow-up to 11a3b05 that answers my first non-blocking note from the prior review. Confirmed the diff touches only the two ADR-1071 files. The added paragraph is mostly accurate against what I measured, but one clause overstates the delta: it says both the numeric-heading and numeric-body cases are "now dropped," when only the numeric-heading case drops the whole section. The numeric-body case keeps its section (heading intact) with an emptied body, not dropped. Requesting a small wording fix before approval. Diff scope
Both files add the identical paragraph (wiki page and its docs/adr mirror stay in sync, as required). No code, test, or other doc file touched. Wording check against measured behaviorAdded paragraph: "Changed, for malformed data only: the renderer used to coerce with Re-ran the exact cases against
So four of the five clauses are accurate. "Both are now dropped" overstates the numeric-body case: it implies the section disappears from the published page the way the numeric-heading case does, but it actually stays, under its valid heading, with an empty body where the number used to render. That is a materially different reader-facing outcome (an empty section heading with no prose, versus no section at all), so it is worth the correction. Gates run (docs-only delta)
Neither gate is scoped to catch this kind of semantic inaccuracy (they check claim-exemption bookkeeping and mirror sync, not behavioral correctness of prose), which is why the manual re-check above is the one that matters here. IssuesBlocking
Non-blockingNone new. The duplicated body-only fallback reader in Hand-offsNone. VerdictREQUEST_CHANGES. Minimum change to unblock: correct the "Both are now dropped" clause in both ADR-1071 files (docs/adr mirror and wiki/adr/cortex) to state the numeric-body case keeps its section with an emptied body rather than being dropped. |
) The previous wording said a numeric heading and a numeric body are "both now dropped". Measured on this head: titled_sections([{"heading": "Overview", "body": 7}]) returns [("Overview", "")], so that section keeps its place under its valid heading and renders with an empty body, while a section whose heading is not text is dropped outright. The two outcomes are now stated separately, as the review of PR #590 required. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
|
ZETETIC-REVIEW: APPROVE SummaryPR #590 head moved to f2646ee, a second docs-only follow-up to 505ac5a that fixes the overstatement I flagged in round 2. Confirmed the diff touches only the two ADR-1071 files, and every clause in the reworded paragraph now matches my own measurement. No further changes required. Diff scope
Both files replace the same one sentence, wiki page and its docs/adr mirror stay in sync. No code, test, or other doc file touched. Wording check against measured behaviorNew paragraph: "Changed, for malformed data only: the renderer used to coerce with Re-ran all five cases against
All five clauses hold. No remaining overstatement. Gates run (docs-only delta)
IssuesBlockingNone. Non-blockingNone new. The duplicated body-only fallback reader in Hand-offsNone. VerdictAPPROVE |
Summary
A draft's sections were read in four places, each spelling out the shape for itself and disagreeing:
core/draft_compiler.pyskipped a section whose heading was falsy, so the published Markdown never carried an untitled section.handlers/wiki_compile.pybuilt thewiki.pagesmirror as{heading: body}with no guard, so the same section landed in the row under an empty key. A file and its mirror described the same page differently.core/draft_curator.py::_missing_required_sectionscalled.strip()on whatever it found, raisingAttributeErrorinsideevaluate_draftfor a heading that is not text, and on aSectionobject whose heading is empty.handlers/wiki_refine.pyhad its own_is_textsince ADR-1070, the only one of the four that was right, and ADR-1070 only closed the client entry point: a draft written bywiki_synthesizeor before that change still reaches the other three.mcp_server/shared/wiki_sections.pynow states the shape once (is_text,heading_of,body_of,titled_sections) and all four read a section through it (ADR-1071).wiki_refine_draftalso refuses two sections carrying the same heading, which the mirror's mapping silently deduped, the second body replacing the first.Closes #589
Type of change
Test plan
tests_py/shared/test_wiki_sections.py: the four functions over dicts,Sectionobjects, missing fields, blank and non-text values.tests_py/handlers/test_wiki_compile_sections_mirror.py: compiles a draft holding a blank heading, a missing heading and a numeric heading on SQLite, then assertswiki.pages.sectionsholds only the titled section and the published Markdown omits the orphan prose.tests_py/core/test_draft_curator_sections.py: four malformed shapes throughevaluate_draft, reported as a missing required section instead of raising, plus a well-formed section still approving.test_duplicate_headings_are_refusedintests_py/handlers/test_wiki_refine_sections.py.AttributeErrorthis fixes.ruff check,ruff format --check: clean.pyright mcp_server/: 0 errors.scripts/check_craftsmanship.py --base origin/main: OK.scripts/check_doc_claims.py: OK.Audit notes
shared/imports the standard library only, so core, handlers and infrastructure can all read a section the same way (docs/module-inventory.md§ Dependency Rules, where the new module is catalogued).Coding-standards compliance
Breaking changes
None outward. A
wiki_refine_draftcall submitting two sections with the same heading now returns a validation error rather than silently keeping the last body.Reviewer checklist
docs/module-inventory.md).🤖 Generated with Claude Code
https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH