Skip to content

fix(policies): keep PDF headings with their section (CS-704)#3358

Merged
tofikwest merged 6 commits into
mainfrom
tofik/cs-704-pdf-heading-keep-together
Jul 6, 2026
Merged

fix(policies): keep PDF headings with their section (CS-704)#3358
tofikwest merged 6 commits into
mainfrom
tofik/cs-704-pdf-heading-keep-together

Conversation

@tofikwest

@tofikwest tofikwest commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What & why

Fixes CS-704 — policy PDF exports split sections across pages: a section heading landed at the bottom of a page while its body flowed to the next page (orphaned heading). Reported for both single-policy and full-pack exports.

Root cause: both PDF generators computed the heading page-break wrong. They only guaranteed the heading (or a fixed slab) fit before rendering it — never the heading plus the first lines of its section — so a heading could render on the last usable line of a page and its body would break to the next page.

Fix

Before rendering a heading, reserve room for the heading plus the first few lines of the following section (HEADING_KEEP_WITH_LINES = 3). If it doesn't fit, the whole heading is pushed to the next page. The reserve is tuned to each generator's real cursor advancement:

  • App (apps/app/src/lib/pdf-generator.ts) — powers the single-policy in-app export (the fallback used whenever a policy has no manually-uploaded PDF). Per-line body checks use lineHeight, so the reserve is headingHeight + spacingAfter + keepWithLines·lineHeight.
  • API (apps/api/src/trust-portal/policy-pdf-renderer.service.ts) — powers the full policy-pack export (in-app and the Trust Portal) via renderPoliciesPdfBuffer. Its checkPageBreak uses a 20mm (DEFAULT_BREAK_SPACE) look-ahead per body line, so the reserve adds one such look-ahead.

These two files carry explicit "keep in sync" markers, so both were updated together.

Additional correctness improvements:

  • The reserve only applies when content actually follows the heading, so a trailing heading (last node) isn't bumped to a lonely page.
  • Heading lines now render without per-line page-break checks (the up-front check already guarantees they fit), so a multi-line heading can no longer be split across pages.

Out of scope: the ticket's soft "Consider basic orphan/widow control" bullet. The reported, reproducible defect is stranded headings; aggressive block-level widow control would repaginate every existing policy and risks new layout regressions, so it's intentionally deferred.

Tests

apps/api/src/trust-portal/policy-pdf-renderer.service.spec.ts:

  • Sweep test — renders 30 sections at growing vertical offsets and asserts no heading is separated from the first line of its section. Verified it fails without the fix (headings orphaned) and passes with it.
  • Trailing-heading guard — a heading as the last node stays on the current page.

cd apps/api && npx jest src/trust-portal/policy-pdf-renderer.service.spec.ts22 passed. Typecheck of both touched files is clean (repo has an unrelated pre-existing RED baseline).

The app generator applies the identical algorithm (proven by the API spec); it isn't separately unit-tested because it's a browser module that triggers a doc.save() download and doesn't expose the buffer — testing it would need a refactor out of scope for this fix.

🤖 Generated with Claude Code


Summary by cubic

Keeps section headings with the start of their section in policy PDF exports (CS-704), including cases with tables and oversized headings. Works for both single‑policy and full‑pack PDFs.

  • Bug Fixes
    • Add keep‑together rule (HEADING_KEEP_WITH_LINES = 3): only render a heading if it and the next 3 body lines fit; otherwise move it to the next page (matches the Linear ask).
    • Measure what follows via sectionLeadHeight: skips empty paragraphs/hard breaks, handles tables by using the first row’s height, and only reserves space when visible content follows (trailing headings stay on the current page).
    • Reserve space per generator: app uses headingHeight + spacingAfter + keepWithLines·lineHeight; API adds a DEFAULT_BREAK_SPACE look‑ahead. The reserve is capped to the usable page height to avoid blank leading pages for oversized headings. Applied to both generators.
    • API safety net: per‑line checks paginate pathological multi‑page headings; normal headings remain intact.
    • Tests cover boundary sweeps (no orphaned headings), heading‑before‑tall‑table, trailing/empty‑node cases, blank‑leading‑page guard, and long‑heading pagination.

Written for commit 439a453. Summary will update on new commits.

Review in cubic

Policy PDF exports orphaned section headings at the bottom of a page while
the body flowed to the next page. Both shared generators only checked that
the heading line itself fit before rendering it — there was no keep-together
logic, so a heading could land on the last line of a page.

Before rendering a heading, reserve room for the heading PLUS the first few
lines of the following section (HEADING_KEEP_WITH_LINES); if that doesn't fit,
push the whole heading to the next page. The reserve accounts for each
generator's actual cursor advancement (the app's per-line lineHeight checks
vs. the API renderer's DEFAULT_BREAK_SPACE look-ahead) and only applies when
content actually follows the heading, so a trailing heading isn't bumped to a
page of its own. Heading lines now also render without per-line page-break
checks, so a multi-line heading can no longer be split across pages.

Applied to both:
- apps/app/src/lib/pdf-generator.ts (single-policy in-app export)
- apps/api/src/trust-portal/policy-pdf-renderer.service.ts (full policy-pack
  export in-app and via the Trust Portal)

Adds a regression test that sweeps headings across page boundaries and asserts
none are separated from the first line of their section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013q6YNeuzKbUdX4yZ3yTnZk
@linear

linear Bot commented Jul 6, 2026

Copy link
Copy Markdown

CS-704

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jul 6, 2026 9:08pm
comp-framework-editor Ready Ready Preview, Comment Jul 6, 2026 9:08pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
portal Skipped Skipped Jul 6, 2026 9:08pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot 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.

cubic analysis

2 issues found across 3 files

Confidence score: 3/5

  • In apps/api/src/trust-portal/policy-pdf-renderer.service.ts, the hasFollowingContent check appears to treat trailing empty paragraphs/hard breaks as real content, so headings may reserve extra keep-together space unnecessarily and force avoidable page breaks or layout gaps in generated PDFs — tighten the guard to ignore empty trailing nodes before merging.
  • In apps/api/src/trust-portal/policy-pdf-renderer.service.ts, the heading rendering loop no longer does per-line page-break checks, so very long wrapped headings can run past the bottom margin and produce clipped/overflowed output — restore per-line pagination checks (or equivalent overflow handling) before merging.

Linked issue analysis

Linked issue: CS-704: create a ticket in team CS "[Bug] - Policy PDF export splits sections across multiple pages

Status Acceptance criteria Notes
Headings are not orphaned at page bottoms — a heading stays on the same page as the first lines of its section The PR adds explicit keep‑together checks before rendering headings so the heading plus following lines fit or the heading is pushed to the next page. The API tests include a sweep that fails before the fix and passes after it.
Fix applied to both single-policy (app) and full-pack (API) exporters Both the app generator and the API renderer were updated so the same keep‑together behavior applies across exports.
Reserve logic implemented: require heading + first N lines (HEADING_KEEP_WITH_LINES = 3) with generator-specific measurement PR introduces HEADING_KEEP_WITH_LINES and uses generator-specific calculations (lineHeight for app; DEFAULT_BREAK_SPACE look‑ahead for API) when computing required space before rendering a heading.
Trailing headings are not pushed to their own page when no content follows The implementation only reserves following-section space when content actually follows a heading, and there is a regression test to ensure a trailing heading stays on the current page.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trust-portal/policy-pdf-renderer.service.ts
Comment thread apps/api/src/trust-portal/policy-pdf-renderer.service.ts Outdated
Two issues raised by cubic on the CS-704 keep-together change:

1. hasFollowingContent only checked array position, so a heading followed
   only by empty paragraphs or hard breaks (common trailing nodes in TipTap
   docs) still reserved keep-together space and could be bumped to a lonely
   page — defeating the trailing-heading guard. Now checks whether any
   following sibling renders visible text (hasRenderableTextAfter). Applied to
   both the app and API generators.

2. The API heading render loop dropped per-line page-break checks, so a
   pathological heading wrapping past a full page would overflow the bottom
   margin instead of paginating. Restored a per-line check as a safety net;
   the up-front keep-together reserve means normal headings never trip it, so
   they still can't be split across pages.

Adds regression tests for both (verified failing without the fixes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013q6YNeuzKbUdX4yZ3yTnZk
@vercel vercel Bot temporarily deployed to Preview – portal July 6, 2026 20:47 Inactive
@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

cubic analysis

2 issues found across 3 files

Confidence score: 3/5

  • In apps/api/src/trust-portal/policy-pdf-renderer.service.ts, heading pagination can fail when requiredHeight exceeds usable page height, since checkPageBreak only advances one page and never clamps; very tall wrapped headings can produce blank/invalid layout pages for users — clamp/split oversized heading blocks (or loop page-break checks) before merging.
  • In apps/api/src/trust-portal/policy-pdf-renderer.service.ts, the fixed keep-together reserve only protects headings followed by plain paragraphs, so headings before other block types can still orphan, which conflicts with CS-704 and can degrade document readability — apply keep-with-next logic consistently across block types and add a regression test for mixed content.

Linked issue analysis

Linked issue: CS-704: create a ticket in team CS "[Bug] - Policy PDF export splits sections across multiple pages

Status Acceptance criteria Notes
Keep headings with the first few lines of their section (add keep‑together logic so headings are not orphaned at page bottoms). PR adds an explicit keep‑together reserve (HEADING_KEEP_WITH_LINES = 3) and uses it when deciding page breaks, ensuring a heading is pushed to the next page if its following lines don't fit.
Apply the fix to both single‑policy (in‑app) and full‑pack (API/Trust Portal) PDF generators. Both the app generator and the API renderer were updated with matching keep‑together logic and in‑code sync comments.
Do not push a trailing heading (a heading with no following visible content) onto its own page. The implementation checks whether visible content actually follows a heading before reserving keep‑together space; tests assert trailing headings stay on the same page.
Handle multi‑line or pathological headings safely (don't silently overflow; paginate long headings rather than let them overflow or split normal headings across pages). Headings are measured up‑front so normal multi‑line headings are kept intact; per‑line checks act as a safety net so extremely long headings paginate correctly. A test verifies a very long heading spans multiple pages rather than overflowing.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trust-portal/policy-pdf-renderer.service.ts
Comment thread apps/api/src/trust-portal/policy-pdf-renderer.service.ts
@vercel vercel Bot temporarily deployed to Preview – portal July 6, 2026 21:02 Inactive
…bic)

Second cubic pass on the CS-704 change raised two more edge cases:

1. The keep-together reserve was a fixed text-only slab, so a heading
   immediately followed by a table with a tall first row (or by leading hard
   breaks) could still be orphaned — the table's own per-row page-break fires
   after the heading already committed. Replaced the position/text check with
   sectionLeadHeight(), which measures what the following section actually
   needs: a few text lines, a table's first-row height, or 0 for a trailing
   heading (skipping empty paragraphs and hard breaks).

2. An oversized heading (wrapping past a full page) could emit a blank leading
   page: requiredHeight exceeded the usable page height, so the up-front
   checkPageBreak added a page while the current one was still empty. Capped
   requiredHeight to the usable page height; the per-line loop paginates the
   heading across pages instead.

Applied to both generators (app + API). Adds regression tests for a
heading-before-tall-table and the blank-page guard (both verified failing
without the fixes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013q6YNeuzKbUdX4yZ3yTnZk
@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

cubic analysis

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Linked issue analysis

Linked issue: CS-704: create a ticket in team CS "[Bug] - Policy PDF export splits sections across multiple pages

Status Acceptance criteria Notes
Add keep‑together logic so a heading is only rendered if it plus the first few lines of its section fit on the page (HEADING_KEEP_WITH_LINES = 3). Both generators introduce HEADING_KEEP_WITH_LINES and compute a heading reserve before rendering.
Prevent orphaned headings — headings must share a page with the first line(s) of their section. Heading rendering now reserves space for the heading plus section lead and the API tests assert no heading is orphaned across many offsets.
Apply the fix to both single‑policy (app) and full‑pack / API generators. Both the app module and the API renderer were updated with matching keep‑together logic and notes to keep them in sync.
Do not push a trailing heading (no visible following content) onto a fresh page. The code only reserves lead space when visible content follows; there is a unit test covering trailing headings.
Handle tables: reserve the table first‑row height so a heading followed by a tall table row is not orphaned. The renderer measures a table's first row and includes it in the reserve; test ensures headings stay with table first rows.
Avoid inserting a blank leading page for an oversized heading (cap the reserve to usable page height). Required reserve is capped to usable page height; test asserts the first page carries the oversized heading rather than a blank page before it.
Safety net: very long headings paginate rather than overflow the page. Per‑line checks remain for pathological multi‑page headings; test verifies a very long heading spans pages.

Re-trigger cubic

@tofikwest tofikwest merged commit 5bac5c9 into main Jul 6, 2026
10 checks passed
@tofikwest tofikwest deleted the tofik/cs-704-pdf-heading-keep-together branch July 6, 2026 21:16
claudfuen pushed a commit that referenced this pull request Jul 6, 2026
## [3.98.1](v3.98.0...v3.98.1) (2026-07-06)

### Bug Fixes

* **policies:** keep PDF headings with their section (CS-704) ([#3358](#3358)) ([5bac5c9](5bac5c9))
* **risk-treatment:** rank tasks by exact in-process cosine, not filtered ANN ([#3359](#3359)) ([6293aad](6293aad))
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.98.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants