Skip to content

fix: unified-tree follow-ups in expand, intros and standard indexing - #554

Merged
rejojer merged 7 commits into
mainfrom
fix/unified-tree-review
Oct 7, 2026
Merged

rejojer merged 7 commits into
mainfrom
fix/unified-tree-review

Conversation

@rejojer

@rejojer rejojer commented Oct 7, 2026

Copy link
Copy Markdown
Member

These are follow-ups to #541 (the unified document tree). There are seven commits; please squash them on merge.

Fixes

An index could fail while its summaries were being frozen (50caf15)
Expand priced a candidate level without the intro that attach_children then adds. When an intro shared its first child's page, the committed node could cost exactly its span, so the next merge folded it back after its summary was final. The whole index then raised "dropped or changed after it was marked final". expand_cost now prices the level together with its intro.

Running headers cut intros and the Preface a page early (50caf15)
heading_at_page_start accepted any first line containing the heading. Now the first line must start with the heading, and no later line may start with it. A heading with no Latin letter is never placed, and an uncertain page is shared.

Expand only keeps the subsections printed inside the node (50caf15, ff4cfd0, 2873883, c74e192)
Expand used to adopt headings that belong elsewhere: the next node's heading on a shared last page, the parent's heading printed above, or a neighbour's subsection. Now a proposal is dropped in two cases:

  • It already is a node, either in the tree as expand found it or one the node's own ancestors made.
  • On a page the node shares, it is printed above the node's heading (else the nearest ancestor heading found there), or at or below the next node's heading (else the heading of the next node's first descendant on that page).

A heading that isn't found on its page decides nothing. A leading number is ignored only when exactly one of the two titles prints it, and only when the titles contain Latin letters. This stops CJK chapters from losing their first subsection.

The ownership filter now runs outside the model-error handler (c29ffa7)
Before, a bug in the filter would have been logged as a model error and the node would have been left silently unexpanded.

optimize_tree(pdf_path) and the tree_optimize CLI now read pages the way flash does (c74e192)
Lines used to be sorted by height across columns, so on two-column pages subsections fell after the next section's heading and were cut. Pages now come from flash's own text in layout order. Reading a 1,100-page book takes about 30 s instead of 5 s, and the CLI no longer needs pymupdf.

Standard indexing

  • The large-node split no longer loops forever when the model lists another heading above the section's own, for example "PART II" above "Chapter 3" (c74e192).
  • post_processing no longer ends a TOC item before its start (ff4cfd0).
  • Node text that nothing reads is no longer built (c74e192). The stored tree and the prompts are unchanged.

Smaller fixes

  • A summary reply only counts as answered when a summary parses out of it. A model that returns {"summary": ""} everywhere now hits the all-failed error (ff4cfd0).
  • The local get_document_structure tool reads through client.get_tree, so it gets the end < start clamp (50caf15).

Tests

  • A test runs the standard tree_parser path end to end (50caf15).
  • The digit-only heading rule and intro idempotency are pinned (cdcaf65).
  • Every part of the ownership filter has a test that fails without it (0be2352).
  • A two-column test runs without pymupdf: build_pdf in tests/conftest.py now also accepts (x, y, size, text) lines.

Known issues, not fixed here

  1. The tree can depend on reply order. This happens on a page a node shares with the next node, when the next node's heading is not recognized there and the next node is expanded in the same pass. The fallback anchor then reads that node's children while they are being built.
  2. The standard large-node split can adopt the next section's heading. When the next section's heading sits on the shared last page, the split can add it as a one-page child. This was there before, and the cloud has the same code.

#553 also edits build_pdf in tests/conftest.py, so it will be rebased after this lands.

Flash expand priced a candidate level without the intro attach_children
then adds. An intro sharing its first child's page could make the
committed node cost exactly its span, so the next merge folded it back
after its summary was final, and the whole index raised "dropped or
changed after it was marked final" (PRML 5.1, the 2023 annual report).
expand_cost now prices the level with its intro.

heading_at_page_start took any first line containing the heading as the
heading opening its page. A running header repeating the section title
cut intros and the Preface a page early, so the opening text above the
real heading was in no node. The first line must now start with the
heading, no later line may start with it, and a heading with no Latin
letter is never placed. An uncertain page is shared.

Expand on an intro or the Preface could list the heading printed on its
shared last page (the next node's) or atop it (its parent's) as a new
child. A proposal whose page and title, leading number aside, match a
node already in the tree is dropped.

The local get_document_structure tool read the stored tree through
LocalAPI.raw_tree and missed get_tree's end < start clamp. raw_tree is
gone; both modes read client.get_tree.

A test runs the standard tree_parser path end to end: intro insertion
before and after the large-node split, and the covering pass on the
default tail.
50caf15 dropped a proposed child whose page and title, leading number
aside, matched any node in the live tree. Sibling expansions run
concurrently, so the result hung on reply order: when Methods' reply
landed before Results', Methods kept "3.1 Discussion" from their shared
page and Results lost its own. Stripping every leading number also took
"3.2 Results" for its parent "3 Results" and dropped it.

A proposal is now dropped when it already is a node, in the tree as
expand found it or made by the node's own ancestors, which no concurrent
branch can change; or when, on a page the node shares, it is printed
above the node's heading or at and below the next node's. A heading not
found on its page decides nothing. A leading number is ignored only when
one of the two titles lacks it. The check runs before the empty-reply
retry, and the tree is read once per pass instead of once per node.

A summary reply counted as answered whenever it was non-empty, so a
model returning {"summary": ""} everywhere stored a document of fallback
summaries without the all-failed error. It now counts only when a
summary parses out of it.

post_processing ended a TOC item a page before its start when the next
item opened the same page; the end is now at least the start.
heading_at_page_start never places a heading without a Latin letter, so
a bare "2" (often a page number) shares its page; nothing tested that.
The add_intro_nodes idempotency check compared the tree with itself, the
same object the call returns, so it could not fail; it now compares a
second pass over a copy with the first.
The ownership filter ran inside the try that absorbs failed model calls,
so a bug in it would be logged as a model error, retried, and leave the
node silently unexpanded. It now runs after the call, where an error
surfaces. The neighbor test now also pins the retry: a reply the filter
empties is asked again. own_children drops a guard for lines expand
always passes.
A mutation pass over the filter found 13 of 21 mutants surviving the
suite. Each part now has a test that fails without it: the snapshot of
known nodes (Results' run-in heading leaves only the tree to recognize
its subsection), the lineage of nodes this pass made, the ancestors and
next node each new child is handed, filtering of cached headings, the
occurrence each anchor reads (last line of a repeated next heading,
last line of a child mentioned above), `>=` for a variant printed on
the next heading's own line, key_items for a same-page fusion, and the
clamp for a TOC item listed out of order.
…ading

normalize keeps only Latin letters and digits, so "1. 概要" reads as
"1" and "1.1 背景" as "1 1". same_heading took the bare "1" for an
unnumbered title and matched the pair: a CJK chapter dropped its first
subsection as already a node, a "1.2" heading on the page where "2."
opens was dropped the same way, and a "2.2" line passed for the next
node's "2." heading, so the next node's "2.1", printed above it, stayed.
Ignoring a leading number only one title prints now needs a Latin
letter to compare.

On a node's shared first page, the cut for "printed above the node's
heading" took the first line matching the node or any ancestor that
starts there. With the parent's heading printed above the node's, what
sits between (the parent's opening, or a sibling's subsection) was kept
as the node's child. The cut now reads the nearest heading found on the
page: the node's own, else its parent's, and so on up.
load_pages, the reader behind optimize_tree(pdf_path) and the
tree_optimize CLI, sorted each page's lines by height across columns.
On a two-column page a subsection low in the left column landed after
the next section's heading at the top of the right column, and expand's
line cuts dropped it: 29 of 694 subsections on generated two-column
papers. It now takes flash's own page text, read in layout order, so
both entry points see the same lines. pymupdf's raw stream order was
not enough: the 9/11 report draws a mid-page heading before the text
above it. Reading a 1,100-page book takes about 30 s (5 s before), and
the CLI no longer needs pymupdf.

On a page a node shares with the next one, the cut anchored only on the
next node's own heading and kept everything when that heading was not
found (an unprinted bookmark title, a heading split by extraction). It
now falls back to the heading of the next node's first descendant that
starts on that page. In Murphy's ML book "23.2.1 Using the cdf" no
longer hangs under "22.6.5".

process_large_node_recursively could split the same pages forever when
the model listed another heading above the section's own ("PART II"
above "Chapter 3"): the rebuilt copy was again a large leaf. A leaf
whose range equals the range last split above it now stays a leaf, as
in compute.

Local standard indexing asked page_index_main for node text that nothing
reads any more and that the store strips before saving. It no longer
builds it; the stored tree and every prompt are unchanged.

build_pdf in tests/conftest.py also takes a page as (x, y, size, text)
lines, so the two-column test runs without pymupdf.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-07T09:24:42.095256Z c74e192 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@rejojer
rejojer merged commit d57adcd into main Oct 7, 2026
16 checks passed
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.

1 participant