Skip to content

FE-1322: Generate Petrinaut architecture docs from in-code annotations - #9165

Draft
kube wants to merge 10 commits into
mainfrom
cf/fe-1322-generate-petrinaut-architecture-docs-from-in-code
Draft

FE-1322: Generate Petrinaut architecture docs from in-code annotations#9165
kube wants to merge 10 commits into
mainfrom
cf/fe-1322-generate-petrinaut-architecture-docs-from-in-code

Conversation

@kube

@kube kube commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Petrinaut's architecture documentation had no mechanism keeping it true. This makes the architecture something you declare next to the code it describes, generates the docs from those declarations, and fails CI when a declaration stops matching reality.

The output is a portable bundle, not a website — so the same artefact renders locally, embeds into hash.dev/docs/petrinaut, or gets handed to an AI agent.

flowchart LR
  subgraph source["Petrinaut source — versioned"]
    A["Folder README.md frontmatter<br/><i>layer · role · seams · boundaries</i>"]
    B["Doc-comment tags<br/><i>@layerRoot @boundary<br/>@invariant @seam</i>"]
  end

  subgraph content["content/ — versioned, optional"]
    C["Authored MDX<br/><i>attachTo: core.simulation</i>"]
    D["Diagram components<br/><i>.tsx</i>"]
  end

  E{{"Extractor<br/>+ dependency-cruiser"}}

  subgraph bundle["bundle/ — build output, git-ignored"]
    F["architecture.json · architecture.md · llms.txt<br/>manifest.json · pages/**.mdx · diagrams · components"]
  end

  G["Starlight site<br/><i>apps/petrinaut-docs</i>"]
  H["hash.dev"]
  I["AI agents"]

  A --> E
  B --> E
  E -->|"real import graph<br/>+ layer model"| F
  C --> F
  D --> F
  F --> G
  F --> H
  F --> I
Loading

🔍 What does this change?

1. The generated part reads annotations automatically

Nothing is registered in a central file. A build walks the packages, reads two kinds of annotation, and resolves the model:

Folder README.md frontmatter declares a layer — and the prose below it becomes that layer's page, so folder READMEs that already explained themselves became architecture pages for free:

---
layer: core.simulation.monte-carlo
role: Runs many simulations with bounded frame memory
seams: ["@hashintel/petrinaut-core/workers/monte-carlo"]
boundaries:
  - kind: worker
    note: Frame buffers stay inside the worker
---

Doc-comment tags attach facts to the specific code that upholds them, picked up from any block comment in any file:

/**
 * @layerRoot core.lsp            declares a layer from an entry file
 * @role Language-server client for editing user code
 * @boundary thread — requests reach the server over a worker transport
 * @invariant Two reusable frame buffers per run; no per-frame allocation
 * @seam @hashintel/petrinaut-core/workers/lsp
 */

Files with no annotation inherit from the nearest declaring ancestor. That is what keeps this proportional to the architecture rather than to the file count: ~40 declarations cover 412 files. Layer sizes, dependency edges and boundary tables are then derived from the real TypeScript import graph.

Result: 37 layers, 177 edges, 15 boundaries, 25 invariants — none of it hand-maintained.

2. Hand-written content merges into the generated tree

content/ is entirely optional (the system works with the directory absent), and pages there carry the reasoning an import graph cannot express.

A page names the layer it explains and moves inside the generated tree, rather than sitting in a separate section:

---
title: Memory model
attachTo: core.simulation   # a layer declared in the source
---

The layer's page gains a Guides section, and the sidebar nests the guide beside that layer's sub-layers.

Because attachTo decides where a page ends up, pages link by name and the path is computed at emit time — [text](layer:core.simulation.engine), [text](doc:simulation/memory-model). Unresolved targets fail the build rather than 404ing later.

Authored pages may also import diagram components from the bundle (import { ByteMap } from "@diagrams/byte-map") — lane diagrams, pipelines, the frame byte map, message sequences.

3. CI enforces the claims

lint:arch-docs fails on: an unannotated source file, a layer id implying an undeclared ancestor, a duplicate declaration, a malformed tag, a @seam that is no longer a real export, an attachTo or link target that does not resolve, and any dependency violating a rule in architecture.config.ts.

Three rules are enforced today. The substantive one — react must not depend on ui — already held (0 imports against 251 the other way), so it locks in an existing property.

4. What this replaces

  • petrinaut-core/scripts/generate-dependency-diagrams.mjs held the architecture as ~180 lines of if (path.startsWith(...)) far from the code, with a fallback that silently mis-bucketed anything renamed. It also hard-coded 7 of petrinaut-core's 10 entry points, so imports through ./ai, ./optimization and ./compiled-model were absent from the diagrams entirely. Entry points now derive from each package's exports.
  • The 3,100 lines of hand-written HTML in docs/architecture/ are migrated to authored MDX. Doing so surfaced two stale claims: UUID support described as unimplemented (it is fully implemented), and a sandbox description thinner than the current implementation.

🔗 Related links

  • FE-1322 (this PR)
  • FE-1157 — moving Petrinaut docs to hash.dev/docs/petrinaut. This produces the exportable artefact that work needs; it publishes nothing itself.
  • FE-1139 — the ticket that produced the HTML pages migrated here.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

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

@hashintel/petrinaut and petrinaut-core are touched only by comments, READMEs, and removal of a private script and its dependency-cruiser devDependency. No runtime code, types or exports change. Happy to add a patch changeset if reviewers would rather the README changes ship.

📜 Does this require a change to the docs?

  • require changes to docs which are made as part of this PR

The user-facing guide (libs/@hashintel/petrinaut/docs/) is untouched — no UI or behaviour changed. AGENTS.md gains a section on declaring layers and what CI enforces.

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

  • affected the execution graph, and the turbo.json's have been updated to reflect this

Adds doc:architecture / lint:arch-docs on the generator, and sync:bundlebuild / lint:tsc on the docs app. Removes doc:dependency-diagram from petrinaut-core.

⚠️ Known issues

  • Three packages are not covered. petrinaut-cli, petrinaut-website and petrinaut-opt have no declarations. A TypeScript package is a config entry plus one root declaration; the Python app needs docstring extraction, which is not written.
  • Authored pages need a React-capable MDX pipeline in any host, because of the diagram components. Generated pages stay plain CommonMark, and architecture.md — the artefact for agents — has no components.
  • starlight-llms-txt was dropped, not worked around: it renders MDX in a container with no React renderer, and its exclude option is accepted but never passed to the /llms-full.txt route. The site serves the bundle's own architecture.md and architecture.json instead.
  • The docs site is local-only. Nothing deploys it yet — see next steps.
  • mise run fix:package-json could not run locally (needs a nightly Cargo feature), so package.json key ordering was verified by reading the sorter's field list.

🐾 Next steps

  • Decide where the site is deployed. demo.petrinaut.org/docs is feasible — the SPA has no catch-all rewrite, and an Astro base: "/docs" build was tested — but it needs d2 added to the Vercel install step, and it is worth settling against FE-1157 first so there is one canonical URL.
  • Cover the remaining three packages.

🛡 What tests cover this?

58 tests in @local/petrinaut-arch-docs:

  • tags.test.ts — tag grammar: separators, multi-line continuation, duplicates, typo suggestions, and that a tag named in prose is not a declaration.
  • frontmatter.test.ts — declarations, defaults, malformed YAML, half-written declarations, CRLF.
  • extract.test.ts — inheritance through undeclared folders, @layer overrides, uncovered files, stable ordering.
  • check.test.ts — each CI check in both directions: fires when broken, silent when not.
  • emit/mdx.test.ts — link resolution at varying depths, fragments, unresolved targets.

Existing suites unaffected: 842 (petrinaut-core), 187 (petrinaut).

❓ How to test this?

yarn workspace @apps/petrinaut-docs dev   # http://localhost:4321

Confirm the checks hold, and hold in either order — formatting and generation both used to claim the bundle's files:

yarn fix:format && yarn workspace @local/petrinaut-arch-docs doc:architecture
yarn lint:format                                          # expect 0
yarn workspace @local/petrinaut-arch-docs lint:arch-docs  # expect 0

Then break something and confirm it is caught: change a role: in any layer-declaring README, point an attachTo at a layer that does not exist, or reference layer:core.nonexistent — each fails lint:arch-docs with the offending file named.

For the AI-facing side, read libs/@local/petrinaut-arch-docs/bundle/architecture.md — the whole architecture in one file.

kube added 4 commits August 5, 2026 17:40
Architecture docs rot because nothing fails when they stop being true.
`@local/petrinaut-arch-docs` extracts the architecture from annotations
that live next to the code they describe, and CI fails when a declaration
stops matching reality.

Two inputs, each in its natural home:

- Folder `README.md` frontmatter declares a layer, and the prose below it
  becomes that layer's page — folder docs that already exist turn into
  architecture pages for free.
- `@boundary`, `@invariant` and `@seam` doc-comment tags attach facts to
  the specific code that upholds them.

Files with no annotation inherit from the nearest declaring ancestor, so a
few dozen declarations cover several hundred files.

The output is a portable bundle rather than a site: `architecture.json`
(the model), `architecture.md` (the whole architecture in one file, which
is the cheapest read for an agent), `llms.txt`, `manifest.json` (a page
tree so a host can build navigation without crawling), MDX pages, and D2
diagram sources. Generated MDX is YAML frontmatter plus plain CommonMark
with no JSX, which is what lets one bundle render in Astro, in hash.dev's
Next.js MDX pipeline, and as plain text.

`lint:arch-docs` runs in CI and fails on: a source file no declaration
covers, a layer id implying an undeclared ancestor, a duplicate layer or
two declarations on one folder, a malformed tag, a `@seam` that is no
longer an export, a dependency violating a declared rule, and a committed
bundle that no longer matches the source. It needs no `d2`, comparing the
text artefacts rather than re-rendering SVGs.

Edges carry only `crossesPackage` as a boundary fact. Which runtime
boundaries an import crosses cannot be read off a static import graph — a
module importing into a worker-boundary layer is how you obtain the
module, not evidence that a thread hop occurs — so that is the one such
fact that is always true when reported.

yarn.lock covers this package's dependencies and those of the docs site
added in a later commit.
Replaces the hand-maintained path-to-layer mapping with declarations that
sit next to the code they describe: 37 layers across petrinaut-core and
petrinaut, covering 412 source files.

Most declarations are frontmatter added to READMEs that already existed
and already explained their folder, so their prose now doubles as the
layer's documentation. Where a folder had a barrel entry file and no
README, `@layerRoot` on that file does the same job. Fifteen boundaries
and 25 invariants are recorded against the specific files that uphold
them.

Three dependency rules are now enforced against the real import graph. The
substantive one is that `react` must not depend on `ui`: state providers
stay mountable without rendering the editor. That already held — 0 imports
in that direction against 251 the other way — so the rule locks in an
existing property rather than asking for new work.

Retires `scripts/generate-dependency-diagrams.mjs`, which held the
architecture as ~180 lines of `if (path.startsWith(...))` far from the
code, with a fallback that silently mis-bucketed anything renamed. It also
hard-coded seven of petrinaut-core's ten entry points, so imports through
`./ai`, `./optimization` and `./compiled-model` resolved to nothing and
were absent from the diagrams entirely; aliases are now derived from the
package's `exports`. Its generated `.d2`/`.svg` output and
`dependency-diagrams.md` go with it, and `dependency-cruiser` is no longer
a petrinaut-core dependency.

The hand-written HTML in `docs/architecture/` is deliberately left in
place — the content is valuable but unverified, and its custom
lane-and-box CSS needs rewriting page by page. A README there records that
status and points at the generated docs for facts about the current shape
of the system.
The bundle is committed because it is what CI diffs against to detect
drift, and what a host embedding these docs consumes. Regenerate with
`mise run doc:architecture` after changing annotations or moving code.

`@apps/petrinaut-docs` is a Starlight site that owns no content: every
page comes from the bundle. That is deliberate — the bundle has to render
in a host that did not generate it, so this site is a portability test as
much as a way to read the docs, and anything that only works here is a bug
in the bundle. It builds its sidebar from `manifest.json` rather than from
Starlight-shaped frontmatter, which is what keeps the bundle framework
neutral.

`trailingSlash: "never"` and `build.format: "file"` are load-bearing.
Inter-page links in the bundle are relative and assume a slug maps to a
URL with no trailing slash; serving `/architecture/core/` instead would
resolve them one level too deep.

The bundle is copied into the app rather than loaded in place because
`astro dev` resolves an MDX page's relative image paths against the
project root, so a diagram referenced from outside the project cannot be
found. Copying is also what an embedding host does.

`installConfig.hoistingLimits` nests this app's dependencies. Astro's
generated prerender entry resolves `cookie` from the app's build output,
which would otherwise reach the root-hoisted `cookie@0.7.2` that `express`
pins and fail on a missing `parseCookie` export. Nesting keeps Astro on
its own `cookie@2.x` without changing hoisting for the rest of the
monorepo.

There is no `lint:tsc` for the app: everything in it is `.mjs`, so
`astro check` would pull in `@astrojs/check` and `typescript` to check
almost nothing. `astro build` is the real check.
oxfmt and the architecture generator both claimed ownership of the
bundle's MDX, which made the format check and the drift check mutually
exclusive: formatting the bundle made a fresh generate look like drift,
and regenerating it made the format check fail.

The generator owns that output byte-for-byte — CI diffs a fresh build
against the committed files to detect drift — so the bundle is now ignored
by the formatter, alongside the other autogenerated paths. Authored pages
in `content/` are still formatted; they are inputs, and the bundle copies
them verbatim.
@kube kube self-assigned this Aug 5, 2026
@vercel

vercel Bot commented Aug 5, 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 5, 2026 11:41pm
hashdotdesign-tokens Ready Ready Preview Aug 5, 2026 11:41pm
petrinaut Ready Ready Preview Aug 5, 2026 11:41pm

@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team area/apps type/legal Owned by the @legal team labels Aug 5, 2026
export const GENERATED_ORDER_BASE = 1000;

const escapeTableCell = (text: string): string =>
text.replace(/\|/gu, "\\|").replace(/\n/gu, " ");

const lineStarts = lineStartOffsets(sourceText);

for (const match of sourceText.matchAll(blockCommentPattern)) {

const fields = match[1] ?? "";
const read = (key: string): string | null => {
const found = new RegExp(`^${key}\\s*:\\s*(.+)$`, "mu").exec(fields);
@codspeed-hq

codspeed-hq Bot commented Aug 5, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 15.38%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 2 regressed benchmarks
✅ 96 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
bit_matrix/dense/iter_row[64] 140.8 ns 170 ns -17.16%
bit_matrix/dense/iter_row[200] 185.8 ns 215 ns -13.57%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing cf/fe-1322-generate-petrinaut-architecture-docs-from-in-code (f7a7839) with main (2d99645)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (b13818b) during the generation of this report, so 2d99645 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

The bundle is derived entirely from the annotations in the source and the
authored pages in `content/`, so committing it meant reviewing every
change twice and resolving conflicts in generated files. It is now
git-ignored build output.

This removes the need for the drift check that compared a fresh build
against the committed copy: with nothing stored, nothing can be stale.
What `lint:arch-docs` still enforces is the part that was always the real
value — unannotated files, undeclared ancestors, malformed tags, dead
`@seam`s, and dependency rules the import graph violates. It builds the
bundle in memory and discards it.

The docs site now regenerates the bundle rather than assuming a committed
one is present, so `dev` and `build` share one code path and cannot render
a stale copy.

One consequence worth naming: a reviewer can no longer see the rendered
documentation change in a PR diff. The annotations that produce it are
still reviewable, and the docs can be regenerated locally in seconds.
kube added 2 commits August 5, 2026 23:56
The five hand-written HTML pages in `petrinaut-core/docs/architecture/`
carried genuinely useful detail — the binary frame format, the worker
message protocol, the ack contract, the Monte Carlo memory model — but
nothing verified them, and their custom lane-and-box CSS meant they could
only be read by opening a file in a browser.

They are now authored MDX under `content/simulation/`, bundled alongside
the generated pages and reachable from the docs site. Diagrams that relied
on CSS are plain tables or fenced text blocks, so they render anywhere the
rest of the bundle does.

Migrating surfaced staleness, which was the main argument for moving them:

- The frame-format page said UUID support was designed but not implemented
  ("the layout machinery is ready; the value plumbing is not"). `uuid` is a
  real element type now, with parsing, formatting, namespaced generation
  and seeded fallback.
- The sandbox section described only global shadowing plus a constructor
  guard "for the duration of the call". The guard now swaps `.constructor`
  descriptors on the built-in prototypes and freezes the user-facing
  argument.

Everything retained was checked against the code rather than copied:
frame version and header size, the play-mode backpressure profiles, the
worker and Monte Carlo batch defaults, and the string-pool design.

Content the generated pages already own — module maps, per-layer file
lists, responsibilities — was dropped rather than duplicated, so these
pages carry only what an import graph cannot express.

The three inbound references to the HTML now point at the docs site, and
the site nests authored pages by slug directory so the five appear as one
"Simulation" group.
The bundle referenced `diagrams/*.svg` unconditionally, but rendering them
needs `d2`, and a failure to render only produced a warning. In any
environment without `d2` the result was a bundle pointing at images that
were never written, which fails the consuming site's build rather than
degrading.

`d2` is a declared repo tool, so a `mise install` environment is fine. An
environment that installs tools individually is not — the Vercel install
script for the Petrinaut website names its tools one by one and does not
include it, which is exactly the situation a deployment would hit.

The generator now probes for the renderer before emitting pages and omits
the diagram images when it is absent, so the bundle is internally
consistent either way. `check` skips the probe: it writes nothing, so
availability cannot affect its result.

Verified in both directions — with `d2`, 8 diagrams and 7 pages embedding
them; without it, no SVGs, no references, and the site still builds all 47
pages.
Hand-written guides sat in their own sidebar section, separate from the
generated reference for the same code. Someone reading about the Monte
Carlo layer had no reason to discover the page explaining its memory
model, and vice versa.

An authored page can now name the layer it explains:

    attachTo: core.simulation.monte-carlo

which moves it beneath that layer's page, adds it to a "Guides" section
there, and — because nesting follows the slug — places it in the sidebar
next to the layer's sub-layers. The five simulation deep-dives now live
inside the architecture tree rather than beside it.

`attachTo` references a layer; it does not declare one. Declaring layers
from `content/` stays forbidden, and naming a layer that does not exist
fails the check.

This required a way to link between pages that does not depend on where a
page ends up, since `attachTo` decides that:

    [the engine](layer:core.simulation.engine)
    [memory model](doc:simulation/memory-model)

Both resolve to the correct relative path at emit time, fragments
included, and an unresolved target is a build error rather than a link
that 404s for a reader. Ordinary relative and absolute links are
untouched, so pages that will never move can still use them.

The site's sidebar now nests purely by slug rather than by whether a page
was generated, which is what lets an attached guide appear inside a
generated group at all.
Migrating the HTML pages flattened their diagrams into tables and fenced
text. The lane-and-box thread views, the frame memory map and the message
sequence carried real information in their layout — a byte map read as a
table loses the sense of a single contiguous buffer, which is the point of
the format.

They are back as React components in the bundle, imported by authored
pages:

    import { ByteMap } from "@diagrams/byte-map";

`@diagrams/` is rewritten to a real relative path at emit time, for the
same reason as `layer:` and `doc:` — a page's depth depends on `attachTo`.
An import naming a component that does not exist fails the check.

Four components cover every diagram the old pages had: `lanes` (parallel
columns of boxes), `pipeline` (a numbered chain), `byte-map` (an offset
gutter with typed sections) and `sequence` (two actors exchanging
messages). They are data-driven, so a page supplies content and the
component owns presentation.

Two constraints keep the bundle portable, both learned the hard way here:

- **Plain React, no dependencies.** Styling is one stylesheet deriving its
  colours from the host's `currentColor`, so it works on light and dark
  themes it has never seen. No design system, no Astro, no `next/*`.
- **String props, never JSX.** JSX inside MDX is compiled by the host's MDX
  renderer, and handing that to a React component fails at render with
  "Objects are not valid as a React child". Props are strings, and
  backticks render as `<code>`.

This is the one thing the bundle now asks of a host: a React-capable MDX
pipeline for authored pages. Generated pages stay plain CommonMark, and
`architecture.md` — the single-file artefact for agents — has no
components at all.

`starlight-llms-txt` is dropped rather than worked around: it renders MDX
to text in a container with no React renderer, and its `exclude` option is
accepted but never passed to the `/llms-full.txt` route, so components
were a hard build failure. The site now serves the bundle's own
`architecture.md` and `architecture.json`, which is better anyway — the
machine-readable surface is identical to what any other host would serve.

Also fixes a race the components exposed: `build` and `lint:tsc` each
invoked `sync:bundle`, which wipes and recopies the same directories, so
Turborepo running them concurrently failed intermittently. `sync:bundle`
is now a task both depend on.
CI checks out only the files a package declares a dependency on, so the
`@apps/petrinaut-docs` lint job had no `petrinaut-core/src` to scan and
failed with ENOENT the moment it ran the generator.

The dependency is real — the generator walks those packages' source for
annotations — it was simply never written down, because nothing is
imported from them. Declaring it as a devDependency of the generator makes
the sparse checkout include them and states the coupling honestly.

Also dedupes the lockfile, which `lint:yarn-deduplicate` flagged after the
Astro and React additions.
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.57%. Comparing base (ac5ec9c) to head (f7a7839).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9165   +/-   ##
=======================================
  Coverage   59.57%   59.57%           
=======================================
  Files        1413     1413           
  Lines      138053   138053           
  Branches     6510     6510           
=======================================
+ Hits        82240    82241    +1     
+ Misses      54771    54770    -1     
  Partials     1042     1042           
Flag Coverage Δ
apps.hash-ai-worker-ts 1.99% <ø> (ø)
apps.hash-api 12.56% <ø> (ø)
blockprotocol.type-system 40.84% <ø> (ø)
local.claude-hooks 0.00% <ø> (ø)
local.harpc-client 51.49% <ø> (ø)
local.hash-backend-utils 3.27% <ø> (ø)
local.hash-graph-sdk 10.02% <ø> (ø)
local.hash-isomorphic-utils 12.25% <ø> (ø)
rust.antsi 2.36% <ø> (ø)
rust.error-stack 90.81% <ø> (ø)
rust.harpc-codec 84.70% <ø> (ø)
rust.harpc-net 96.21% <ø> (+0.01%) ⬆️
rust.harpc-tower 67.03% <ø> (ø)
rust.harpc-types 0.00% <ø> (ø)
rust.harpc-wire-protocol 92.23% <ø> (ø)
rust.hash-codec 72.76% <ø> (ø)
rust.hash-graph-api 7.36% <ø> (ø)
rust.hash-graph-authorization 62.59% <ø> (ø)
rust.hash-graph-embeddings 91.88% <ø> (ø)
rust.hash-graph-postgres-store 29.66% <ø> (ø)
rust.hash-graph-store 42.16% <ø> (ø)
rust.hash-graph-temporal-versioning 47.95% <ø> (ø)
rust.hash-graph-types 0.00% <ø> (ø)
rust.hash-graph-validation 84.71% <ø> (ø)
rust.hashql-ast 89.63% <ø> (ø)
rust.hashql-compiletest 28.39% <ø> (ø)
rust.hashql-core 78.98% <ø> (ø)
rust.hashql-diagnostics 72.51% <ø> (ø)
rust.hashql-eval 79.82% <ø> (ø)
rust.hashql-hir 89.09% <ø> (ø)
rust.hashql-mir 87.92% <ø> (ø)
rust.hashql-syntax-jexpr 94.04% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread .claude/launch.json
Comment on lines +1 to +17
{
"version": "0.0.1",
"configurations": [
{
"name": "petrinaut-docs",
"runtimeExecutable": "yarn",
"runtimeArgs": [
"workspace",
"@apps/petrinaut-docs",
"dev",
"--port",
"4321"
],
"port": 4321
}
]
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimDiekmann @CiaranMn what do you think of having this .claude/launch.json in the repo?

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Benchmark results

@rust/hash-graph-benches – Integrations

policy_resolution_large

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2002 $$26.8 \mathrm{ms} \pm 216 \mathrm{μs}\left({\color{lightgreen}-19.654 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.42 \mathrm{ms} \pm 21.9 \mathrm{μs}\left({\color{lightgreen}-25.138 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1002 $$12.1 \mathrm{ms} \pm 78.5 \mathrm{μs}\left({\color{lightgreen}-33.331 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$43.0 \mathrm{ms} \pm 440 \mathrm{μs}\left({\color{lightgreen}-12.901 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$15.8 \mathrm{ms} \pm 141 \mathrm{μs}\left({\color{lightgreen}-16.506 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1527 $$26.2 \mathrm{ms} \pm 253 \mathrm{μs}\left({\color{lightgreen}-17.872 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$28.0 \mathrm{ms} \pm 180 \mathrm{μs}\left({\color{lightgreen}-24.660 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.76 \mathrm{ms} \pm 23.9 \mathrm{μs}\left({\color{lightgreen}-20.418 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$13.2 \mathrm{ms} \pm 129 \mathrm{μs}\left({\color{lightgreen}-27.206 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$3.82 \mathrm{ms} \pm 26.8 \mathrm{μs}\left({\color{lightgreen}-27.572 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.98 \mathrm{ms} \pm 15.7 \mathrm{μs}\left({\color{lightgreen}-28.680 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 52 $$3.34 \mathrm{ms} \pm 15.1 \mathrm{μs}\left({\color{lightgreen}-28.019 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$5.17 \mathrm{ms} \pm 39.2 \mathrm{μs}\left({\color{lightgreen}-21.164 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.54 \mathrm{ms} \pm 18.7 \mathrm{μs}\left({\color{lightgreen}-26.625 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 108 $$4.12 \mathrm{ms} \pm 22.7 \mathrm{μs}\left({\color{lightgreen}-19.931 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$4.42 \mathrm{ms} \pm 27.2 \mathrm{μs}\left({\color{lightgreen}-31.531 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.44 \mathrm{ms} \pm 16.4 \mathrm{μs}\left({\color{lightgreen}-21.897 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$4.13 \mathrm{ms} \pm 25.6 \mathrm{μs}\left({\color{lightgreen}-27.472 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.76 \mathrm{ms} \pm 20.6 \mathrm{μs}\left({\color{lightgreen}-5.154 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.59 \mathrm{ms} \pm 15.0 \mathrm{μs}\left({\color{lightgreen}-26.856 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 2 $$2.74 \mathrm{ms} \pm 14.6 \mathrm{μs}\left({\color{lightgreen}-21.530 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$3.09 \mathrm{ms} \pm 21.4 \mathrm{μs}\left({\color{lightgreen}-17.048 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.83 \mathrm{ms} \pm 16.0 \mathrm{μs}\left({\color{lightgreen}-12.786 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$3.04 \mathrm{ms} \pm 17.4 \mathrm{μs}\left({\color{lightgreen}-18.622 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$3.06 \mathrm{ms} \pm 22.1 \mathrm{μs}\left({\color{lightgreen}-30.724 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.76 \mathrm{ms} \pm 17.4 \mathrm{μs}\left({\color{lightgreen}-30.343 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 26 $$3.01 \mathrm{ms} \pm 18.6 \mathrm{μs}\left({\color{lightgreen}-30.437 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$3.44 \mathrm{ms} \pm 20.2 \mathrm{μs}\left({\color{lightgreen}-27.941 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$2.99 \mathrm{ms} \pm 18.8 \mathrm{μs}\left({\color{lightgreen}-28.916 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 27 $$3.31 \mathrm{ms} \pm 19.3 \mathrm{μs}\left({\color{lightgreen}-25.514 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$3.50 \mathrm{ms} \pm 22.2 \mathrm{μs}\left({\color{lightgreen}-26.720 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.97 \mathrm{ms} \pm 15.6 \mathrm{μs}\left({\color{lightgreen}-23.304 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$3.33 \mathrm{ms} \pm 16.1 \mathrm{μs}\left({\color{lightgreen}-25.818 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$42.3 \mathrm{ms} \pm 292 \mathrm{μs}\left({\color{lightgreen}-21.089 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$33.0 \mathrm{ms} \pm 200 \mathrm{μs}\left({\color{lightgreen}-25.360 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$35.8 \mathrm{ms} \pm 232 \mathrm{μs}\left({\color{lightgreen}-28.408 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$32.8 \mathrm{ms} \pm 280 \mathrm{μs}\left({\color{lightgreen}-6.511 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$41.5 \mathrm{ms} \pm 246 \mathrm{μs}\left({\color{lightgreen}-23.773 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$49.7 \mathrm{ms} \pm 309 \mathrm{μs}\left({\color{lightgreen}-13.855 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$40.2 \mathrm{ms} \pm 222 \mathrm{μs}\left({\color{lightgreen}-20.792 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$90.0 \mathrm{ms} \pm 626 \mathrm{μs}\left({\color{lightgreen}-18.693 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$37.3 \mathrm{ms} \pm 2.01 \mathrm{ms}\left({\color{lightgreen}-13.668 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$275 \mathrm{ms} \pm 1.06 \mathrm{ms}\left({\color{lightgreen}-10.445 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$11.0 \mathrm{ms} \pm 69.6 \mathrm{μs}\left({\color{lightgreen}-19.685 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$11.1 \mathrm{ms} \pm 64.9 \mathrm{μs}\left({\color{lightgreen}-19.824 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$11.1 \mathrm{ms} \pm 67.1 \mathrm{μs}\left({\color{lightgreen}-25.824 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$11.1 \mathrm{ms} \pm 70.7 \mathrm{μs}\left({\color{lightgreen}-21.612 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$11.1 \mathrm{ms} \pm 96.4 \mathrm{μs}\left({\color{lightgreen}-17.491 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$11.0 \mathrm{ms} \pm 58.8 \mathrm{μs}\left({\color{lightgreen}-20.930 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$11.2 \mathrm{ms} \pm 75.3 \mathrm{μs}\left({\color{gray}-4.887 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$11.2 \mathrm{ms} \pm 82.1 \mathrm{μs}\left({\color{lightgreen}-28.112 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$11.1 \mathrm{ms} \pm 59.9 \mathrm{μs}\left({\color{lightgreen}-31.846 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$11.2 \mathrm{ms} \pm 77.3 \mathrm{μs}\left({\color{lightgreen}-34.124 \mathrm{\%}}\right) $$ 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 $$11.4 \mathrm{ms} \pm 62.5 \mathrm{μs}\left({\color{lightgreen}-27.268 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$11.4 \mathrm{ms} \pm 62.6 \mathrm{μs}\left({\color{lightgreen}-24.901 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$11.4 \mathrm{ms} \pm 58.4 \mathrm{μs}\left({\color{lightgreen}-31.431 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$12.0 \mathrm{ms} \pm 239 \mathrm{μs}\left({\color{lightgreen}-26.822 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$11.4 \mathrm{ms} \pm 86.0 \mathrm{μs}\left({\color{lightgreen}-23.572 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$11.4 \mathrm{ms} \pm 68.1 \mathrm{μs}\left({\color{lightgreen}-27.191 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$11.4 \mathrm{ms} \pm 77.6 \mathrm{μs}\left({\color{lightgreen}-29.527 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$11.5 \mathrm{ms} \pm 69.8 \mathrm{μs}\left({\color{lightgreen}-27.359 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$11.4 \mathrm{ms} \pm 61.2 \mathrm{μs}\left({\color{lightgreen}-29.071 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity_type

Function Value Mean Flame graphs
get_entity_type_by_id Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba $$8.53 \mathrm{ms} \pm 46.6 \mathrm{μs}\left({\color{lightgreen}-24.874 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$60.3 \mathrm{ms} \pm 513 \mathrm{μs}\left({\color{lightgreen}-23.957 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$112 \mathrm{ms} \pm 646 \mathrm{μs}\left({\color{lightgreen}-17.077 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$66.9 \mathrm{ms} \pm 587 \mathrm{μs}\left({\color{lightgreen}-27.689 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$76.9 \mathrm{ms} \pm 629 \mathrm{μs}\left({\color{lightgreen}-26.132 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$84.9 \mathrm{ms} \pm 437 \mathrm{μs}\left({\color{lightgreen}-15.500 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$90.6 \mathrm{ms} \pm 530 \mathrm{μs}\left({\color{lightgreen}-21.603 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$43.0 \mathrm{ms} \pm 318 \mathrm{μs}\left({\color{lightgreen}-21.076 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$70.2 \mathrm{ms} \pm 348 \mathrm{μs}\left({\color{lightgreen}-22.820 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$48.6 \mathrm{ms} \pm 310 \mathrm{μs}\left({\color{lightgreen}-15.414 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$57.7 \mathrm{ms} \pm 404 \mathrm{μs}\left({\color{lightgreen}-13.971 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$59.9 \mathrm{ms} \pm 363 \mathrm{μs}\left({\color{lightgreen}-24.271 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$59.9 \mathrm{ms} \pm 505 \mathrm{μs}\left({\color{lightgreen}-18.297 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$119 \mathrm{ms} \pm 643 \mathrm{μs}\left({\color{gray}-2.042 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$130 \mathrm{ms} \pm 583 \mathrm{μs}\left({\color{lightgreen}-5.761 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$19.7 \mathrm{ms} \pm 108 \mathrm{μs}\left({\color{lightgreen}-17.389 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$527 \mathrm{ms} \pm 1.35 \mathrm{ms}\left({\color{gray}-0.174 \mathrm{\%}}\right) $$ Flame Graph

@@ -0,0 +1,5 @@
# License

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thee docs need switching from AGPLv3 to Apache 2.0/MIT dual-license.

// The helmet carries the Petrinaut identity, so the title beside it names
// only what this site is. `replacesTitle: false` keeps both.
logo: {
src: "./src/assets/petrinaut-helmet.png",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half a megabyte!

layer: core.hir
name: HIR compiler
role: Lowers user-authored TypeScript to a source-spanned IR, then typechecks, lints and emits it
seams:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a term other than seams? Feels very AI.

@lunelson

lunelson commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Nice initiative, I've been thinking about doing something similar. One thing you might consider adding here is drift.

Current checks cover facts (ancestors, exports, dep rules, links), but not whether a given sentence is still true; whereas drift binds a markdown file to a source file or AST symbol, so you can use drift check to trace when your doc goes (potentially) stale.

The docs stay pure markdown(bindings live in drift.lock, not in frontmatter) so it doesn't collide with attachTo or the emit pipeline.

It might also be a good answer to the petrinaut-opt item under Known Issues, since drift parses Python natively

Adoption, roughly, according to claude:

  • Bind each content/**.mdx page to the handful of symbols it actually describes, e.g. drift link libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx <frame buffer allocation site>#<symbol>. Roughly 15–20 bindings total.
  • Add drift check --changed to the existing lint:arch-docs CI job; it exits 1 on stale.
  • Skip its broken-link checking — emit/mdx.ts already resolves links, and doubling up would just produce two error formats for one problem.
  • Don't let bindings substitute for the generator's coverage check; drift is blind to any file nobody bound, so the two checks are doing different jobs.

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

Labels

area/apps area/deps Relates to third-party dependencies (area) 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 type/eng > frontend Owned by the @frontend team type/legal Owned by the @legal team

Development

Successfully merging this pull request may close these issues.

4 participants