FE-1514: Highlight arch-docs changes against a base ref on preview builds - #9366
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
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.
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.
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.
PR SummaryMedium Risk Overview The
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.

🌟 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
mainis compared withmain.A reviewer opening the preview gets:
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"]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
mainfindsmain'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_BASEturn on diff mode. The base tree comes from the local clone viagit 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.manifest.jsongains an optionaldiffsection (base ref and SHA, slug → status); hosts without diff styling ignore it.src/diff/base-cache.tsimplements the cache described above; every clean build seeds it with its own side.Docs site (
@apps/petrinaut-docs)astro.config.mjsturnsmanifest.diffinto sidebar badges and group roll-up counts;chrome.cssstyles removed entries and the header chips (DiffBadges.astro).scripts/resolve-diff-base.mjsresolves the PR's target branch via the GitHub API (anonymously, falling back tomain) andvercel-build.shexports it. Production builds ofmainnever diff.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
turbo.json's have been updated to reflect thisdoc:architecturedeclares the new environment variables it readschangedbadge with no block marked.🛡 What tests cover this?
resolve-diff-base.mjsresolves FE-1056: Move the handle integration guide into the architecture docs #8926 to its stack branch and FE-1514: Highlight arch-docs changes against a base ref on preview builds #9366 tomainagainst the live API.❓ How to test this?
yarn workspace @local/petrinaut-arch-docs doc:architecture --diff-base 'main~60', thenturbo run build --filter '@apps/petrinaut-docs'and browsedist/— 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 badgednew).changedpage, and the header chips naming the compared PRs.🤖 Generated with Claude Code