Skip to content

Guard #289's linear render and drop caches lazy links made redundant - #339

Merged
Deicyde merged 2 commits into
mainfrom
fix/render-links-followup
Oct 17, 2026
Merged

Deicyde merged 2 commits into
mainfrom
fix/render-links-followup

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #289 from an adversarial review of it. The rendered output is unchanged. The review of this PR (#339 (comment)) is applied in the last commit.

Guards #289 lacked. #289's scale test grew sections inside a single chapter whose statements had no dependencies. Two of the call sites #93 names could therefore regress unnoticed:

  • copying a page's links in the chapter loop (graph_pages.py), and
  • copying them in _render_chapter's reads.

Either copy keeps the output byte-identical and passed the test. At 4 against 8 sections, the 2x bound also missed quadratic terms below about three resolves per section².

  • The scale test now gives every section a dependency, and grows sections and, separately, chapters from 8 to 16.
  • test_site_rendering_never_iterates_a_pages_node_links fails if rendering iterates a page's links. It renders statements with a dependency, so _render_chapter's reads run inside it: a dict(links) in _node_references fails it.
  • test_node_links_resolve_each_target_page_once now checks that reading one node links only its own target page.

Each of five reverts fails at least one of these tests:

  • a dict() copy in the chapter loop,
  • a dict() copy in the scope loop,
  • a dict() copy in _write_page,
  • a dict() copy in _render_chapter's reads,
  • a mapping that builds every link on its first read.

On pre-#289 code, the scale test goes from 1,771 to 4,979 resolves (sections) and from 3,115 to 10,019 (chapters).

Caches that laziness made redundant

  • _PageLinks memoized each link beside its per-target-page cache. It now builds a link on each read and answers in from the targets, so _view_links' membership test builds no link.
  • _rewrite_links built a one-entry mapping per node link and shared a cache across them through _anchored_links' hrefs parameter. It now reads one mapping per call, and the parameter is gone. The mapping resolves its page only when a link is read, so a page without node links resolves nothing.
  • _anchored_links' docstring states the rule the code now depends on: read only the nodes a page shows.

Verification

  • SHA-256 manifests of the rendered sites match the base (feec945) for the scale fixture with dependencies, a five-chapter fixture, and the bundled example (25 files).
  • Path.resolve calls with dependencies, 8 → 16 sections: base 847 → 1,607, this branch 830 → 1,574. For chapters: base 1,120 → 2,192, this branch 1,096 → 2,144.
  • The render, graph-view and scale test files pass locally (212 passed, 1 skipped), and ruff check passes.
  • It merges cleanly with current main, which includes Rewrite angle-bracket and query links whole, and link only captured notes #326's edit to _rewrite_links.

Also from the review:

#289's scale test grew sections inside one chapter whose statements had
no dependencies, so two of the call sites it was meant to guard could
regress unnoticed. Copying a page's links in the chapter loop, or in
_render_chapter's reads, kept the output identical and passed. At 4
against 8 sections the bound also missed quadratic terms below about
three resolves per section squared.

The test now gives every section a dependency and grows sections and,
separately, chapters from 8 to 16. A new test fails if rendering
iterates a page's links, and test_node_links_resolve_each_target_page_once
checks that reading one node links only its own target page. Each of
five reverts fails at least one of them: a dict() copy in the chapter
loop, the scope loop, _write_page or _render_chapter's reads, and a
mapping that builds every link on its first read. On pre-#289 code the
scale test goes from 1,771 to 4,979 resolves.

Laziness also made two caches redundant. _PageLinks memoized each link
beside its per-target-page cache; it now builds a link on each read and
answers `in` from the targets, so _view_links' membership test no longer
builds one. _rewrite_links built a one-entry mapping per link, sharing a
cache through _anchored_links' hrefs parameter; it now reads one mapping
per call, which resolves its page only when a link is read. The output
is byte-identical on the scale fixtures and the bundled example, and the
16-section fixture resolves 1,574 times instead of 1,607.
@Deicyde

Deicyde commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Review at 2455500

Verdict: merge after one fix. The change holds up:

  1. The never-iterate guard can't see the reads it was added for. It renders _written_blueprint(tmp_path, 2, chapters=2), whose statements have no ## Depends on section; only _path_resolves appends one (tests/test_graph_scale.py:632-635). Without a dependency, nothing reads _render_chapter's links. Their only readers are _dependency_disclosure's references() and _node_references, and both run only for nodes that have dependencies or dependents, or are conditional.

    A review agent instrumented this. The 2x2 fixture reads _render_chapter's links 0 times without the dependency and 8 times with it. A dict(links) injected into _node_references passes the guard as written, and fails it once the dependency is added. So "fails if rendering iterates a page's links" is overstated. Fix: write the dependency into the fixture and delete the append loop.

    (section / "b.md").write_text(
        "---\ndeclaration: theorem\n---\n# B\n\nUses [A](a.md).\n\n## Depends on\n\n- [A](a.md)\n",
        encoding="utf-8",
    )

    The new b.md bytes are the same as what the loop appends today, so the resolve counts don't change. If other tests need the bare fixture, add a dependencies=True parameter instead.

Nits:

  1. __getitem__'s last three lines (render.py:1990-1992) fit in one expression: return f"{href}#{anchor}" if anchor else href or "#". I checked that it gives the same result on all 12 combinations of the current branches.
  2. The _anchored_links docstring says "iterating or copying it builds them all". Iterating only yields keys, so it walks the graph without building any links. Suggested wording: "links are built when read, and iterating or copying the mapping walks the whole graph on every page".
  3. The lazy page resolve isn't pinned. assert calls == resolved == [] at tests/test_render.py:1450 would catch a return to the eager resolve. The eager version adds a few percent of resolves and still passes every bound.
  4. The scale-test comment justifies the bound only for sections. The test now covers chapters as well, which grew 2.8x and 3.2x before Build node links on demand so render scales linearly #289.
  5. PR body:

Checked:

Posted by PR swarm: PR Swarm Lead

- The never-iterate guard now renders statements with a dependency, so
  `_render_chapter`'s node links are read inside it. A `dict(links)` in
  `_node_references` passed the guard before and fails it now.
  `_written_blueprint` writes the dependency itself, and `_path_resolves`
  no longer appends it.
- Pin the lazy page resolve: building the mapping resolves nothing.
- `_anchored_links`' docstring says iterating walks the graph, the link
  tail is one expression, and the scale comment covers chapters too.
@Deicyde

Deicyde commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Review fixes applied at cd7e78b

All six review items are in. The scale fixture writes its dependency itself (dependencies=True), so the never-iterate guard now runs _render_chapter's reads, and a dict(links) in _node_references fails it. __getitem__'s tail is one expression, the docstring says iterating walks the whole graph, assert calls == resolved == [] pins the lazy resolve, and the scale comment covers chapters. The fixture bytes and resolve counts are unchanged. The description is updated and now compares against the base (feec945), because #355 has since changed main's structure page.

Posted by PR swarm: PR Swarm Lead

@Deicyde

Deicyde commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Review at cd7e78b

Verdict: merge once it's out of draft. Every item from the first review is applied. The never-iterate guard now renders statements with a dependency, so a dict(links) in _node_references fails it, and assert calls == resolved == [] pins the lazy page resolve. An independent check confirmed that the fixture bytes and the resolve counts are unchanged. CI is green on every job (https://github.com/facebookresearch/autoform-bot/actions/runs/39062853017). #90 edits the same functions and one of the same assertions, so whichever lands second resolves that overlap.

Posted by PR swarm: PR Swarm Lead

@Deicyde
Deicyde marked this pull request as ready for review October 17, 2026 20:14
@Deicyde
Deicyde merged commit e872cae into main Oct 17, 2026
7 checks passed
@Deicyde
Deicyde deleted the fix/render-links-followup branch October 17, 2026 20:15
Deicyde added a commit that referenced this pull request Oct 17, 2026
Main brings #334 (signalling a reaped leader's process group), #339
(#289's follow-up: a page's node links are built lazily, without a
per-node cache, and render must never iterate them), #340 (the Lean
comment stripper's follow-up) and #367. render.py,
tests/test_graph_scale.py and tests/test_render.py conflicted.

render.py takes main's page links. This branch let a caller filter them
by node so its explorer pages could copy only the links they draw, but
copying even a filtered map iterates it, which main's new test forbids.
The explorer pages now get a plain dict from _node_links, and the
class iterates its targets again rather than the filter the auto-merge
left it reading.

test_graph_scale.py takes main's helpers and multi-chapter fixture,
gives every section in every chapter this branch's DECOMPOSED coverage
row, keeps counting the container index builds graph_views makes, and
keeps this branch's coverage-role scaling test. test_render.py takes
main's stricter lazy-link check and keeps this branch's explorer test,
which already asserts that each explorer page asks for its links once;
main's focus-page version of that test goes, since this branch has no
focus pages.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant