Skip to content

fix: refuse incomplete anchor-prepass results - #493

Merged
entelecheia merged 2 commits into
mainfrom
fix/anchor-prepass-diagnostics
Nov 10, 2026
Merged

entelecheia merged 2 commits into
mainfrom
fix/anchor-prepass-diagnostics

Conversation

@entelecheia

Copy link
Copy Markdown
Member

Problem

layout::synthesized_body_anchors returned a flat Vec<BodyAnchor> whose entries were all
initialized to BodyAnchor::default(), and it threw away the private accumulator it ran the layout
with. A paragraph the layout never reached therefore carried the same value as a genuine page-top
anchor (v_pos = 0, starts_page = false), and lineseg::synthesize_linesegs had neither a
completion result nor an initialized-entry marker when it consumed those anchors for a floating-table
section.

Change

  • synthesized_body_anchors returns BodyAnchors: one Option<BodyAnchor> slot per paragraph plus a
    completion result. A slot stays None when the paragraph was not visited, and completion is false
    when a page or display-item budget refused a page transition anywhere in the pass. Both halves are
    needed: a floating-table continuation can be refused after every anchor was already recorded, so
    the slots alone cannot reveal that stop.
  • The prepass's diagnostics: only what can invalidate an anchor is propagated (the budget refusal, as
    layout_budget_exceeded). The prepass keeps its private accumulator, so it neither spends the
    caller's page/display-item budget nor reports the document's other warnings twice - the real layout
    pass still reports those once.
  • New RenderIssueCode::AnchorPrepassIncomplete (anchor_prepass_incomplete, severity incomplete,
    stage layout), mirrored into render-report-v1 and certification-report-v1.
  • synthesize_linesegs publishes anchors only for a section whose anchors are complete. Otherwise
    that section falls back to the base cursor path it had before floating-table anchors existed and
    reports the fallback once per section. A fallback, not a typed refusal: the base cursor is the
    pre-existing layout for those sections, so a hypothetical stop degrades positions instead of failing
    a render. Policy written up in docs/design/05-rendering.md §2.2.1 (and the Korean twin).

Ordinary inputs are unchanged: the prepass itself sets no page or display-item limit, so neither
completion flag can fire on a document today. The guard exists so the failure boundary is closed
before a limit is ever injected there, which is exactly what the tests do.

Tests

Four regressions in crates/hwp-render/src/lineseg.rs (anchor_prepass_tests), each paginating from
font-free flow geometry - a body paragraph advances 1600 HWPUNIT plus 300 of space-after, an empty
cell paragraph is one 1000-HWPUNIT line - with an isolated FontStore, so nothing depends on a host
face and no assertion counts pages or reads glyph metrics:

test stop assertion
complete_prepass_publishes_every_measured_anchor_and_its_page_start none every paragraph publishes its measured v_pos, and the page-top anchor's starts_page reaches LineSeg.flags bit 0
prepass_stop_during_paragraph_pagination_never_publishes_a_default_zero_anchor the third paragraph's first line opens page 2 fourth anchor is None, one anchor_prepass_incomplete plus the propagated layout_budget_exceeded, and the fourth paragraph's synthesized v_pos is not the default zero
prepass_stop_during_floating_table_continuation_still_reports_incomplete the three-row floating table needs its next page after both anchors were recorded every slot is Some yet completion is false, one warning, and the fallback does not apply a page-start flag it did not measure
prepass_stop_leaves_a_later_section_unvisited_and_reported section 0's pagination section 1's slots are None, it is still synthesized by the base cursor (nonzero v_pos), and only the floating section reports

Red-first check against the pre-fix behaviour (temporarily restoring the old consumption: anchors
published unconditionally, slot().unwrap_or_default()):

  • with the fallback warning removed, the three incomplete-pass tests fail at their diagnostic
    assertion (anchor_prepass_incomplete count 0 vs 1);
  • with the warning kept but the anchors still published, they fail at their behavioural assertion
    (default-zero v_pos, page-start bit set from an anchor the section was not allowed to use).

The complete-pass test passes on both, as it should.

Verification

== check: OK (fmt/clippy/test/crate-edges/pdf-runner/structured-corpus/claims/doc-surface/release-block/readiness-selfcheck/skip-accounting/public-parity=skipped) skipped-for-missing-fixtures=49 (optional=4) ==

Hancom open/export verification is a separate person-run evidence leg and was not performed here.

Closes #426.

Copilot AI balanced review requested due to automatic review settings November 10, 2026 13:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T13:38:54.069770Z fc76440 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@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: fc76440e0c

ℹ️ 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 crates/hwp-render/src/issues.rs Resolved
The floating-table body-anchor prepass returned a flat Vec<BodyAnchor> whose
entries all started at BodyAnchor::default(), so a paragraph the layout never
visited was indistinguishable from a legitimate page-top anchor (v_pos 0,
starts_page false), and the prepass's own budget diagnostics were discarded.

synthesized_body_anchors now returns BodyAnchors: one Option<BodyAnchor> slot
per paragraph plus a completion result. A slot stays None when the paragraph
was not visited, and completion is false when a page or display-item budget
refused a page transition anywhere in the pass, including a floating-table
continuation that stopped after every anchor had been recorded. The prepass
keeps its private accumulator, so it neither spends the caller's budget nor
repeats the document's warnings, and propagates only the diagnostics that
invalidate an anchor.

synthesize_linesegs publishes anchors only for a complete section. Otherwise
that section falls back to the base cursor path it had before floating-table
anchors existed and reports the new anchor_prepass_incomplete code once per
section. Falling back rather than refusing keeps a hypothetical stop a
position degradation instead of a failed render.

Regression tests cover a complete pass (including a page-top anchor and its
line flag), a stop during paragraph pagination, a stop during a floating-table
continuation, and an unvisited later section. Every fixture paginates from
font-free flow geometry, and the three incomplete-pass tests fail on the
pre-fix consumption path at their default-zero or page-start assertion.
The file is touched by #426, so AGENTS.md requires its development comments in
English. This is comment-only: the module doc, the three WMF code docs and the
two accumulator method docs, with the same meaning. User-facing Korean strings
in this crate are unchanged.
@entelecheia
entelecheia force-pushed the fix/anchor-prepass-diagnostics branch from be16dd2 to 6eb5914 Compare November 10, 2026 14:58
@entelecheia
entelecheia merged commit f3ca450 into main Nov 10, 2026
7 checks passed
@entelecheia
entelecheia deleted the fix/anchor-prepass-diagnostics branch November 10, 2026 15:20
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.

render: propagate anchor-prepass diagnostics and reject incomplete synthesized body anchors

2 participants