Skip to content

FE-1514: Highlight arch-docs changes against a base ref on preview builds - #9366

Merged
kube merged 11 commits into
mainfrom
claude/fe-1514-arch-docs-diff-highlighting
Aug 27, 2026
Merged

FE-1514: Highlight arch-docs changes against a base ref on preview builds#9366
kube merged 11 commits into
mainfrom
claude/fe-1514-arch-docs-diff-highlighting

Conversation

@kube

@kube kube commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Make a PR's architecture-docs preview show what the PR changed. On preview deployments, the generator builds the bundle twice — once for the branch, once for the base — and marks the differences. The base is the PR's target branch, so a stacked PR shows only its own delta; a PR against main is compared with main.

Screenshot 2026-08-27 at 17 08 40

A reviewer opening the preview gets:

  • sidebar badges on every page the branch added, changed, or removed, with roll-up counts on collapsed groups so a change deep in a subtree is visible from the top;
  • block-level markers inside changed pages: a green bar on added blocks, a blue bar on edited ones, removed content as a red collapsed block in place;
  • a struck-through entry for each removed page, linking to a stub that carries the removed source;
  • two linked chips in the header saying what is being compared: the deployed PR and its base, each with the commit built.

The diff is filtered so only a page's own changes flag it. Transitive drift — import counts and their ordering, file/line totals, sidebar positions, the depended-on-by list — is masked before comparison: an incoming edge flags the importing layer, not its target. A branch that touches no covered package flags zero pages.

Process

---
title: How a diff build compares the branch against its base
---
flowchart LR
    branch["branch checkout"] --> gen1["generator"]
    base["base ref (the PR's target branch)"] -. "git archive, or an<br/>anonymous sparse fetch" .-> tree["base source tree<br/>(scratch directory)"]
    tree --> gen2["generator<br/>(same version, same config)"]
    gen1 --> bb["branch bundle"]
    gen2 --> kb["base bundle<br/>(in memory only)"]
    bb --> cmp["normalize + compare<br/>mask counts, ordering, incoming edges;<br/>diff per block"]
    kb --> cmp
    cmp --> out["annotated bundle<br/>badges, block markers, tombstones"]
Loading

Caching

The built base side is cached under node_modules/.cache, which CI persists between builds. Entries are keyed in the file name by a hash of the generator's own inputs (sources, config, pinned dependencies), then the base commit — nothing names the commit being built, so every push against an unmoved base reuses the entry, and different generator versions keep separate entries.

Every clean build also seeds the cache with its own side. The CI cache is restored per branch with a fallback to the production deployment's, so a PR targeting main finds main's entry on its first build and computes no base at all. Source-URL prefixes (which carry the built commit) are masked per side; that is what makes an entry from an earlier build comparable.

Security

Cached pages are compiled as MDX, so an entry is trusted exactly as far as the build that wrote it. That crosses no new boundary: only this project's own builds write the directory, and they can already run arbitrary code (package scripts). The per-branch scoping means entries shared across branches are only ever written by protected-branch builds. A shared remote cache would change this analysis and is deliberately not used.

🔗 Related links

🔍 What does this change?

Generator (@local/petrinaut-arch-docs)

  • --diff-base <ref> / PETRINAUT_ARCH_DOCS_DIFF_BASE turn on diff mode. The base tree comes from the local clone via git archive, or — when the build has no usable clone, as on Vercel — from an anonymous blobless fetch of the public repository with a sparse checkout of only the covered directories (~3 s). The same generator, config, and source-URL prefix run over both trees, so nothing but real changes can differ.
  • src/diff/ classifies pages (added / changed / removed), computes an LCS over fence- and JSX-aware blocks, and rewrites changed pages with <DiffMarker> elements. Markers sit between blocks and CSS styles the next sibling, so no markdown is nested in JSX and headings/TOC behave as on a plain page.
  • Removed pages become tombstone stubs at their old slug, carrying the removed source collapsed.
  • manifest.json gains an optional diff section (base ref and SHA, slug → status); hosts without diff styling ignore it.
  • src/diff/base-cache.ts implements the cache described above; every clean build seeds it with its own side.
  • An unresolvable base degrades to a plain bundle with a warning — the diff decorates the build, it never gates it.

Docs site (@apps/petrinaut-docs)

  • astro.config.mjs turns manifest.diff into sidebar badges and group roll-up counts; chrome.css styles removed entries and the header chips (DiffBadges.astro).
  • Which ref to compare against is the deployment's decision, not the generator's: on previews, scripts/resolve-diff-base.mjs resolves the PR's target branch via the GitHub API (anonymously, falling back to main) and vercel-build.sh exports it. Production builds of main never diff.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR
    • a dedicated docs page, Previewing changes, explains the highlighting and how the comparison is built (with the same flow diagram, rendered from D2); the generator's README documents diff mode and its masking rules; the docs-site README documents the preview behaviour

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • affected the execution graph, and the turbo.json's have been updated to reflect this
    • doc:architecture declares the new environment variables it reads

⚠️ Known issues

  • A page flagged by a file-membership change alone (counts are masked, and the page never lists its files) shows a changed badge with no block marked.

🛡 What tests cover this?

❓ How to test this?

  1. yarn workspace @local/petrinaut-arch-docs doc:architecture --diff-base 'main~60', then turbo run build --filter '@apps/petrinaut-docs' and browse dist/ — or open the docs preview of a stacked PR (FE-1056: Move the handle integration guide into the architecture docs #8926 shows its two new pages badged new).
  2. Confirm the sidebar badges, the roll-up counts on collapsed groups, the block markers on a changed page, and the header chips naming the compared PRs.
  3. Build without the variable and confirm the site is unchanged from today.

🤖 Generated with Claude Code

@kube kube self-assigned this Aug 27, 2026
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 27, 2026 3:27pm
petrinaut-docs Ready Ready Preview Aug 27, 2026 3:27pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 27, 2026 3:27pm
petrinaut Skipped Skipped Aug 27, 2026 3:27pm

Request Review

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team area/apps labels Aug 27, 2026
Sort masked depends-on entries so count-driven reordering cannot flag a
page; scope every mask to the block shape it targets, keeping embedded
README prose untouched. Clean up the scratch tree on any extraction
failure and only swallow git-archive pathspec misses; refuse refs with a
leading dash. Warn on an empty --diff-base, drop dead custom properties,
and mark sidebar links with a data attribute instead of a second class.
…supply it

A Vercel build gets a snapshot of the sources with no usable git clone
behind it, so resolving the base ref locally fails and every preview
degraded to a plain bundle. When local resolution fails, fetch the ref
from the public repository into a scratch repo — blobless, with a
sparse checkout of only the covered directories — and log which
strategy ran. Verified against github.com from a directory with no
clone: 3s to materialize, and the base model matches a local build.
…base

Which ref to compare against is the deployment's decision, so the docs
app's CI script now resolves it: the PR's target branch via the GitHub
API, so a stacked PR shows only its own delta, falling back to main.
The generator stays ref-in, diff-out and never knows about PRs.

The built base side is cached under node_modules/.cache keyed by the
base commit and a hash of the generator's own inputs, so pushing to a
PR whose base has not moved skips the base build. Each side's
source-URL prefix is masked before comparison, which both removes
derived noise and makes a cached base from an earlier build comparable.
Two linked chips beside the title on diff builds: the deployed PR (or
branch) and the base it is diffed against, each with the commit built.
The base's PR is looked up by head branch, anonymously; the context is
resolved once in astro.config.mjs and injected as a compile-time
constant, since a bundled component has no stable path to the manifest.
Comment thread apps/petrinaut-docs/src/diff-context.ts Dismissed
Cache entries are keyed in the file name by the generator-inputs hash
then the base commit, so different generator versions keep separate
entries instead of overwriting one slot. Every clean build also stores
its own side as a future base: the CI cache falls back to the
production deployment's, so the entry a main build writes is what a PR
targeting main finds on its first build — the common case computes no
base at all, and entries shared across branches are only ever written
by protected-branch builds.
Base-side acquisition (cache lookup, materialization, build, seeding)
moves from cli.ts into diff/base-side.ts, leaving the CLI as wiring and
logging. Ref validation runs at every entry point that takes a ref, not
only in materialization, so no call order lets an unvalidated ref reach
a git command line; the credentialed origin-fetch fallback is removed —
local resolution either succeeds or the anonymous fetch takes over.
ls-remote resolution peels annotated tags to the commit the cache and
materialization both key on. The CI resolver's crash costs the
highlighting instead of the deploy, and it only prints branch-name
shaped output; branch names are URL-encoded in the header chips.
@vercel
vercel Bot temporarily deployed to Preview – petrinaut August 27, 2026 14:57 Inactive
The default stays node_modules/.cache, which CI providers persist
without configuration and local builds can always write; a CI whose
persisted location differs sets PETRINAUT_ARCH_DOCS_CACHE_DIR instead
of the code guessing its layout.
@cursor

cursor Bot commented Aug 27, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Adds CI git fetch, GitHub API calls, and MDX cache trust to the docs build path; diff failures are non-blocking but preview highlighting depends on network and correct base resolution.

Overview
PR previews of architecture docs now show what changed vs the merge base, not a full diff against main only. Vercel preview builds set PETRINAUT_ARCH_DOCS_DIFF_BASE (via resolve-diff-base.mjs → GitHub PR target branch, else main); production main builds never enable diff mode.

The @local/petrinaut-arch-docs generator gains --diff-base / PETRINAUT_ARCH_DOCS_DIFF_BASE: it builds the head bundle, materializes sources at the base ref (git archive or anonymous sparse fetch on Vercel), runs the same generator for the base side (with LRU cache keyed by generator inputs + base SHA), then compares pages with masked transitive noise (counts, ordering, incoming edges). Output adds optional manifest.diff, block-level <DiffMarker> annotations, and tombstone stubs for removed pages; failures degrade to an unhighlighted bundle.

@apps/petrinaut-docs reads manifest.diff for sidebar badges (including group roll-ups), styles removed links and block markers, and shows header compare chips (DiffBadges + diff-context.ts) injected from astro.config.mjs. Maintainer docs and READMEs document the flow.

Reviewed by Cursor Bugbot for commit 3a85f23. Bugbot is set up for automated code reviews on this repo. Configure here.

Two of these were invisible on GitHub: base-cache.ts carried raw NUL
bytes as hash separators, so git rendered it as a binary file — now the
escape spelling, with identical hash input — and the ref guard's
control-character class tripped oxlint's no-control-regex; it is now an
allow-list, which also subsumes the empty and leading-dash checks.

The prune could evict exactly the shared base entry: eviction now
orders by last use (a hit refreshes the timestamp), the cap rises to
16, and seeding runs after the diff has read its base. Cache keys hash
package-relative paths, so relocated checkouts sharing a cache
directory hit. Seeding is wrapped whole — no failure in a bonus write
may fail a passed build. Network git calls get a timeout and prompt
suppression; the marker stylesheet reads every hue through its custom
property so re-theming works as documented; empty Vercel variables no
longer produce empty header chips.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3a85f23. Configure here.

Comment thread libs/@local/petrinaut-arch-docs/src/diff/bundle-diff.ts
@kube
kube added this pull request to the merge queue Aug 27, 2026
Merged via the queue into main with commit c09e1ef Aug 27, 2026
49 checks passed
@kube
kube deleted the claude/fe-1514-arch-docs-diff-highlighting branch August 27, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

3 participants