Skip to content

feat: add MD→HTML context-tree migration script (Python, one-shot) - #700

Closed
RyanNg1403 wants to merge 11 commits into
proj/byterover-tool-modefrom
feat/ENG-2834
Closed

feat: add MD→HTML context-tree migration script (Python, one-shot)#700
RyanNg1403 wants to merge 11 commits into
proj/byterover-tool-modefrom
feat/ENG-2834

Conversation

@RyanNg1403

@RyanNg1403 RyanNg1403 commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds scripts/migrate-context-tree-py/migrate_context_tree.py — a one-shot Python migrator that converts a project's .brv/context-tree/ from the legacy Markdown-with-YAML-frontmatter format to the bv-* HTML format this branch's curate flow writes today. Offline, no daemon, three flags (--project-root, --dry-run, --rollback), PyYAML as sole runtime dep.
  • The migrator emits only the closed bv-* vocabulary defined in src/server/infra/render/elements/registry.ts. Orphan markdown content (non-canonical ## X sections like ## Overview / ## Evidence / ## Architecture / ## Patterns) is routed to the nearest semantically-fitting bv-* target via a heading-name heuristic; canonical content always wins and unmapped headings surface as dropped-orphan-section warnings. The rationale: the HTML reader (src/server/infra/render/reader/html-reader.ts:77) and LLM renderer (html-renderer.ts:155) silently skip non-bv-* tags, so any preservation in <p data-md-section="…"> or <section> would be dead content from brv's pipeline perspective.
  • Covers the failure modes surfaced while testing against real legacy trees: empty/missing frontmatter title: falls back to body # H1, lede paragraph (between H1 and first ##) hoists into <bv-topic summary>, "Rule N:" prefix splitter (line-start or after sentence-end), plural **Subsection:** tolerance under ## Raw Concept, mixed bullet style (-/*/+/1.) in ## Facts and Changes/Files lists with indented continuation preservation, fenced code blocks anywhere in body promoted to <bv-diagram> with language-driven type, multi-dot filename support (e.g. node.js.mdnode.js.html), rollback that preserves pre-existing .html siblings via an archive-root manifest, --rollback --dry-run preview + --yes confirmation gate, and per-topic warnings for malformed YAML / unknown frontmatter keys / type-mismatched frontmatter values / YAML ' #' truncation hazards.

Test plan

  • Pyflakes clean, syntax compiles
  • Golden-baseline diff over 246 HTML files (220-file real production archive snapshot + 26 LLM-generated / synthetic fixtures): 0 canonical bv-* elements lost, 0 files disappeared, every changed file matches an expected output pattern
  • Live validation across two LLM providers (Gemini 3 Flash Preview + OpenAI gpt-5.4-mini) on freshly-generated context trees + a developer's live tree snapshot: 0 migration failures, 0 non-bv-* tag emissions in any output HTML
  • Rollback round-trip verified across all test workspaces (restored count matches archived count, generated .html removed, pre-existing .html siblings preserved per the archive manifest, archive folder removed)
  • End-to-end fixtures: multi-dot filename (node.js.md) produces matching node.js.html + <bv-topic path="node.js">; preserve-list test (pre-existing foo.html alongside foo.md survives migrate → rollback unchanged)
  • Reviewer to spot-check the warning surface (malformed-frontmatter, dropped-frontmatter-key:<key>, frontmatter-type-mismatch:<key>, yaml-comment-truncation:<key>, dropped-orphan-section:<name>, dropped-raw-concept-subsection:<label>, dropped-narrative-subsection:<label>, dropped-snippets, missing-timestamps) — these are the operator's only signal for what was lost or guessed

RyanNg1403 and others added 4 commits May 14, 2026 14:40
Standalone Python migrator under scripts/migrate-context-tree-py/.
Walks .brv/context-tree/, converts every .md topic to a single
<bv-topic> HTML document matching the format curate writes on
proj/html-mem-conversion, and archives originals to
.brv/_migrations/context-tree-md-<YYYY-MM-DD>/ for full reversibility
via --rollback. _archived/ subtree is skipped (no <bv-archive-stub>
in the vocabulary). After one run the live tree contains zero .md
outside _archived/.
Refactor YAML loading to use FrontmatterLoader and enhance frontmatter parsing.
Summary of changes to migrate_context_tree.py:

Refactor: drop non-vocabulary <p data-md-section> emission. The brv HTML
reader (html-reader.ts:77) and LLM renderer (html-renderer.ts:155) both
silently skip non-bv-* tags, making preservation in <p>/<section> dead
content. Output now uses ONLY the closed bv-* vocabulary; orphan content
is mapped to existing bv-* targets via a heading-name heuristic, or
dropped with a per-file warning so the operator sees what was lost.

Edge-case fixes:
- H1 body title falls back into <bv-topic title> when frontmatter title
  is empty/missing
- Body prose between H1 and first ## hoists into <bv-topic summary> when
  summary attr is empty
- Orphan section heuristic: ## Overview/Purpose -> <bv-reason>,
  ## Architecture/Structure/Scope -> <bv-structure>, ## Evidence -> bv-fact
  bullets, ## Rules -> split bv-rule siblings, ## Patterns -> bv-pattern,
  ## Decisions -> bv-decision, ## Abstract/Summary -> bv-topic summary attr
- Unknown frontmatter keys emit dropped-frontmatter-key warnings;
  runtime-signal keys (importance, recency, maturity, accessCount,
  updateCount) are allow-listed and dropped silently per spec
- Multi-rule blocks with 'Rule N:' prefix split correctly (line-start and
  after sentence-end + whitespace)
- Plural raw-concept labels tolerated (Tasks, Flows, Changes, Files,
  Patterns, Timestamps, Authors); unknown labels warned
- Unknown ### subsections under ## Narrative routed via heuristic:
  ### Patterns -> bv-pattern siblings, ### Decisions -> bv-decision,
  ### Overview -> bv-structure
- ## Facts / ## Raw Concept Changes/Files lists accept any bullet style
  (-, *, +, 1.) — previously only - was matched
- All fenced code blocks anywhere in body promoted to <bv-diagram>;
  language tag drives type (mermaid/plantuml/ascii/dot/graphviz, else other)
- YAML scalar values containing ' #' emit truncation-hazard warning
- _index.md descriptors with only ## Purpose / ## Scope now produce
  populated <bv-topic> instead of empty stubs

Validation: zero regressions on 246-file golden baseline (real 220-file
production archive + 26 synthetic and LLM-generated fixtures); zero
canonical bv-* elements lost. Live validation across two LLM providers
(Gemini 3 Flash Preview + OpenAI gpt-5.4-mini) confirmed 0 failures and
0 non-bv-* tag emissions on freshly-generated corpora plus a snapshot
of the developer's live context tree.
@RyanNg1403
RyanNg1403 requested a review from DatPham-6996 as a code owner May 25, 2026 02:45
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 7m 14s —— View job


Claude Code PR Review — #700

Overall score: 4 / 5 — comprehensive, well-documented, and well-tested. One real correctness bug (multi-dot filenames) and a handful of correctness/UX nits worth a pass before merge.

Todo

  • Read changed files (migrate_context_tree.py, .gitignore, requirements.txt)
  • Cross-reference HTML reader/renderer to confirm bv-* vocabulary claims
  • Audit edge-case fixes for correctness and regressions
  • Audit orphan-section routing heuristic
  • Check security/safety (path traversal, archive handling, YAML loading)
  • Post inline comments + summary review

Summary

The PR delivers exactly what the description promises: orphan section content that would otherwise be silently dropped by html-reader.ts/html-renderer.ts (which skip non-bv-* tags) gets routed into the closed bv-* vocabulary via a heading-name heuristic, with conflict resolution favoring canonical content and per-file warnings for unrecoverable drops. The vocabulary set emitted by the migrator matches src/server/infra/render/elements/ exactly, the topic-path safety check matches the TS writer's invariants, and the timestamp / RFC3339 format matches the writer's output.

The codebase is unusually well-commented for a script — case numbering tying back to the ticket, schema enums mirrored from TS as Python constants, explicit conflict-resolution policies in _process_orphan_sections. That's a clear net positive for future maintainers.

Significant findings (one per inline)

Severity Finding
issue (bug) Path.with_suffix(".html") misuse breaks any topic whose filename has two or more dots (e.g. node.js.md → writes to node.html, mismatching the <bv-topic path> attribute). Three sites: _html_sibling_exists, _process_file, rollback. The 246-file baseline likely had no such names.
issue (correctness) Malformed YAML frontmatter is silently dropped — no warning surfaced to the operator despite frontmatter being a primary source of title / summary / tags.
issue (correctness) _RULE_PREFIX_LINE split treats prose appearing before the first Rule N: as a leading rule entry. The intro paragraph becomes <bv-rule> #1.
suggestion (correctness) _SECTION_REGEX / _parse_section don't respect fenced code blocks — a ## … line inside \``bashterminates the current section.~~~` fences also unsupported.
issue (correctness) _FENCED_BLOCK_REGEX title group is not line-start anchored — **bold** anywhere on a line preceding a fence becomes the diagram title.
suggestion (safety) --rollback has no --dry-run mode and no confirmation prompt, despite being destructive and adjacent to --dry-run in argparse. One slip = trashed archive + clobbered .html.
nit (forward-looking) The emitted <bv-topic> carries createdat= / updatedat= — the schema rejects these as reserved on LLM input, so re-curating a migrated topic risks attribute-validation errors when the LLM round-trips them.
nit (minor) _append_extra_rules / _append_narrative's rule emission don't share a seen_ids set across canonical + orphan-routed rules — same-text rules can produce duplicate bv-rule id="..." siblings.
nit (edge case) Adjacent ## Narrative followed by another H2 with no intervening body breaks the Narrative regex's lookahead.
nit (naming collision) Files like architecture.overview.md with no architecture.md sibling get classified derived and archived even when they're real topics.
nit (silent coercion) Frontmatter values of the wrong type (e.g. title: 42) are silently coerced to defaults without a warning.

Things I checked and was happy with

  • escape_html_text ordering (& first) is correct.
  • rel_path_to_topic_path correctly rejects .. / . segments — matches validateHtmlTopic's safety check in TS.
  • Archive root uses YYYY-MM-DD per day; re-running the same day is safe because the live tree has no .md files after a successful pass.
  • FrontmatterLoader extends SafeLoader (not the unsafe Loader) and additionally disables the timestamp implicit resolver so dates stay as strings. Good.
  • _write_atomic uses tmp + rename and forces LF line endings — matches TS writer's reproducibility goal.
  • The PR's claim that html-reader.ts:77 and html-renderer.ts:155 silently skip non-bv-* content is correct (verified in both files); the closed-vocabulary refactor genuinely fixes a class of data loss.
    · branch feat/ENG-2834

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5acb808d44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
P1 (correctness bugs):
- Path.with_suffix(".html") misuse breaks multi-dot filenames
  (e.g. node.js.md). New _html_sibling_path() helper uses string
  concatenation so the topic path attribute and on-disk filename
  agree for all topic names. Fixes 3 sites: _html_sibling_exists,
  _process_file, rollback.
- Rollback unconditionally deleted .html siblings for every
  restored .md — destructive on mixed trees that already had .html
  files before migration. Migration now writes a manifest of pre-
  existing siblings (_pre_existing_html_siblings.json) under the
  archive root; rollback reads it and skips deletion for entries
  listed there.

P2 (correctness):
- Malformed YAML frontmatter is now surfaced as a
  malformed-frontmatter warning instead of silently dropping
  title/summary/tags to defaults.
- _RULE_PREFIX_LINE split was capturing pre-prefix prose as a
  leading rule entry (the intro paragraph became <bv-rule> #1).
  Drop parts[0] explicitly.
- _SECTION_REGEX / _parse_section now mask fenced code blocks
  before matching so a literal `## ...` line inside ```bash …
  ``` doesn't terminate the enclosing section. Same masking via
  new _mask_fenced_blocks helper.
- _FENCED_BLOCK_REGEX title group line-anchored — a `**bold**`
  mid-sentence before a fence no longer becomes a spurious
  diagram title.
- Rules splitter and bullet-list parsers now use
  _collect_bullet_items_with_continuations so indented
  continuation lines on multi-line list items are preserved
  rather than silently discarded.

P3 (UX / minor):
- --rollback honors --dry-run (preview restore/delete counts) and
  requires --yes (or stdin TTY confirmation) before executing the
  destructive action.
- _classify_entry only marks <name>.abstract.md / <name>.overview.md
  as derived when the base <name>.md sibling exists. Empty sidecars
  without a base archive as "empty-sidecar"; non-empty orphans are
  migrated as real topics. Avoids silently archiving user-authored
  topics whose filenames happen to end in those suffixes.
- bv-rule id dedup now shared across canonical + orphan-routed
  rules via a topic-scoped seen-ids set; new _uniquify_id helper.
- _parse_narrative and _diagrams_section_span use (?ms) anchoring
  so back-to-back `## A\n## B` (no blank line) terminates the
  Narrative block correctly.
- Frontmatter values of the wrong type (title: 42, tags: not-a-list)
  emit frontmatter-type-mismatch warnings instead of silent
  coercion via the new _opt_str_typed / _str_list_typed helpers.
- Inline comment near createdat=/updatedat= emission documents the
  bv-topic schema's RESERVED_TOPIC_ATTRIBUTES hazard for downstream
  curate prompt builders.

Validation: zero regressions on 246-file golden baseline (zero
canonical bv-* elements lost on any previously-migrated file). 37
newly-migrated topics from the case-12 sidecar promotion. Live-val
across 6 workspaces holds at 79 migrated / 0 failed. End-to-end
preserve-list test confirms rollback no longer deletes pre-existing
HTML siblings; end-to-end multi-dot test confirms node.js.md
migrates to node.js.html with matching <bv-topic path>.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 7m 29s —— View job


Claude Code PR Review — #700 (follow-up)

Overall score: 4 / 5 — the follow-up commit cleanly addresses every P1/P2/P3 finding from the previous round, plus the two Codex findings. One residual bug from the same class as a previous P2 (fence-mask incompleteness), one data-safety concern around manifest write timing, and a few low-priority nits.

Todo

  • Pull latest branch state
  • Verify the multi-dot filename fix (P1 from prior round) — clean, via _html_sibling_path() (comment)
  • Verify malformed-frontmatter warning surface — clean, 4-tuple return
  • Verify _RULE_PREFIX_LINE lede-paragraph fix — clean, parts[1:] drops the pre-prefix chunk
  • Verify fenced-code-block masking — partial; _parse_narrative and _diagrams_section_span still operate on unmasked body (comment)
  • Verify _FENCED_BLOCK_REGEX line-start anchor — good for mid-line bold; still over-associates with blank line between **bold** and fence (comment)
  • Verify rollback safety (--dry-run / --yes / preserve manifest) — functionally clean; manifest-write timing is a data-safety concern (comment)
  • Verify rule-bullet continuation handling — clean, _collect_bullet_items_with_continuations
  • Verify createdat/updatedat hazard — documented inline as out-of-scope (curate prompt builder's job)
  • Verify shared seen_ids across rule emission paths — clean, _uniquify_id + rule_id_registry
  • Verify ## Narrative back-to-back H2 lookahead — clean, multiline anchor
  • Verify .overview.md / .abstract.md sibling-existence check — clean for non-empty; empty-file branch is inconsistent (comment)
  • Verify _opt_str type-mismatch warning — clean, _opt_str_typed / _str_list_typed
  • Spot-check _parse_section for the same anchoring issue — flagged minor (comment)
  • Spot-check rollback UX (stdin/stdout/stderr) — flagged minor (comment)
  • Post inline comments + summary

Summary

The follow-up commit (4eb1262) does what the description says — addresses every flagged correctness/safety bug from the previous round and the Codex P1/P2 findings. The new helpers (_html_sibling_path, _mask_fenced_blocks, _uniquify_id, _collect_bullet_items_with_continuations, _opt_str_typed, _str_list_typed) are well-factored, well-commented, and tie back to the ticket cases by number.

The biggest residual issue is that the fence-masking fix was applied to _list_orphan_sections (line 404) and _parse_section (line 527) but not to _parse_narrative (line 701) or _diagrams_section_span (line 934). Same class of bug — a ## ... line inside a fenced block within Narrative will terminate the section early. The other issues are lower priority (manifest write-timing race, title over-association across a blank line, empty-sidecar UX inconsistency, stdin/stdout/stderr cosmetics).

Significant findings

Severity Finding
issue (bug) _parse_narrative and _diagrams_section_span regex run against unmasked body## fake heading inside a fence inside ## Narrative terminates the section early. Same class of bug as the previous review's _SECTION_REGEX finding; the fence-mask fix wasn't mirrored here.
issue (data-safety) Preserve-list manifest written after all file moves complete. Crash mid-loop → no manifest → next --rollback deletes pre-existing .html siblings (exactly the bug this PR fixes for the happy path). Write the manifest before the loop or incrementally.
suggestion (correctness) _FENCED_BLOCK_REGEX line-anchor fix correctly rejects mid-line **bold**, but \s*\n between title and fence still over-associates a **bold** line that's followed by a blank line + fence. Also: _FENCE_MASK_REGEX masks both ``` and ~~~ but _FENCED_BLOCK_REGEX only matches ```~~~-fenced diagrams survive masking but never become <bv-diagram>.
nit (minor) _parse_section still uses \n##\s lookahead — back-to-back ## A\n## B (no body between) makes section A swallow section B. Same fix as _parse_narrative (multiline-anchored (?m)^##\s[^#]) would resolve it.
nit (UX) Empty-file branch in _process_file (line 1556) auto-archives *.abstract.md / *.overview.md as empty-sidecar even when the sibling base doesn't exist — disagreeing with case 12's policy that standalone sidecars are real topics. Empty version of a standalone sidecar vanishes silently with no .html emitted.
nit (cosmetic) input() prompt writes to stdout while the surrounding preview goes to stderr — inconsistent stream usage on the destructive path.

Things I checked and was happy with

  • P1 multi-dot fix: _html_sibling_path() correctly uses string concatenation across all three call sites; <bv-topic path> and on-disk filename now agree for node.js.md-style names.
  • Preserve-list logic (modulo the write-timing concern): rollback's rel in preserve_html_siblings check correctly preserves pre-existing HTML siblings; the manifest is properly excluded from rollback's restore pass.
  • Malformed-frontmatter surfacing: 4-tuple return + malformed-frontmatter: ... warning is clean.
  • Rule lede-paragraph fix: parts[1:] explicitly drops the pre-prefix chunk.
  • Bullet continuation handling: _collect_bullet_items_with_continuations correctly folds indented follow-ups into the parent item.
  • Shared rule-id seen-set: _uniquify_id + threaded rule_id_registry deduplicates canonical + orphan-routed rules across the topic.
  • Narrative back-to-back H2: (?m)^##\s[^#] correctly handles ## Narrative\n## Facts (no blank line).
  • Sidecar existence check: non-empty standalone *.overview.md correctly classified as topic when sibling base is missing.
  • Type-mismatch warnings: _opt_str_typed / _str_list_typed correctly surface type mismatches without swallowing falsy values.
  • Rollback dry-run + --yes: safe-by-default with TTY confirmation; --dry-run for rollback works.
  • createdat/updatedat schema risk: documented inline at lines 1225–1236 — clear demarcation of the bv-topic schema's RESERVED_TOPIC_ATTRIBUTES boundary and the responsibility split with curate prompt builders.
    · branch feat/ENG-2834

@RyanNg1403 RyanNg1403 changed the title fix: MD→HTML migration script edge-case fixes + bv-* vocabulary refactor feat: add MD→HTML context-tree migration script (Python, one-shot) May 25, 2026
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
P1 (data-safety, bug):
- Preserve-list manifest is now written BEFORE archiving begins.
  Computing the list of .md topics whose .html siblings predate the
  migration up front (rather than after the archive loop) means a
  Ctrl+C / OOM / power loss mid-run still leaves a usable manifest on
  disk; rollback won't silently delete pre-existing siblings just
  because the script was killed before the manifest write.
- _parse_narrative and _diagrams_section_span now run against a
  fence-masked body so a literal `## ...` line inside a fenced code
  block within `## Narrative` doesn't truncate the section early and
  miscompute the diagrams-exclude span (which would otherwise produce
  duplicate <bv-diagram> emissions in _extract_all_fenced_blocks).
- Rollback warns loudly on stderr when the preserve manifest is
  missing or unreadable, instead of silently falling back to an empty
  preserve set.

P2 (correctness):
- _FENCED_BLOCK_REGEX now requires the optional **Title** prefix to
  be immediately followed by the fence opener (`[ \t]*\n`) so a
  standalone bolded paragraph + blank line before a fence is no
  longer captured as a spurious diagram title.
- _FENCED_BLOCK_REGEX matches both ``` and ~~~ fence markers,
  mirroring _FENCE_MASK_REGEX. Previously a ~~~-fenced diagram
  was masked from section detection but never promoted to bv-diagram,
  so its content vanished from the migrated HTML.
- _parse_section now uses the same `(?m)^##\s[^#]` anchoring as
  _parse_narrative so back-to-back H2s with no blank line between
  (e.g. `## Reason\n## Facts`) terminate the section correctly.

Consistency / UX:
- Empty `.abstract.md` / `.overview.md` files with no base sibling now
  surface as `failed` (empty-file) instead of being silently archived
  as `empty-sidecar`. Aligns with case-12: non-empty standalone
  sidecars migrate as real topics, so empty ones are real topics with
  empty content — the operator can decide whether to delete or fill.
- Rollback TTY confirmation prompt and dry-run preview are both
  written to stderr; the prompt itself is read via sys.stdin.readline
  rather than input() (which writes to stdout). Stream consistency
  for `--rollback 2>preview.log`.

Validation:
- Re-ran migration on all 9 baseline workspaces (246 file golden
  diff): 137 changed, 109 unchanged, 37 new files (case-12 sidecar
  promotions), 0 lost files, 0 canonical bv-* losses,
  0 non-bv-* tag emissions.
- 3 sub-agent corpora freshly generated via Gemini
  gemini-3-flash-preview (36 topics across realistic engineering
  domains + an explicit stress-test agent): 0 migration failures,
  0 non-bv-* tag emissions, all 285 tag emissions are from the
  closed bv-* vocabulary plus <li> legitimately nested in
  bv-changes/bv-files.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 6m 26s —— View job


Claude Code PR Review — #700 (post-fix verification)

Overall score: 5 / 5 — the 881eb2f follow-up commit cleanly addresses every P1/P2/nit from the prior round. Two minor residual nits surfaced (one fence-mask gap in the rules splitter, one help-text/code mismatch on the non-TTY rollback path), neither blocking.

Todo

  • Pull latest branch state and inspect 881eb2f diff vs 4eb1262
  • Verify P1 fix: fence-mask applied to _parse_narrative and _diagrams_section_spanclean (comment)
  • Verify P2 fix: preserve-manifest written before destructive moves — clean (comment)
  • Verify nit fix: _FENCED_BLOCK_REGEX blank-line-before-fence + ~~~ support — clean (comment)
  • Verify nit fix: _parse_section back-to-back H2 anchoring — clean
  • Verify nit fix: empty-sidecar UX inconsistency — clean, now routes through _archive_failed("empty-file") so the operator notices
  • Verify nit fix: prompt UX stream consistency — clean, stderr+stdin, plus --dry-run+--rollback preview also moved to stderr
  • Spot-check for regressions: split_rules_block fence-mask gap — flagged minor (comment)
  • Spot-check rollback --yes semantics — flagged minor docs/code mismatch (comment)
  • Post inline comments + summary

Summary

The follow-up commit (881eb2f) is high quality. Every concern flagged in the prior round is addressed; the new code is well-commented (each change ties back to a ticket case and explains why the prior approach was wrong); the validation surface listed in the commit body (137 changed / 109 unchanged / 37 new / 0 lost across 246 baseline files, plus 3 Gemini-generated stress corpora) matches the change scope.

The biggest residual is symmetry, not correctness — split_rules_block still operates on the unmasked text, so a Rule 1: or - code line inside a fenced code block inside ### Rules can flip the splitter mode. Same class of bug the rest of this commit cleaned up; the fix is uniform (one _mask_fenced_blocks call). Low priority because rules blocks rarely contain fenced code samples.

The non-TTY rollback path's --yes help text says it's "required for non-interactive use" but the code silently skips confirmation when stdin isn't a TTY. Pick a contract — either tighten the code or loosen the help text.

Significant findings

Severity Finding
suggestion (correctness) split_rules_block doesn't mask fenced blocks before bullet/prefix detection or paragraph split. Same class as the previously fixed _parse_narrative / _parse_section / _list_orphan_sections / _diagrams_section_span cases — a Rule N: or - foo line inside a fenced code sample inside ### Rules can flip the detector and produce spurious <bv-rule> entries. Low priority.
nit (docs vs. code) --yes help text says "Required for non-interactive use" but if not args.yes and sys.stdin.isatty(): silently skips the prompt when stdin isn't a TTY. CI scripts that mistakenly enable --rollback run destructively without confirmation. Tighten the code or update the help text.
nit (cosmetic) When pre_existing_preserve is empty no manifest is written, so rollback emits the "no preserve-list manifest" warning even for clean migrations. Could write an empty manifest unconditionally so the warning only fires when something actually went wrong.

Things I checked and was happy with

  • P1 fence-mask propagation: _parse_narrative (line 717) and _diagrams_section_span (line 957) now both mask fences before regex; content is sliced from the unmasked body via matched spans, so fenced code survives intact. _extract_all_fenced_blocks (which walks the unmasked body) gets correct dedup spans.
  • P2 manifest write-timing: preserve list computed and persisted at line 1671–1689 before any _move() call. Crash mid-loop → manifest still on disk → rollback correctly preserves pre-existing siblings. Rollback also warns on stderr for missing/corrupt manifests (line 1748–1762).
  • _FENCED_BLOCK_REGEX rewrite: (?:(?:^|\n)\*\*(.+?)\*\*[ \t]*\n)?(```|~~~)(\w*)\n([\s\S]*?)\2 correctly: (a) rejects mid-line bold as a title, (b) rejects a blank line between title and fence, (c) matches both ``` and ~~~ openers, (d) pairs closer to opener style via the \2 backref. All three call sites updated to the new group numbering (1/3/4).
  • _parse_section anchoring: now (?ms)^##\s[^#] like _parse_narrative — back-to-back ## Reason\n## Facts (no blank line) terminates correctly.
  • Empty-sidecar UX: removed the empty-sidecar archive branch; empty standalone *.abstract.md / *.overview.md now routes through _archive_failed("empty-file"), consistent with case 12's contract that standalone sidecars are real topics.
  • Rollback prompt: uses sys.stderr.write + sys.stdin.readline() so the prompt sits in the same stream as the preview. --dry-run+--rollback preview also goes to stderr. Stream-consistent for --rollback 2>preview.log.
  • createdat= / updatedat= schema risk: documented inline at lines 1249–1260 — clear demarcation of RESERVED_TOPIC_ATTRIBUTES boundary and the responsibility split with curate prompt builders.
    · branch feat/ENG-2834

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Correctness:
- split_rules_block runs its bullet / numbered / Rule-prefix /
  paragraph detection against a fence-masked copy of the input. Same
  class of bug the prior commit cleaned up elsewhere: a `- some code`
  line or a `Rule 1:` comment inside a fenced sample within `### Rules`
  used to flip the detector and spawn a spurious rule entry. Item
  extraction still operates on the original text via byte-span slicing
  so the bullet collector keeps multi-line continuations intact.

UX (safety):
- `--rollback` without `--yes` now ERRORS OUT when stdin is not a TTY
  (CI / piped invocations), instead of silently proceeding with the
  destructive operation. Help text updated to match. The previous
  behavior — "assume non-interactive caller accepted the risk" — is
  unsafe for a typo'd CI command; explicit consent is required.

Observability:
- Preserve-list manifest is now written unconditionally (even when
  the list is empty) so rollback's "no preserve-list manifest" stderr
  warning only fires on genuine anomalies (archive predates feature,
  prior run killed before write, manifest deleted) — not on the
  common case of a clean migration with no pre-existing siblings.

Validation:
- 9 baseline workspaces vs golden: 137 changed, 109 unchanged,
  37 new (case-12 sidecar promotions), 0 lost, 0 canonical bv-* lost,
  0 non-bv-* tag emissions.
- 2 fresh sub-agent corpora via Gemini gemini-3-flash-preview, one
  explicitly stress-testing the new fence-mask-in-split-rules-block
  fix (fenced "Rule N:" comments, mixed ``` / ~~~ fences, bullet
  rules with fenced pseudocode): 25 topics migrated, 0 failures,
  0 non-bv-* tag emissions, 200 tag emissions all from the closed
  bv-* vocabulary plus <li>/<pre>/<code> as documented children.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 4m 7s —— View job


Claude Code PR Review — #700 (4th-round verification)

Overall score: 5 / 5 — commit 38e68f2 cleanly addresses all three concerns from the prior round (split_rules_block fence-mask gap, --yes docs/code mismatch, empty-manifest cosmetic). One residual same-class symmetry issue surfaced (_parse_narrative's inner ### X subsection iterator still walks unmasked content); low-priority and not blocking.

Todo

  • Pull latest branch state and inspect 38e68f2 diff vs 881eb2f
  • Verify split_rules_block fence-mask fix — clean (comment)
  • Verify --yes help text / code reconciliation — clean (comment)
  • Verify "empty manifest written unconditionally" cosmetic fix — clean (comment)
  • Spot-check for regressions — inner narrative ### X iterator residual (comment)
  • Post inline comments + summary

Summary

The follow-up commit (38e68f2) is high quality. Each of the three flagged items lands cleanly:

  • split_rules_block fence-mask: detector now runs on a masked copy (lines 289–291, 296), and the rule-prefix + paragraph-fallback branches use a clever finditer(masked) + slice-from-original idiom (lines 310–326) so fenced content between prefixes is preserved verbatim. The author's inline comment correctly acknowledges the residual fence-blindness of the bullet collector as an acceptable tradeoff (the detector prevents entry when only fenced bullets exist).
  • --yes for non-TTY: now explicitly errors out with return 2 and a clear stderr message, matching the updated help text. This is the safer default for CI / piped invocations.
  • Unconditional manifest write: dropping and pre_existing_preserve from the gate silences the false-positive warning on clean migrations while keeping dry-run safety. Empty-list round-trip is symmetrical.

The one residual is _parse_narrative's inner ### X subsection iterator (line 754–756). The previous commits fence-masked the outer narrative span and _diagrams_section_span, but the inner subsection regex still walks section (unmasked). Same class of bug — a fenced ### Foo line inside ## Narrative > ### Structure will truncate Structure's body and spawn a phantom subsection routed through case 8 heuristic. Low priority because the authoring pattern is rare; same uniform fix as the rest of the PR's fence-mask refactor.

Significant findings

Severity Finding
suggestion (correctness) _parse_narrative's inner ### X subsection iterator (line 754–756) runs against the unmasked section. Same class of bug as the outer _parse_narrative regex was before its fix — fenced ### Foo inside Narrative truncates the enclosing ### Structure/### Highlights/etc. subsection and emits a phantom unknown subsection. Suggest mirroring the mask-and-span-slice pattern used by the outer narrative regex and _parse_section. Also: the inner lookahead \n##\s is still the literal-newline anchor that _parse_section and _parse_narrative were both upgraded away from — ^##\s[^#] would catch the back-to-back-H2 edge case here too.

Things I checked and was happy with

  • split_rules_block fence-mask trace: masked preserves byte positions via same-length whitespace replacement, so _RULE_PREFIX_LINE.finditer(masked) yields spans valid against trimmed. Worked the ```python\nRule 1: dont\n``` case end-to-end — falls through to paragraph fallback, fenced content emitted verbatim. ✓
  • Help-text alignment: --yes REQUIRED for non-TTY now matches return 2 enforcement at line 1921. Exit code 2 follows argparse usage-error convention.
  • Manifest empty-list round-trip: json.dumps({"preserve_html_siblings": []}, indent=2) → file → data.get("preserve_html_siblings", []) → empty set. Symmetrical, no edge-case regression.
  • Dry-run safety preserved: manifest write still gated by if not dry_run; no disk side effects on preview runs.
  • Outer fence-mask invariants from prior round are still intact: _parse_narrative outer span, _parse_section, _list_orphan_sections, _diagrams_section_span all still mask before regex and slice from unmasked via spans.
  • Validation claims in commit body (9 baseline workspaces: 137 changed / 109 unchanged / 37 new / 0 lost; 25 stress-tested topics: 0 failures, 0 non-bv-* emissions, 200 tags all from closed vocabulary) align with the change scope and the targeted nature of the diff.
    · branch feat/ENG-2834

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
…terator symmetry)

Close the last fence-mask symmetry gap: `_parse_narrative`'s inner
`### X` subsection iterator now runs against a fence-masked copy of
`section` (mirroring what the outer narrative regex, `_parse_section`,
`_list_orphan_sections`, and `_diagrams_section_span` already do).
Without this, a fenced `### Foo` inside `## Narrative > ### Structure`
would truncate Structure's body and emit a phantom subsection routed
via the case-8 heuristic.

Also upgrade the inner lookahead from `\n##\s` to `^##\s[^#]`
(multiline) so a `### X` followed IMMEDIATELY by another H2 (no blank
line) terminates correctly — consistent with the same upgrade
previously applied to `_parse_section` and `_parse_narrative`'s outer
regex.

Validation:
- Baseline (246 files, 9 workspaces): 137 changed, 109 unchanged,
  37 new (case-12 sidecar promotions), 0 lost, 0 canonical bv-* lost,
  0 non-bv-* tag emissions.
- 2 fresh sub-agent corpora via Gemini gemini-3-flash-preview, one
  explicitly stress-testing the inner-iterator fix with `### Foo`
  embedded inside narrative fenced code samples: 28 topics migrated,
  0 failures, 0 non-bv-* tag emissions, 211 tag emissions all from
  the closed bv-* vocabulary + <li> as a documented child.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 2m 31s —— View job


Claude Code PR Review — #700 (5th-round verification)

Overall score: 5 / 5 — commit 050eeeb cleanly resolves the 4th-round residual. The inner narrative ### X iterator now mirrors the exact mask-and-span-slice pattern used by the five other section-walking regexes in the file. PR is ready to merge from a correctness standpoint.

Todo

  • Pull latest branch state and inspect 050eeeb diff vs 38e68f2
  • Verify inner-narrative ### X iterator fence-mask + anchor fix — clean (comment)
  • Spot-check for any new regressions — two low-priority symmetry nits (snippets) · (diagrams span)
  • Post inline comments + summary

Summary

The diff is minimal and surgical — 12 lines added, 2 removed, all in _parse_narrative's inner subsection iterator. Three substantive changes:

  1. section_masked = _mask_fenced_blocks(section) before the inner regex
  2. re.finditer(..., section_masked) instead of ..., section
  3. Lookahead \n##\s^##\s[^#] (multiline), aligned with _parse_section and the outer _parse_narrative
  4. sub_body = section[sm.start(2):sm.end(2)] (byte-span slice from unmasked) instead of sm.group(2)

This is the same uniform fix the rest of the PR has been applying across all six section-walkers (split_rules_block, _list_orphan_sections, _parse_section, outer _parse_narrative, _diagrams_section_span, and now the inner ### X iterator). The 4th-round review's worked example — fenced ### Foo inside ## Narrative > ### Structure truncating Structure and emitting a phantom subsection — is fully resolved.

Inline comments record the verification and two non-blocking symmetry nits I noticed while spot-checking:

  • _extract_snippets_from_body is the one remaining function that walks ## headings without fence-masking. The failure mode is a dropped-snippets warning false-positive/false-negative, not data loss. Cheap to fix, same idiom as split_rules_block.
  • _diagrams_section_span inner diagram-subsection regex still uses \n###\s|\n##\s while the outer narrative regex two lines up was upgraded to ^##\s[^#]. Benign because the outer span has already bounded the search, but minor cosmetic asymmetry.

Both are low priority and unrelated to the core fix in this commit.

Significant findings

Severity Finding
nit (minor) _extract_snippets_from_body (lines 1458–1465) doesn't fence-mask before stripping canonical headings — one remaining section-walker that didn't get the uniform treatment. Failure mode is a snippets-warning false positive/negative, not data loss. (comment)
nit (cosmetic) _diagrams_section_span's inner ### Diagrams regex (line 1004) still uses `\n###\s

Things I checked and was happy with

  • Mask invariants verified: _mask_fenced_blocks at line 424 uses " " * len(m.group()) whitespace replacement, preserving byte positions. The new section[sm.start(2):sm.end(2)] slice into unmasked section is valid.
  • Trace check: synthetic body with ### Structure\n\nreal intro\n\n```bash\n### fake\n```\n\nmore real\n\n### Highlights — masked text whitespaces out the fence, ### Structure's span extends through "more real" to ### Highlights, fenced content recovered verbatim via byte-span slice into unmasked section. ✓
  • Lookahead upgrade: ^##\s[^#] (multiline) correctly handles back-to-back ### Structure\n## Facts and ## Facts\n## Reason cases that the previous \n##\s literal-newline anchor would have absorbed.
  • Symmetry across the file: all six section-walking regexes (split_rules_block, _list_orphan_sections, _parse_section, outer _parse_narrative, inner _parse_narrative ### iterator, _diagrams_section_span) now follow the same mask-and-span-slice pattern. Maintainability win.
  • No regressions: outer narrative regex, fence-mask helper, byte-position invariants, and lookahead semantics all unchanged in this commit.
    · branch feat/ENG-2834

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py Outdated
…mmetry)

Both findings labeled low-priority by the reviewer ("not a primary
data-loss path", "essentially defensive"), but applying for full
pattern uniformity: every section walker in the file now uses the
same fence-mask + `(?m)^##\s[^#]` multiline-anchor idiom, so future
edits can't accidentally drift back into the old literal-`\n##\s`
behavior in one place while the rest of the file stays consistent.

_extract_snippets_from_body:
- Heading-strip loop and orphan-section strip now operate on a
  fence-masked copy of the body so a literal `## X` line inside a
  code fence within (say) `## Reason` can't terminate the strip
  early and produce a false-positive `dropped-snippets` warning.
- Strip is now span-based — drop spans are collected from the masked
  text, merged, then the residual is rebuilt from the ORIGINAL body
  via the inverse span set. Fenced content inside the residual
  (i.e., outside any canonical section) survives byte-for-byte.
- Lookahead also upgraded to `(?ms)^##\s[^#]|\n---\n|\Z` for the
  back-to-back-H2 case.

_diagrams_section_span inner `### Diagrams` regex:
- Inner lookahead upgraded from `\n###\s|\n##\s|$` to `(?ms)^###\s|^##\s[^#]|\Z`,
  matching the outer narrative regex, _parse_section, the inner
  narrative iterator, and `split_rules_block`. Benign in practice
  (outer regex already bounds `section`), purely a consistency fix.

Validation:
- Baseline (246 files, 9 workspaces): 137 changed, 109 unchanged,
  37 new, 0 lost, 0 canonical bv-* losses, 0 non-bv-* tag emissions.
- 2 fresh sub-agent corpora via Gemini gemini-3-flash-preview,
  one explicitly stress-testing the snippet extractor (fenced `## X`
  inside narrative) and the inner Diagrams anchor (back-to-back
  `### Diagrams\n### Implementation Notes` with no blank line):
  18 topics migrated, 0 failures, 0 non-bv-* tag emissions.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 4m 46s —— View job


Claude Code PR Review — #700 (6th-round verification)

Overall score: 5 / 5 — commit f7a1c7a resolves the 5th-round symmetry concerns. The previously-flagged _diagrams_section_span inner-regex anchoring matches the rest of the file, and _extract_snippets_from_body now masks fences before running its strip patterns. One residual: the new parallel-span collection in _extract_snippets_from_body introduces a subtle regression vs. the prior sequential-sub() chain that affects only the dropped-snippets warning surface (not content emission).

Todo

  • Pull latest branch state and inspect f7a1c7a diff vs 050eeeb
  • Verify nit fix: _extract_snippets_from_body fence-mask propagation — partial; warning-surface regression (comment)
  • Verify nit fix: _diagrams_section_span inner regex anchoring symmetry — clean (comment)
  • Spot-check for any new regressions or remaining symmetry gaps — no new symmetry gaps
  • Post inline comments + summary

Summary

The diff is minimal (+42 / −6, both in migrate_context_tree.py). Two substantive changes:

1. _diagrams_section_span inner regex (lines 1009–1013): lookahead upgraded from \n###\s|\n##\s to (?ms)^###\s|^##\s[^#]|\Z. The inline comment correctly notes this is symmetry-only — the outer narrative regex already bounds section so no ## line appears inside it — but the change closes the last cosmetic gap from the 5th-round review. Clean.

2. _extract_snippets_from_body fence-mask + span-merge refactor (lines 1465–1500): the function now masks fences before running the canonical-heading strip and the orphan-section strip, accumulates match spans, merges overlapping spans, and rebuilds the residual text from the unmasked body. This is the correct architectural shape — a fenced ## X line no longer terminates a strip pattern, and fenced content survives intact via byte-span slicing. But the orphan-section strip via _SECTION_REGEX lacks the \n---\n terminator that the canonical-heading strip has. So when a canonical heading sits adjacent to a ruler-separated snippet block (e.g. ## Reason\nfoo\n---\nlegacy snippet), the orphan strip greedily covers the snippet content too. The merge step takes the larger span and the residual collapses to empty — no dropped-snippets warning fires where one would have under the prior sequential-sub() chain. Severity is low because the function only drives a warning at line 1273–1278 (it doesn't gate any bv-* element emission), but it's a behavior change introduced by this commit. Fix is a one-line filter mirroring _list_orphan_sections (line 440).

The PR's golden-baseline corpus (246 files) wouldn't catch this — the test asserts "0 canonical bv-* elements lost" / "0 non-bv-* tags emitted", which are content-emission claims. The warning surface isn't part of the validation matrix.

Significant findings

Severity Finding
suggestion (correctness) _extract_snippets_from_body regression: _SECTION_REGEX spans (no \n---\n terminator) overlap and swallow canonical-heading spans (which DO terminate at \n---\n), causing the merge step to strip ruler-separated snippets that were detected under the prior sequential-sub() chain. Filter canonical headings out of the orphan loop with a one-line if m.group(1).strip() in KNOWN_SECTION_HEADINGS: continue. Warning-surface only, no data-emission impact.

Things I checked and was happy with

  • _diagrams_section_span inner regex symmetry: anchoring upgraded consistently with the other six section-walkers in the file. The outer regex already bounds section so this is purely maintainability — but the symmetry is welcome.
  • Fence-mask propagation in _extract_snippets_from_body: all section regexes now run against masked; content sliced from unmasked body via byte-spans. Byte-position invariants (_mask_fenced_blocks uses whitespace replacement at line 424) hold.
  • Span-merge correctness: the interval-merge algorithm at lines 1487–1493 sorts then merges if s_start <= merged[-1][1] — classic and correct. Residual rebuild at lines 1494–1499 correctly walks body and skips merged spans.
  • All six section-walkers now follow the uniform mask-and-span-slice + multiline-anchor pattern: split_rules_block, _list_orphan_sections, _parse_section, outer _parse_narrative, inner _parse_narrative ### X iterator, _diagrams_section_span (outer + inner). Symmetry complete.
  • No regressions in content-emission paths: the changes are isolated to the snippet-detection helper (which only drives a warning) and a cosmetic regex tweak. The fenced-block extraction, orphan routing, canonical strip, rule splitter, narrative iteration, and topic emission paths are unchanged.
  • Commit message ties each change to its prior review-round nit and explains the rationale. Good provenance trail.
    · branch feat/ENG-2834

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
The previous commit's parallel-span collection in
`_extract_snippets_from_body` introduced a subtle regression: the
orphan-section loop walked `_SECTION_REGEX` (which has NO `\n---\n`
terminator), so a canonical `## Reason` heading adjacent to a
`---`-separated snippet would match the WHOLE `## Reason\n...\n---\n
snippet...` span, the merge step picked the larger span over the
canonical-loop's `\n---\n`-terminated span, and the snippet vanished
from the residual — no `dropped-snippets` warning fired.

The pre-refactor sequential `.sub()` chain dodged this because the
second strip operated on the already-cleaned residual where the
canonical heading was gone. The parallel-span refactor lost that
ordering guarantee.

Fix: filter out canonical headings in the orphan-section loop (same
filter that `_list_orphan_sections` already uses at the `KNOWN_SECTION_
HEADINGS` check). Canonical spans are added by the dedicated loop
above with the correct `\n---\n` terminator; the orphan loop should
only contribute spans for truly-unknown headings.

Added an inline regression test in the commit message verification:
```
case 1: ## Reason\\nbecause foo\\n---\\nlegacy snippet content
  → ['legacy snippet content']  ✓ (was [] before fix)
case 2: ## Reason\\nfoo\\n---\\nsnippet\\n## Facts\\nbar
  → ['snippet']  ✓ (was [] before fix)
```

Validation:
- Baseline (246 files, 9 workspaces): 137 changed, 109 unchanged, 37
  new, 0 lost, 0 canonical bv-* losses, 0 non-bv-* tag emissions.
- 2 fresh sub-agent corpora via Gemini gemini-3-flash-preview, one
  explicitly producing the ruler-separated snippet shape the bot
  flagged: 26 topics migrated, 0 failures, 0 non-bv-* tag emissions.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 2m 56s —— View job


Claude Code PR Review — #700 (7th-round verification)

Overall score: 5 / 5 — commit 07e61e2 cleanly resolves the 6th-round snippet-swallow regression. The fix matches the previously-suggested one-liner exactly and threads correctly with the existing canonical-vs-orphan span ordering. One theoretical residual (case-sensitivity asymmetry between the canonical loop's re.IGNORECASE and the orphan-filter's case-sensitive set membership) noted as low-priority — fully optional.

Todo

  • Pull latest branch state and inspect 07e61e2 diff vs f7a1c7a
  • Verify the 6th-round residual: _extract_snippets_from_body snippet-swallow regression — clean (comment)
  • Spot-check for any new regressions or remaining symmetry gaps — one low-priority case-sensitivity nit
  • Post inline comments + summary

Summary

Minimal, surgical diff (+10 lines, all in _extract_snippets_from_body's orphan-section loop). The change adds the canonical-heading filter that was missing from the 6th-round refactor — exactly the one-liner suggested in the prior review:

for m in _SECTION_REGEX.finditer(masked):
    if m.group(1).strip() in KNOWN_SECTION_HEADINGS:
        continue
    drop_spans.append((m.start(), m.end()))

Canonical headings now contribute spans only through the dedicated loop (line 1473–1479) which carries the \n---\n terminator. The orphan loop contributes spans only for truly-unknown headings. The merge step at line 1497–1503 no longer picks an over-long orphan span over a correctly-terminated canonical one.

Trace check (both worked examples from the commit message):

Input Canonical span Orphan span Residual Snippets
## Reason\nbecause foo\n---\nlegacy snippet content (0, 21) (terminates at \n---\n) skip (Reason ∈ KNOWN) \n---\nlegacy snippet content ["legacy snippet content"]
## Reason\nfoo\n---\nsnippet\n## Facts\nbar Reason (0, 15), Facts (...) both skip \n---\nsnippet\n between ["snippet"]

The pre-fix behavior would have returned [] for both cases (snippet vanished), suppressing the dropped-snippets warning. Post-fix returns the snippets correctly so the warning surface at line 1273–1278 fires when something would otherwise be silently lost.

Significant findings

Severity Finding
nit (low-priority, theoretical) KNOWN_SECTION_HEADINGS set membership (line 1491) is case-sensitive, but the canonical loop above uses re.IGNORECASE. For a lowercase ## reason heading: canonical loop adds a \n---\n-terminated span (correct), orphan filter doesn't match ("reason" ∉ {"Reason", ...}) → orphan loop adds an unterminated span → merge picks the larger → snippet vanishes. Same regression class as the fix. In practice every production tree uses Title Case and the 246-file baseline + Gemini stress corpora caught nothing. Cheap fix is a .lower() on both sides, mirrored in _list_orphan_sections (line 440) for consistency. Optional. (comment)

Things I checked and was happy with

  • KNOWN_SECTION_HEADINGS ↔ canonical-loop set alignment: line 104's {"Reason", "Raw Concept", "Narrative", "Facts", "Relations"} exactly matches the canonical loop's iteration tuple at line 1473 ("Relations", "Reason", "Raw Concept", "Narrative", "Facts"). No drift.
  • Filter call pattern matches prior art: _list_orphan_sections at line 440 uses the same if heading in KNOWN_SECTION_HEADINGS: continue pattern. Symmetric.
  • _SECTION_REGEX capture-group safety: m.group(1).strip() strips trailing whitespace before $ anchor, consistent with how heading text flows through the rest of the module.
  • Dry-run / mask invariants preserved: the helper itself is pure, no I/O, no side effects on body. The masked vs. unmasked discipline (residual rebuilt from body[cursor:s_start] at line 1507) is untouched.
  • Validation claims in commit body (246 baseline files: 137 changed / 109 unchanged / 37 new / 0 lost / 0 canonical bv-* losses; 2 fresh Gemini-generated corpora explicitly including the ruler-separated snippet shape) align with the targeted nature of the diff.
  • All six section-walkers still consistent: split_rules_block, _list_orphan_sections, _parse_section, outer _parse_narrative, inner _parse_narrative ### X iterator, _diagrams_section_span (outer + inner), and now _extract_snippets_from_body all follow the mask-and-span-slice + multiline-anchor pattern. Symmetry complete.
    · branch feat/ENG-2834

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
…try)

Make the orphan-section canonical-heading filter case-insensitive so it
stays consistent with the canonical-heading loops that use
`re.IGNORECASE`. Add `KNOWN_SECTION_HEADINGS_LOWER` constant and apply
the lowered comparison in both `_list_orphan_sections` and
`_extract_snippets_from_body`.

Without this, a lowercase `## reason` adjacent to a `---`-separated
snippet would slip past the orphan-walker filter and emit an
unterminated drop span that swallows the snippet on merge — the same
regression class the previous round closed for the canonical-vs-orphan
walker. Theoretical for current production trees (all Title Case) but
removes the asymmetry.
@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @RyanNg1403's task in 5m 46s —— View job


Claude Code PR Review — #700 (8th-round verification)

Overall score: 5 / 5 — commit 8078940 cleanly resolves the 7th-round case-sensitivity asymmetry exactly as suggested. One previously-unnoticed esoteric edge case in split_rules_block's paragraph fallback flagged for the record (low-priority, not blocking). The PR is in excellent shape.

Todo

  • Pull latest branch state and inspect 8078940 diff vs 07e61e2
  • Verify the 7th-round residual: case-sensitivity asymmetry in _extract_snippets_from_body + _list_orphan_sectionsclean (comment)
  • Spot-check for any new regressions or remaining symmetry gaps — one esoteric paragraph-fallback edge case noted (comment)
  • Post inline comments + summary

Summary

Surgical diff (+15 / −2, all in migrate_context_tree.py). Three substantive changes:

1. New constant KNOWN_SECTION_HEADINGS_LOWER (line 109): case-folded view of the canonical heading set, computed once at module load. Cleaner than .lower()-ing the set inline at every call site.

2. _list_orphan_sections line 448: if heading in KNOWN_SECTION_HEADINGSif heading.lower() in KNOWN_SECTION_HEADINGS_LOWER. The orphan walker now matches the case-insensitive semantics of the canonical parser (_parse_section / _parse_narrative both use re.IGNORECASE). Previously a lowercase ## reason heading would have been routed through BOTH the canonical parser AND the orphan heuristic.

3. _extract_snippets_from_body line 1504: same .lower() + KNOWN_SECTION_HEADINGS_LOWER swap. Prevents a lowercase canonical heading from yielding an unterminated _SECTION_REGEX span that would swallow an adjacent \n---\n-separated snippet on merge (same regression class the 6th-round commit closed for the case-sensitive variant).

The inline comments at both call sites explicitly explain the why (consistency with re.IGNORECASE) and the failure mode if you skip the fold (snippet swallow / canonical-vs-orphan duplication).

Significant findings

Severity Finding
nit (very low priority, esoteric) split_rules_block's paragraph fallback at lines 322–331 runs \n\s*\n against masked. The fence-masker replaces fence content (including internal newlines) with all-spaces, so an inline fenced block between two prose paragraphs (no blank lines) looks like a blank-line separator after masking — the fence content falls into a gap between spans and silently vanishes. Only triggers when a ### Rules block uses neither bullets nor Rule N: prefixes AND has an inline fence. Posted for the record; not blocking. (comment)

Things I checked and was happy with

  • Constant location & naming: KNOWN_SECTION_HEADINGS_LOWER placed adjacent to KNOWN_SECTION_HEADINGS at line 109 with explanatory comment; one-time computation via {h.lower() for h in KNOWN_SECTION_HEADINGS}. Module-level scope means no recomputation per call.
  • Symmetry across both call sites: both filters use the same pattern heading[.strip()].lower() in KNOWN_SECTION_HEADINGS_LOWER. No drift.
  • Trace check (lowercase canonical adjacent to ruler-separated snippet) — confirmed in inline comment: canonical loop adds terminated span, orphan loop now skips, residual contains snippet, warning fires. ✓
  • No regressions in adjacent code: the rest of _extract_snippets_from_body (canonical loop at lines 1481–1487, fence-masking at line 1479, merge step at lines 1510–1516, residual rebuild at lines 1517–1522) is unchanged. The fix is purely additive.
  • All six section-walkers (split_rules_block, _list_orphan_sections, _parse_section, outer _parse_narrative, inner _parse_narrative ### X iterator, _diagrams_section_span outer + inner) plus _extract_snippets_from_body now follow the uniform mask-and-span-slice + multiline-anchor pattern. Symmetry complete across the entire file.
  • Commit body correctly notes the production tree case (Title Case headings) wouldn't hit this and acknowledges the fix is asymmetry removal rather than active-bug closure. Honest provenance.
  • Validation surface (per commit body): 246-file baseline + Gemini stress corpora unchanged from prior round; no functional drift expected since production trees use Title Case.
    · branch feat/ENG-2834

Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
Comment thread scripts/migrate-context-tree-py/migrate_context_tree.py
@RyanNg1403 RyanNg1403 closed this May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants