FE-1322: Add a Starlight site that renders the docs bundle - #9206
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
e2d210b to
86d0383
Compare
86d0383 to
cd9dcfb
Compare
cd9dcfb to
185c0c8
Compare
185c0c8 to
126ce85
Compare
PR SummaryLow Risk Overview
Reviewed by Cursor Bugbot for commit 61ef474. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
Adds a private Astro/Starlight app for rendering the generated Petrinaut architecture bundle.
Changes:
- Adds the Starlight site, navigation, branding, and bundle synchronization.
- Connects generation, synchronization, checking, development, and builds through Turborepo.
- Documents usage and adds required dependencies.
Reviewed changes
Copilot reviewed 13 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
AGENTS.md |
Documents browsing the architecture site. |
yarn.config.cjs |
Allows the app’s TypeScript dependency. |
yarn.lock |
Locks the new Astro/Starlight dependencies. |
apps/petrinaut-docs/.gitignore |
Ignores generated and build output. |
apps/petrinaut-docs/LICENSE-APACHE.md |
Adds the Apache license. |
apps/petrinaut-docs/LICENSE-MIT.md |
Adds the MIT license. |
apps/petrinaut-docs/LICENSE.md |
Declares dual licensing. |
apps/petrinaut-docs/README.md |
Documents operation and bundle contracts. |
apps/petrinaut-docs/astro.config.mjs |
Configures Starlight and manifest-driven navigation. |
apps/petrinaut-docs/package.json |
Defines app scripts and dependencies. |
apps/petrinaut-docs/public/favicon.ico |
Adds the site favicon. |
apps/petrinaut-docs/scripts/sync-bundle.mjs |
Copies bundle content into the app. |
apps/petrinaut-docs/src/assets/petrinaut-helmet.png |
Adds site branding. |
apps/petrinaut-docs/src/content.config.mjs |
Configures the Starlight docs collection. |
apps/petrinaut-docs/tsconfig.json |
Enables strict Astro type checking. |
apps/petrinaut-docs/turbo.json |
Connects bundle generation to app tasks. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
c34b7be to
61ef474
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/petrinaut-docs/scripts/sync-bundle.mjs:55
- The generator deliberately supports an absent authored-content directory, but in that state it removes
bundle/componentsand emits no replacement (libs/@local/petrinaut-arch-docs/src/cli.ts:69-76). This unconditional copy therefore makes the documented “no authored content” build fail withENOENT. Treat a missing components directory as an empty optional input (while still propagating other filesystem errors).
// Diagram components imported by authored pages. Copied as siblings of `docs/`
// because that is the layout the bundle's own relative imports assume.
await cp(`${bundleRoot}components`, `${contentRoot}components`, {
recursive: true,
});
apps/petrinaut-docs/astro.config.mjs:70
- The generator accepts authored pages recursively and preserves their directory in the slug (
libs/@local/petrinaut-arch-docs/src/content.ts:142,206). For a valid page such ascontent/guides/setup.mdxwithout a separateguidespage, this traversal never reachesguides/setup: the root call only selects pages whose parent is empty, and recursion only starts from actual page slugs. The page is consequently missing from navigation. Build groups for intermediate slug segments, or explicitly flatten parentless authored paths.
const itemsUnder = (parentSlug) =>
pages
.filter((page) => parentOf(page.slug) === parentSlug)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/petrinaut-docs/astro.config.mjs:121
- The PR says the deployment location is still to be chosen, but Astro's
sitesetting declares the final deployed origin and is used for generated canonical URLs. A build hosted at the documenteddemo.petrinaut.org/docscandidate would therefore advertisepetrinaut-docs.hash.devURLs. Remove this setting until deployment is settled, or derivesite(and anybase) from deployment-specific configuration.
site: "https://petrinaut-docs.hash.dev",
apps/petrinaut-docs/astro.config.mjs:23
- The navigation reads
manifest.jsondirectly from the generator bundle while the pages come from the copiedsrc/contentsnapshot. After regenerating the bundle without runningsync:bundle, the directdevcommand documented inREADME.md:14-15combines a new sidebar with old pages; deleting the bundle also makes that command fail instead of rendering the last synced content. Copy the manifest duringsync:bundleand read that copied file here so the sidebar and pages always come from the same snapshot.
const manifestPath = fileURLToPath(
new URL(
"../../libs/@local/petrinaut-arch-docs/bundle/manifest.json",
import.meta.url,
),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/petrinaut-docs/astro.config.mjs:140
- The manifest order is lost when sidebar entries cross group boundaries.
itemsUnderalways emits direct pages before implied groups here, and the top-level result similarly appends the Architecture group after every authored item. For example, the generator explicitly supports an authored page withsidebar_order: 2000sorting after generated reference pages (libs/@local/petrinaut-arch-docs/src/emit/mdx.ts:31-36), but this sidebar will still place it before Architecture; a nested guide with a lower order than a direct sibling is likewise placed after that sibling. Carry each group’s effective order into the constructed sidebar entries and sort all siblings together before returning them.
return [
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 0230207. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/petrinaut-docs/astro.config.mjs:162
- Appending every implied directory after all direct pages drops the manifest ordering. For example, the valid pages
guides/setupwith order 1 andintrowith order 10 produce “Intro” before the synthesized “Guides” group, despiteorderspecifying the reverse. Merge direct pages and implied groups into one ordered list (using the minimum descendant order for an implied group, with slug as the tie-breaker) before mapping them to sidebar items, and add this case as a regression test.
...implied.map((slug) => ({
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/petrinaut-docs/astro.config.mjs:44
buildSidebarcontains the app's core navigation logic, but no test asserts its output;astro buildonly proves the pages render, not that they are reachable. For example, the comments document a previous regression whereguides/setupbuilt successfully while being omitted from the sidebar. Please extract this pure function and add focused tests for nested implied groups, attached guides, and the authored-index fallback so those regressions fail CI.
const buildSidebar = () => {
The bundle is meant to be portable — the same artefact rendering locally, in hash.dev, or read as plain text by an agent. Nothing proved that. A generator with exactly one consumer, and no consumer at all in this case, drifts toward whatever that consumer happens to need. This is that consumer, and it deliberately owns no content. Everything it serves is synced from `bundle/`, and `src/content/` is git-ignored, so anything it renders had to survive the trip through a bundle a different host could equally well read. When something cannot be expressed portably, the site is where that shows up as a failure rather than as a quiet coupling. `sync:bundle` only copies. It used to spawn the generator itself with `spawnSync`, which put a cross-package build step inside a shell call Turborepo could not see, order or report on. The whole chain now resolves in the task graph: build / lint:tsc / dev → sync:bundle → @local/petrinaut-arch-docs#doc:architecture Running the sync without a bundle exits 1 naming the Turborepo command to use, rather than failing partway through a copy. `trailingSlash: "never"` is required, not stylistic: links between generated pages are relative and assume slugs map to URLs without a trailing slash. A host serving `/architecture/core/simulation/` has to rewrite them, and `manifest.json` carries every slug for exactly that. This is the largest part of the change by line count and almost all of it is `yarn.lock` — Astro, Starlight and sharp bring 311 packages. It is separated from the generator for that reason: none of it needs reviewing to judge whether the architecture model is right, and none of it ships to a URL yet. Where the site is deployed is still open, and worth settling against FE-1157 so there is one canonical location.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/petrinaut-docs/astro.config.mjs:44
buildSidebaris substantial custom navigation logic, but the only stated checks (astro checkandbuild) verify that the current manifest renders, not that every manifest page is present and correctly nested. For example, removing the implied-directory recursion would still build successfully while making a validguides/advanced/setuppage unreachable from the sidebar. Please extract this function into an importable module and add focused tests for generated/authored separation, attached guides, missing parent pages, deep implied directories, and ordering.
const buildSidebar = () => {
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |

🌟 What is the purpose of this PR?
Adds an Astro and Starlight site that renders the docs bundle from #9204 and #9205. It is the first thing that reads the bundle, so it is also the first check that the bundle can be rendered by a host that did not produce it.
The site holds no content. Everything it serves is copied from
bundle/, andsrc/content/is git-ignored.Third of three PRs, on top of #9205.
🔍 What does this change?
sync:bundleonly copies files. The chain above resolves entirely in the task graph, so Turborepo orders and reports each step. Running the sync with no bundle present exits 1 and names the Turborepo command to run, rather than failing part-way through a copy.trailingSlash: "never"is required rather than a preference. Links between generated pages are relative and assume slugs map to URLs with no trailing slash. A host serving/architecture/core/simulation/has to rewrite them, andmanifest.jsonlists every slug for that purpose.The sidebar is built from
manifest.jsonrather than Starlight frontmatter, so the nesting comes from the bundle rather than from anything specific to this site.🔗 Related links
hash.dev/docs/petrinaut. Worth settling before choosing where this deploys, so there is one URL rather than two.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
Adds a private app. Nothing is published.
📜 Does this require a change to the docs?
AGENTS.mdgains the command for browsing the docs, with a note that it has to run through Turborepo, since the package script alone skips regenerating the bundle.🕸️ Does this require a change to the Turbo Graph?
turbo.json's have been updated to reflect this@apps/petrinaut-docs#{build,lint:tsc,dev}→#sync:bundle→@local/petrinaut-arch-docs#doc:architecture.yarn.lock. Astro, Starlight andsharpadd 311 packages. That is why it is a separate PR: none of it affects whether the architecture model is correct.d2will need to be added to the Vercel install step, and should be settled against FE-1157 first.d2is probed, not required. If it is missing, the build warns, leaves out the images, and succeeds. A CI runner withoutd2would produce a site with no diagrams and still pass.starlight-llms-txtwas dropped. It renders MDX in a container with no React renderer, and itsexcludeoption is accepted but never passed to the/llms-full.txtroute. The site serves the bundle's ownarchitecture.mdandarchitecture.jsoninstead.$x_{t}$in MDX fails the build here, and on any host withoutremark-math, because{…}parses as a JSX expression. Authored pages have to avoid it.🐾 Next steps
🛡 What tests cover this?
No unit tests, since the app holds no logic. It is covered by
lint:tsc, which runsastro checkover the synced content and the diagram components, and bybuild. Both fail if the bundle cannot be rendered.❓ How to test this?
turbo run dev --filter @apps/petrinaut-docs # http://localhost:4321Open Architecture for the overview, then any layer, including a leaf such as
core.clipboard. Each page opens with a diagram of what it imports and what imports it.To watch the task graph order the work, delete the bundle first:
To check that formatting and generation do not fight over the bundle's files: