diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c8b85c97272..a0d76fc3a88 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -231,6 +231,22 @@ jobs: mise run doc:dependency-diagram git --no-pager diff --exit-code --color '**/docs/dependency-diagram.mmd' + - name: Validate Petrinaut architecture docs + if: ${{ success() || failure() }} + run: | + ## Builds the architecture bundle and reports on the annotations it + ## reads: unannotated files, undeclared layers, dead `@entryPoint`s, + ## and dependency rules the import graph violates. The bundle itself is + ## build output and is discarded. Needs no `d2`: nothing is rendered. + if ! yarn workspace @local/petrinaut-arch-docs lint:arch-docs; then + echo '' + echo '' + echo 'ℹ️ ℹ️ ℹ️' + echo 'Fix the annotations reported above. See libs/@local/petrinaut-arch-docs/README.md.' + echo 'ℹ️ ℹ️ ℹ️' + exit 1 + fi + - name: Run yarn lint:constraints if: ${{ success() || failure() }} run: | diff --git a/AGENTS.md b/AGENTS.md index 2575a5db8ff..381bf97a3e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,6 +107,32 @@ When you change UI or behaviour in the petrinaut packages (`libs/@hashintel/petr If a change ships without doc updates, call that out in your summary so the user can decide whether to follow up. +### Petrinaut architecture docs + +Distinct from the user guide above: the **architecture** docs describe the shape of the code for the people (and agents) working on it. They are generated from annotations in the source by `@local/petrinaut-arch-docs`, and CI fails when they drift. + +The architecture is declared **next to the code it describes** — never in a central mapping file: + +- `@layerRoot ` plus `@role ` in a doc comment on a folder's primary file declares a layer. That pair is the whole declaration; everything else is optional. Prefer this — it needs no new file. +- A folder's `README.md` frontmatter (`layer` and `role`, optionally `name`, `entryPoints`, `boundaries`, `invariants`) does the same, and the prose below becomes that layer's page. Use it when the folder has real prose to carry, or when no single file is the obvious host. +- `@boundary `, `@invariant ` and `@entryPoint ` in any file's doc comments attach facts to the specific code that upholds them. +- Files with no annotation inherit from the nearest declaring ancestor, so only ~40 declarations cover ~400 files. Do not annotate every file. + +The generated docs are **build output and are not committed** — there is nothing to regenerate before pushing. Only the annotations are versioned. + +When you change structure in `libs/@hashintel/petrinaut-core` or `libs/@hashintel/petrinaut`, you MUST: + +1. Add a declaration if you introduce a folder that is a genuinely new architectural unit — a new boundary or a distinct responsibility, not merely a new directory. +2. Update the affected `@boundary`/`@invariant` annotations if you change what they claim. These are claims CI and reviewers rely on; an invariant that is no longer true is worse than none. + +Verify with `yarn workspace @local/petrinaut-arch-docs lint:arch-docs`, which fails on unannotated files, undeclared ancestors, dead `@entryPoint`s and rule violations. To read the docs, `turbo run doc:architecture --filter @local/petrinaut-arch-docs` writes the bundle to `libs/@local/petrinaut-arch-docs/bundle/` (git-ignored). + +Full reference: `libs/@local/petrinaut-arch-docs/README.md`. Browse the docs with `turbo run dev --filter @apps/petrinaut-docs`, which regenerates the bundle first — run it through Turborepo, since the package script alone skips that. For a quick read of the whole architecture, generate the bundle and open `libs/@local/petrinaut-arch-docs/bundle/architecture.md` — the entire model in one file. + +Hand-written MDX in `libs/@local/petrinaut-arch-docs/content/` is optional. Add a page there for reasoning an import graph cannot express; the system works with that directory absent. + +Give such a page `attachTo: ` in its frontmatter and it nests inside the generated tree beneath that layer, rather than sitting in a separate section — that is how the simulation deep-dives are wired. Link between pages with `[text](layer:core.simulation.engine)` or `[text](doc:simulation/memory-model)`; relative paths break when a page's `attachTo` changes, and unresolved targets fail CI. + ## Contextual Rules CRITICAL: For the files referenced below, use your Read tool to load it on a need-to-know basis, ONLY when relevant to the SPECIFIC task at hand: diff --git a/apps/petrinaut-docs/.gitignore b/apps/petrinaut-docs/.gitignore new file mode 100644 index 00000000000..3f53ae7899a --- /dev/null +++ b/apps/petrinaut-docs/.gitignore @@ -0,0 +1,11 @@ +# Copied from the architecture bundle by scripts/sync-bundle.mjs. The bundle in +# libs/@local/petrinaut-arch-docs/bundle is the committed artefact; this is a +# build input derived from it. +src/content/docs/ +src/content/diagrams/ +src/content/components/ +public/architecture.md +public/architecture.json + +dist/ +.astro/ diff --git a/apps/petrinaut-docs/LICENSE-APACHE.md b/apps/petrinaut-docs/LICENSE-APACHE.md new file mode 100644 index 00000000000..4b43328a923 --- /dev/null +++ b/apps/petrinaut-docs/LICENSE-APACHE.md @@ -0,0 +1,189 @@ +# Apache License + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +- **(a)** You must give any other recipients of the Work or Derivative Works a copy of + this License; and +- **(b)** You must cause any modified files to carry prominent notices stating that You + changed the files; and +- **(c)** You must retain, in the Source form of any Derivative Works that You distribute, + all copyright, patent, trademark, and attribution notices from the Source form + of the Work, excluding those notices that do not pertain to any part of the + Derivative Works; and +- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any + Derivative Works that You distribute must include a readable copy of the + attribution notices contained within such NOTICE file, excluding those notices + that do not pertain to any part of the Derivative Works, in at least one of the + following places: within a NOTICE text file distributed as part of the + Derivative Works; within the Source form or documentation, if provided along + with the Derivative Works; or, within a display generated by the Derivative + Works, if and wherever such third-party notices normally appear. The contents of + the NOTICE file are for informational purposes only and do not modify the + License. You may add Your own attribution notices within Derivative Works that + You distribute, alongside or as an addendum to the NOTICE text from the Work, + provided that such additional attribution notices cannot be construed as + modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: Apply the Apache License to a specific file + +To apply the Apache License to an individual file, attach the following notice. +The text should be enclosed in the appropriate comment syntax for the file +format. + + Copyright © 2025–, HASH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/petrinaut-docs/LICENSE-MIT.md b/apps/petrinaut-docs/LICENSE-MIT.md new file mode 100644 index 00000000000..d85585ee20d --- /dev/null +++ b/apps/petrinaut-docs/LICENSE-MIT.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright © 2025–, HASH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/petrinaut-docs/LICENSE.md b/apps/petrinaut-docs/LICENSE.md new file mode 100644 index 00000000000..6dad94c0e5a --- /dev/null +++ b/apps/petrinaut-docs/LICENSE.md @@ -0,0 +1,3 @@ +# License + +Licensed under either of the [Apache License, Version 2.0](LICENSE-APACHE.md) or [MIT license](LICENSE-MIT.md) at your option. diff --git a/apps/petrinaut-docs/README.md b/apps/petrinaut-docs/README.md new file mode 100644 index 00000000000..be89710cf02 --- /dev/null +++ b/apps/petrinaut-docs/README.md @@ -0,0 +1,66 @@ +# `@apps/petrinaut-docs` + +Renders the Petrinaut architecture bundle as a browsable [Starlight](https://starlight.astro.build) +site. + +**Run it through Turborepo**, which is what knows the site needs a freshly +generated bundle: + +```sh +turbo run dev --filter @apps/petrinaut-docs # browse at http://localhost:4321 +turbo run build --filter @apps/petrinaut-docs # static output in dist/ +``` + +`yarn workspace @apps/petrinaut-docs dev` runs the package script directly and +skips the task graph, so it renders whatever was last synced. + +## This app owns no content + +Every page comes from the bundle generated by `@local/petrinaut-arch-docs`. +`scripts/sync-bundle.mjs` copies that bundle into `src/content/`; generating it +is the generator's own `doc:architecture` task, which `sync:bundle` declares as a +dependency. The bundle is build output and is not committed. + +`build`, `lint:tsc` and `dev` all depend on one shared `sync:bundle` rather than +each running it — they used to call it themselves, which raced when Turborepo ran +them concurrently on the same directories. + +That separation is deliberate. The bundle has to render in a host that did not +generate it — hash.dev is the other one — so this site is a test of the bundle's +portability as much as a way to read it. Anything that only works here is a bug +in the bundle. + +To change what the docs say, change the annotations in the Petrinaut source or +the authored pages in `libs/@local/petrinaut-arch-docs/content/`. Anything run +through Turborepo picks the change up; to regenerate the bundle on its own: + +```sh +turbo run doc:architecture --filter @local/petrinaut-arch-docs +``` + +## Why the bundle is copied rather than read in place + +Pointing Astro's `glob` loader at the bundle directory works for `astro build`, +but not for `astro dev`: in dev, Astro resolves an MDX page's relative image +paths against the project root, so `../diagrams/core.svg` cannot be found. +Copying sidesteps that, and matches what an embedding host does anyway. + +The copied directories (`src/content/docs`, `src/content/diagrams`) are +gitignored, as is the bundle they come from. Nothing generated is versioned. + +## Notes on configuration + +Authored pages are optional. When `content/` in the generator package is empty +or absent, there is no authored home page, so `/` redirects to the generated +architecture overview instead. + +`trailingSlash: "never"` and `build.format: "file"` are load-bearing, not +stylistic. The bundle's inter-page links are relative and assume a page's slug +maps to a URL with no trailing slash; serving `/architecture/core/` instead would +resolve those links one level too deep. + +`installConfig.hoistingLimits` nests this app's dependencies rather than hoisting +them. Astro's generated prerender entry resolves `cookie` from this 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. diff --git a/apps/petrinaut-docs/astro.config.mjs b/apps/petrinaut-docs/astro.config.mjs new file mode 100644 index 00000000000..b7146795c99 --- /dev/null +++ b/apps/petrinaut-docs/astro.config.mjs @@ -0,0 +1,165 @@ +// @ts-check +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import react from "@astrojs/react"; +import starlight from "@astrojs/starlight"; +import { defineConfig } from "astro/config"; + +/** + * Renders the architecture bundle produced by `@local/petrinaut-arch-docs`. + * + * This site owns no content. That is the point: the bundle has to render in a + * host that did not generate it, and this site is the first of two such hosts + * (hash.dev being the other). Anything that only works here is a bug in the + * bundle's portability, which is why the sidebar below is built from + * `manifest.json` rather than from Starlight-specific frontmatter. + */ + +const manifestPath = fileURLToPath( + new URL( + "../../libs/@local/petrinaut-arch-docs/bundle/manifest.json", + import.meta.url, + ), +); + +/** @type {import("@local/petrinaut-arch-docs").BundleManifest} */ +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + +/** + * @typedef {{ label: string, link: string }} SidebarLink + * @typedef {{ label: string, collapsed?: boolean, items: SidebarItem[] }} SidebarGroup + * @typedef {SidebarLink | SidebarGroup} SidebarItem + */ + +/** + * Builds Starlight's nested sidebar from the manifest. + * + * Nesting follows the *slug*, not whether a page was generated. An authored + * guide that attached itself to a layer has a slug beneath that layer, so it + * nests with the generated reference for the same code — which is the whole + * point of attaching it. Ordering within a level uses the manifest's `order`, + * so guides (low numbers) sort ahead of sub-layers (1000+). + */ +const buildSidebar = () => { + const pages = [...manifest.pages].sort( + (left, right) => + left.order - right.order || left.slug.localeCompare(right.slug), + ); + + /** + * @param {string} slug + * @returns {string} + */ + const parentOf = (slug) => + slug.includes("/") ? slug.slice(0, slug.lastIndexOf("/")) : ""; + + const hasChildren = new Set( + pages.map((page) => parentOf(page.slug)).filter((slug) => slug !== ""), + ); + + /** + * Recursive, so the return type is annotated explicitly — TypeScript cannot + * infer it from a function that references itself. + * + * @param {string} parentSlug + * @returns {SidebarItem[]} + */ + const itemsUnder = (parentSlug) => + pages + .filter((page) => parentOf(page.slug) === parentSlug) + .map((page) => + hasChildren.has(page.slug) + ? { + label: page.title, + collapsed: true, + items: [ + { label: `${page.title} overview`, link: `/${page.slug}` }, + ...itemsUnder(page.slug), + ], + } + : { label: page.title, link: `/${page.slug}` }, + ); + + const architectureRoot = pages.find((page) => page.slug === "architecture"); + const narrative = pages.filter( + (page) => !page.slug.startsWith("architecture") && !page.slug.includes("/"), + ); + + return [ + ...narrative.map((page) => ({ + label: page.title, + link: page.slug === "index" ? "/" : `/${page.slug}`, + })), + ...(architectureRoot + ? [ + { + label: "Architecture", + collapsed: false, + items: [ + { label: "Overview", link: `/${architectureRoot.slug}` }, + ...itemsUnder(architectureRoot.slug), + ], + }, + ] + : []), + ]; +}; + +/** + * Authored pages are optional, so the site must have a root page without them. + * + * An authored `index` page becomes `/`. With no authored content at all, `/` + * redirects to the generated overview instead, which keeps `content/` a genuine + * opt-in rather than something the site quietly depends on. + */ +const hasAuthoredIndex = manifest.pages.some( + (page) => page.kind === "authored" && page.slug === "index", +); + +export default defineConfig({ + site: "https://petrinaut-docs.hash.dev", + + ...(hasAuthoredIndex ? {} : { redirects: { "/": "/architecture" } }), + + // The bundle's inter-page links are relative and assume slugs map to URLs + // without a trailing slash. `format: "file"` writes `views.html` rather than + // `views/index.html`, so there is no trailing-slash form of a URL for a reader + // to land on and resolve those links one level too deep. See the generator's + // README for the contract. + trailingSlash: "never", + build: { format: "file" }, + + integrations: [ + // Authored pages may import diagram components from the bundle; generated + // pages stay plain Markdown and never need this. + react(), + starlight({ + title: "Architecture Docs", + description: + "How the Petrinaut packages fit together — generated from annotations in the source.", + // 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", + alt: "Petrinaut", + replacesTitle: false, + }, + favicon: "/favicon.ico", + social: [ + { + icon: "github", + label: "GitHub", + href: "https://github.com/hashintel/hash/tree/main/libs/@hashintel/petrinaut", + }, + ], + sidebar: buildSidebar(), + // No llms.txt plugin: the bundle emits its own `architecture.md` and + // `architecture.json`, which `scripts/sync-bundle.mjs` copies into + // `public/`. Serving those keeps the site's machine-readable surface + // identical to what any other host of the bundle would serve, rather than + // a second, site-shaped copy that could disagree with it. + pagination: false, + }), + ], +}); diff --git a/apps/petrinaut-docs/package.json b/apps/petrinaut-docs/package.json new file mode 100644 index 00000000000..e4318ab78a5 --- /dev/null +++ b/apps/petrinaut-docs/package.json @@ -0,0 +1,34 @@ +{ + "name": "@apps/petrinaut-docs", + "version": "0.0.0-private", + "private": true, + "description": "Renders the Petrinaut architecture bundle as a browsable docs site", + "license": "(MIT OR Apache-2.0)", + "type": "module", + "scripts": { + "build": "astro build", + "dev": "astro dev", + "lint:tsc": "astro check --minimumSeverity error", + "preview": "astro preview", + "sync:bundle": "node scripts/sync-bundle.mjs" + }, + "dependencies": { + "@astrojs/react": "6.0.2", + "@astrojs/starlight": "0.41.5", + "astro": "7.1.6", + "react": "19.2.6", + "react-dom": "19.2.6", + "sharp": "0.35.3" + }, + "devDependencies": { + "@astrojs/check": "0.9.10", + "@local/petrinaut-arch-docs": "workspace:*", + "@types/node": "22.18.13", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "typescript": "5.9.3" + }, + "installConfig": { + "hoistingLimits": "dependencies" + } +} diff --git a/apps/petrinaut-docs/public/favicon.ico b/apps/petrinaut-docs/public/favicon.ico new file mode 100644 index 00000000000..93949fb5761 Binary files /dev/null and b/apps/petrinaut-docs/public/favicon.ico differ diff --git a/apps/petrinaut-docs/scripts/sync-bundle.mjs b/apps/petrinaut-docs/scripts/sync-bundle.mjs new file mode 100644 index 00000000000..4bd3a17f24e --- /dev/null +++ b/apps/petrinaut-docs/scripts/sync-bundle.mjs @@ -0,0 +1,65 @@ +/** + * Copies the architecture bundle into this app's content tree. + * + * Generating the bundle is `@local/petrinaut-arch-docs`'s job, declared in + * `turbo.json` as a dependency of this task. This script used to spawn the + * generator itself, which meant a cross-package build step lived in a shell call + * that Turborepo could not see, order or report on. Now it only copies. + * + * Reading the bundle in place via a `glob` loader with an out-of-root `base` + * works for `astro build` but not for `astro dev`: Astro resolves an MDX page's + * relative image paths against the project root in dev, so `../diagrams/core.svg` + * cannot be found. Copying sidesteps that, and is what a host embedding the + * bundle (hash.dev) does anyway — so this stays an honest test of whether the + * bundle is portable. + */ + +import { access, cp, mkdir, rm } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const bundleRoot = fileURLToPath( + new URL("../../../libs/@local/petrinaut-arch-docs/bundle/", import.meta.url), +); +const contentRoot = fileURLToPath(new URL("../src/content/", import.meta.url)); + +try { + await access(bundleRoot); +} catch { + process.stderr.write( + `No bundle at ${bundleRoot}\n` + + "Run this through Turborepo so the generator runs first:\n" + + " turbo run sync:bundle --filter @apps/petrinaut-docs\n", + ); + process.exit(1); +} + +for (const directory of ["docs", "diagrams", "components"]) { + await rm(new URL(directory, `file://${contentRoot}`), { + recursive: true, + force: true, + }); +} + +await mkdir(contentRoot, { recursive: true }); + +// `pages/` becomes `docs/` — the collection name Starlight expects. +await cp(`${bundleRoot}pages`, `${contentRoot}docs`, { recursive: true }); +await cp(`${bundleRoot}diagrams`, `${contentRoot}diagrams`, { + recursive: true, +}); + +// 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, +}); + +// The bundle's machine-readable artefacts are served as-is, so an agent reading +// this site gets byte-identical content to one reading the bundle directly. +const publicRoot = fileURLToPath(new URL("../public/", import.meta.url)); +await mkdir(publicRoot, { recursive: true }); +for (const file of ["architecture.md", "architecture.json"]) { + await cp(`${bundleRoot}${file}`, `${publicRoot}${file}`); +} + +process.stdout.write("Synced architecture bundle into src/content\n"); diff --git a/apps/petrinaut-docs/src/assets/petrinaut-helmet.png b/apps/petrinaut-docs/src/assets/petrinaut-helmet.png new file mode 100644 index 00000000000..44d37070e72 Binary files /dev/null and b/apps/petrinaut-docs/src/assets/petrinaut-helmet.png differ diff --git a/apps/petrinaut-docs/src/content.config.mjs b/apps/petrinaut-docs/src/content.config.mjs new file mode 100644 index 00000000000..be3497ce343 --- /dev/null +++ b/apps/petrinaut-docs/src/content.config.mjs @@ -0,0 +1,18 @@ +import { docsLoader } from "@astrojs/starlight/loaders"; +import { docsSchema } from "@astrojs/starlight/schema"; +import { defineCollection } from "astro:content"; + +/** + * The docs collection is populated by `scripts/sync-bundle.mjs`, which copies the + * generated bundle into `src/content/docs`. This app authors no content of its + * own — see the sync script for why copying rather than loading in place. + * + * The bundle's `sidebar_order` frontmatter is read from `manifest.json` by + * `astro.config.mjs`, not from here, so the schema is Starlight's own. + */ +export const collections = { + docs: defineCollection({ + loader: docsLoader(), + schema: docsSchema(), + }), +}; diff --git a/apps/petrinaut-docs/tsconfig.json b/apps/petrinaut-docs/tsconfig.json new file mode 100644 index 00000000000..942ee7c77ec --- /dev/null +++ b/apps/petrinaut-docs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*", "astro.config.mjs"], + "exclude": ["dist"], + "compilerOptions": { + "checkJs": true + } +} diff --git a/apps/petrinaut-docs/turbo.json b/apps/petrinaut-docs/turbo.json new file mode 100644 index 00000000000..cd13dfbb0f8 --- /dev/null +++ b/apps/petrinaut-docs/turbo.json @@ -0,0 +1,30 @@ +{ + "extends": ["//"], + "tasks": { + // Copies the bundle into `src/content/`. Generating it is the generator's + // job, declared here as a dependency so Turborepo orders the two — the sync + // script used to shell out and run the generator itself, which put a + // cross-package build step outside the task graph. + "sync:bundle": { + "cache": false, + "dependsOn": ["@local/petrinaut-arch-docs#doc:architecture"] + }, + // `build`, `lint:tsc` and `dev` all need the synced content. Depending on one + // shared `sync:bundle` rather than each running it keeps them from racing on + // the same directories when Turborepo runs them concurrently. + "build": { + "cache": false, + "dependsOn": ["sync:bundle"], + "outputs": ["dist/**"] + }, + "lint:tsc": { + "cache": false, + "dependsOn": ["sync:bundle"] + }, + "dev": { + "cache": false, + "persistent": true, + "dependsOn": ["sync:bundle"] + } + } +} diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/architecture.css b/libs/@hashintel/petrinaut-core/docs/architecture/architecture.css deleted file mode 100644 index c9b6ef590c5..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/architecture.css +++ /dev/null @@ -1,478 +0,0 @@ -/* Shared styles for the simulation architecture docs (architecture*.html). */ - -:root { - /* One colour per module, used consistently across all pages. */ - --engine: #2563eb; /* blue — engine (stepping, frames) */ - --engine-bg: #eff6ff; - --authoring: #9333ea; /* purple — compilation of user code */ - --authoring-bg: #faf5ff; - --worker: #059669; /* green — worker + transport protocol */ - --worker-bg: #ecfdf5; - --mc: #ea580c; /* orange — monte carlo / experiments */ - --mc-bg: #fff7ed; - --memory: #dc2626; /* red — buffers, where bytes live */ - --memory-bg: #fef2f2; - --ui: #0891b2; /* cyan — React / UI consumers */ - --ui-bg: #ecfeff; - --ink: #111111; - --muted: #555555; - --line: #d9d9d9; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0 auto; - padding: 2rem 1.5rem 5rem; - max-width: 1000px; - background: #ffffff; - color: var(--ink); - font: - 15px/1.55 -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - Helvetica, - Arial, - sans-serif; -} - -h1 { - font-size: 1.7rem; - margin: 0.5rem 0 0.25rem; -} - -h2 { - font-size: 1.25rem; - margin: 2.5rem 0 0.75rem; - padding-bottom: 0.3rem; - border-bottom: 1px solid var(--line); -} - -h3 { - font-size: 1.02rem; - margin: 1.75rem 0 0.5rem; -} - -p { - max-width: 76ch; -} - -.subtitle { - color: var(--muted); - margin-top: 0; -} - -code, -pre { - font-family: - ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.86em; - background: #f5f5f5; - border-radius: 4px; -} - -code { - padding: 0.1em 0.35em; -} - -pre { - padding: 0.8rem 1rem; - overflow-x: auto; - border: 1px solid #ececec; -} - -pre code { - padding: 0; - background: none; -} - -a { - color: var(--engine); -} - -/* ---- Top navigation ------------------------------------------------- */ - -nav.docs { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - padding: 0.6rem 0; - margin-bottom: 1rem; - border-bottom: 2px solid var(--ink); - font-size: 0.9rem; -} - -nav.docs a { - text-decoration: none; - color: var(--ink); - padding: 0.25rem 0.7rem; - border: 1px solid var(--line); - border-radius: 999px; -} - -nav.docs a.current { - color: #fff; - background: var(--ink); - border-color: var(--ink); -} - -nav.docs a.tag-engine.current { - background: var(--engine); - border-color: var(--engine); -} -nav.docs a.tag-authoring.current { - background: var(--authoring); - border-color: var(--authoring); -} -nav.docs a.tag-worker.current { - background: var(--worker); - border-color: var(--worker); -} -nav.docs a.tag-mc.current { - background: var(--mc); - border-color: var(--mc); -} - -/* ---- Legend chips ---------------------------------------------------- */ - -.legend { - display: flex; - flex-wrap: wrap; - gap: 0.5rem 1rem; - margin: 0.75rem 0 1.25rem; - font-size: 0.82rem; -} - -.legend span::before { - content: ""; - display: inline-block; - width: 0.75em; - height: 0.75em; - margin-right: 0.4em; - border-radius: 3px; - vertical-align: -0.05em; -} - -.legend .l-engine::before { - background: var(--engine); -} -.legend .l-authoring::before { - background: var(--authoring); -} -.legend .l-worker::before { - background: var(--worker); -} -.legend .l-mc::before { - background: var(--mc); -} -.legend .l-memory::before { - background: var(--memory); -} -.legend .l-ui::before { - background: var(--ui); -} - -/* ---- Tables ----------------------------------------------------------- */ - -table { - border-collapse: collapse; - width: 100%; - margin: 0.75rem 0 1.25rem; - font-size: 0.88rem; -} - -th, -td { - border: 1px solid var(--line); - padding: 0.4rem 0.6rem; - text-align: left; - vertical-align: top; -} - -th { - background: #f6f6f6; - font-weight: 600; -} - -/* ---- Generic diagram primitives -------------------------------------- */ - -.box { - border: 1.5px solid var(--ink); - border-radius: 8px; - padding: 0.55rem 0.8rem; - background: #fff; -} - -.box strong { - display: block; - font-size: 0.9rem; -} - -.box small { - color: var(--muted); - font-size: 0.78rem; - line-height: 1.35; - display: block; -} - -.box.b-engine { - border-color: var(--engine); - background: var(--engine-bg); -} -.box.b-engine strong { - color: var(--engine); -} -.box.b-authoring { - border-color: var(--authoring); - background: var(--authoring-bg); -} -.box.b-authoring strong { - color: var(--authoring); -} -.box.b-worker { - border-color: var(--worker); - background: var(--worker-bg); -} -.box.b-worker strong { - color: var(--worker); -} -.box.b-mc { - border-color: var(--mc); - background: var(--mc-bg); -} -.box.b-mc strong { - color: var(--mc); -} -.box.b-memory { - border-color: var(--memory); - background: var(--memory-bg); -} -.box.b-memory strong { - color: var(--memory); -} -.box.b-ui { - border-color: var(--ui); - background: var(--ui-bg); -} -.box.b-ui strong { - color: var(--ui); -} - -/* Vertical pipeline: boxes separated by a downward arrow. */ -.pipeline { - display: flex; - flex-direction: column; - gap: 0; - margin: 1rem 0 1.5rem; - max-width: 620px; -} - -.pipeline > .step { - position: relative; -} - -.pipeline > .step + .step { - margin-top: 1.6rem; -} - -.pipeline > .step + .step::before { - content: "▼"; - position: absolute; - top: -1.45rem; - left: 2rem; - font-size: 0.8rem; - color: var(--muted); -} - -.pipeline > .step + .step[data-arrow]::after { - content: attr(data-arrow); - position: absolute; - top: -1.5rem; - left: 3.4rem; - font-size: 0.75rem; - color: var(--muted); - font-family: ui-monospace, Menlo, monospace; -} - -/* Horizontal flow: boxes with a → between them. */ -.hflow { - display: flex; - flex-wrap: wrap; - align-items: stretch; - gap: 0.35rem; - margin: 1rem 0 1.5rem; -} - -.hflow > .box { - flex: 1 1 0; - min-width: 118px; -} - -.hflow > .arr { - align-self: center; - color: var(--muted); - font-size: 1rem; - padding: 0 0.1rem; -} - -/* Two/three column thread lanes. */ -.lanes { - display: grid; - gap: 1rem; - margin: 1rem 0 1.5rem; -} - -.lanes.two { - grid-template-columns: 1fr 1fr; -} -.lanes.three { - grid-template-columns: 1fr 1fr 1fr; -} - -.lane { - border: 1px dashed #bbb; - border-radius: 10px; - padding: 0.75rem; - background: #fcfcfc; -} - -.lane > h4 { - margin: 0 0 0.6rem; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--muted); -} - -.lane .box { - margin-bottom: 0.6rem; -} - -.lane .box:last-child { - margin-bottom: 0; -} - -/* ---- Byte-layout bars -------------------------------------------------- */ - -.bytes { - display: flex; - width: 100%; - margin: 0.75rem 0 0.25rem; - border: 1.5px solid var(--ink); - border-radius: 6px; - overflow: hidden; - font-family: ui-monospace, Menlo, monospace; - font-size: 0.72rem; - text-align: center; -} - -.bytes .seg { - padding: 0.45rem 0.2rem; - border-right: 1.5px solid var(--ink); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.bytes .seg:last-child { - border-right: none; -} - -.bytes .seg em { - display: block; - font-style: normal; - color: var(--muted); - font-size: 0.66rem; -} - -.seg.s-header { - background: #ededed; -} -.seg.s-u32 { - background: #d3e5ff; -} -.seg.s-f64 { - background: #fecaca; -} -.seg.s-u8 { - background: #d1fae5; -} -.seg.s-tokens { - background: #fee2b8; -} - -.bytes-caption { - font-size: 0.78rem; - color: var(--muted); - margin: 0.15rem 0 1.25rem; -} - -/* ---- Callouts ----------------------------------------------------------- */ - -.note { - border-left: 4px solid #eab308; - background: #fefce8; - padding: 0.6rem 0.9rem; - margin: 1rem 0; - border-radius: 0 6px 6px 0; - font-size: 0.9rem; -} - -.note.refactor { - border-left-color: var(--memory); - background: var(--memory-bg); -} - -.note strong { - display: inline; -} - -/* ---- At-a-glance card ---------------------------------------------------- */ - -.glance { - display: grid; - grid-template-columns: max-content 1fr; - gap: 0.3rem 1.2rem; - border: 1px solid var(--line); - border-radius: 8px; - padding: 0.85rem 1rem; - margin: 1rem 0 1.5rem; - font-size: 0.88rem; - background: #fafafa; -} - -.glance dt { - font-weight: 600; - color: var(--muted); -} - -.glance dd { - margin: 0; -} - -/* ---- SVG diagrams --------------------------------------------------------- */ - -figure { - margin: 1rem 0 1.5rem; -} - -figure svg { - max-width: 100%; - height: auto; -} - -figcaption { - font-size: 0.78rem; - color: var(--muted); - margin-top: 0.25rem; -} - -@media (max-width: 720px) { - .lanes.two, - .lanes.three { - grid-template-columns: 1fr; - } -} diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/authoring.html b/libs/@hashintel/petrinaut-core/docs/architecture/authoring.html deleted file mode 100644 index ae048fa3b0a..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/authoring.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - Simulation Architecture — Compilation (Authoring) - - - - - -

Compilation (Authoring)

-

- The HIR pipeline turns user-authored TypeScript into checked, - source-spanned buffer programs. Scenario code retains a separate - same-realm compiler hardened against accidents, not attackers. -

- -
-
Inputs
-
- Code strings stored on the SDCPN document (lambda, kernel, dynamics, - metric, scenario) -
-
Outputs
-
- Versioned HIR artifacts for runtime code; InitialMarking - for scenarios -
-
Errors
-
- Thrown at compile time with the offending item’s ID - (SDCPNItemError) → surfaced as protocol - error messages / result objects -
-
Runs on
-
- LSP worker (HIR compilation) · simulation workers (buffer programs) · - main thread (scenarios and timeline metric evaluation) -
-
- -

The compilation pipeline (module-style code)

-

- Dynamics, lambdas, kernels, and metrics all compile through the HIR. The - first three are authored as export default Wrapper(fn) - modules; metrics are authored as function bodies. -

- -
-
- User source - export default Lambda((input, parameters) => …) - (TypeScript allowed) -
- -
- Lower to HIR - Parse the supported TypeScript subset with exact source spans -
- -
- Check + analyze - Validate against places, token layouts, parameters, and output - shapes -
- -
- Emit artifact - Generate buffer-native JS with offsets and strides baked in -
- -
- Instantiate in runtime - Validate v4 fingerprint, then bind parameters, pool, and ABI - views -
-
- -
- The - LSP virtual files - (lsp/generate-virtual-files.ts) are the type-level twin of - this pipeline: they generate the Color_*, - Parameters, Dynamics, - TransitionKernel… declarations that Monaco checks against, so - editor diagnostics and runtime artifacts are produced from the same HIR - checks and emitters. -
- -

What gets compiled, where it runs

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SurfaceAuthored asCompiled byExecuted inSignature at runtime
Transition lambdamodule, Lambda(fn)compileHirArtifacts in the LSP workerWorker, per enablement check - (f64, u64, u8, placeBases, indices) → boolean | rate -
Transition kernelmodule, TransitionKernel(fn)compileHirArtifacts in the LSP workerWorker, per firing - (views, placeBases, indices, outputViews, sink) → void -
Dynamicsmodule, Dynamics(fn) - compileHirArtifacts (colours with a real element) - Worker, per step per place - (placeBytes, tokenCount) → Float64Array -
Metricplain function body (no module)compileHirArtifacts in the LSP workerMain thread (timeline) · MC worker (experiments) - (f64, u64, u8, placeCounts, placeOffsets) → finite number - — NaN/∞ throw -
Scenarioper-place expressions or one code-mode bodyauthoring/scenario/compile-scenario.tsMain thread, before createSimulationInitialMarking (+ scenario parameter values)
Place visualizermodule, Visualization(fn) returning JSX - Host UI package (@hashintel/petrinaut, - ui/lib/compile-visualizer.ts — the one inherently - React-based surface, since visualizers author JSX) - Main thread render({tokens, parameters}) → ReactElement
- -

Scenario compilation → InitialMarking

- -
-
-

per_place mode

-
- Uncoloured place → expression string - Evaluated with parameters and scenario in - scope; result rounded and clamped >= 0 (a token count). -
-
- Coloured place → row arrays - Rows in colour element order; each row coerced through - coerceTokenRecord (typed defaults for missing columns, - extra columns throw). -
-
-
-

code mode

-
- One function body - Returns { PlaceName: count | TokenRecord[] } keyed by - place name (per_place uses place IDs — a known - asymmetry). Unknown names are silently dropped. -
-
- Result: InitialMarking (JSON) - Keyed by place ID. Fed to createSimulation, packed to - binary in buildSimulation. -
-
-
- -

Metric compilation and evaluation

-
-
- compileHirArtifacts - Lower, typecheck, and emit a buffer-native metric program. -
- -
- createHirMetricEvaluator - Bind referenced place ordinals and the per-run string pool. -
- -
- Per-frame evaluation - Quick sim: evaluated on the main thread against stored frames. - Experiments: evaluated inside the MC worker per run per frame - (metrics/specs.ts). -
-
- -

Scenario sandboxing (authoring/sandbox.ts)

-
    -
  • - Shadowed globals: scenario expressions declare - var window, document, globalThis, self, fetch, XMLHttpRequest, - importScripts, Function, setTimeout, setInterval, queueMicrotask - — name lookups resolve to undefined. -
  • -
  • - runSandboxed(action): blocks the - constructor-chain escape (({}).constructor.constructor → - Function) for the duration of the call, since shadowing - only stops identifier lookup. -
  • -
  • - Same realm by design: user code executes with - new Function in the worker/main realm. Treat this as - robustness hardening (typos, accidental API use), not isolation. -
  • -
- -

Distributions (deferred sampling)

-

- HIR-emitted kernels construct small runtime values for - Distribution.Gaussian / Uniform / Lognormal. Kernels defer - sampling through the ABI sink; the engine samples each value once per - token with the seeded RNG (sample-distribution.ts), so - chained .map(fn) transforms share a single draw and runs stay - reproducible. When stochasticity is disabled, the runtime isn’t injected - and plain values are required. -

- -
- Compatibility seam. HIR artifacts contain a version and - fingerprint of their sanitized compilation input. Any schema, code, or - extension change requires recompilation before the engine will run them. -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/dependency-diagrams.md b/libs/@hashintel/petrinaut-core/docs/architecture/dependency-diagrams.md deleted file mode 100644 index 7fca3d59c08..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/dependency-diagrams.md +++ /dev/null @@ -1,26 +0,0 @@ -# Petrinaut dependency diagrams - -These diagrams are generated from Petrinaut's TypeScript imports with -[dependency-cruiser](https://github.com/sverweij/dependency-cruiser) and laid -out with [D2](https://d2lang.com/) using ELK. - -## Project modules - -[Open the project dependency diagram](./petrinaut-dependencies.svg). - -![Dependencies between Petrinaut modules](./petrinaut-dependencies.svg) - -## Compilation and execution path - -[Open the focused compilation dependency diagram](./petrinaut-compilation-dependencies.svg). - -![Dependencies around the LSP, HIR compilation, and simulation runtimes](./petrinaut-compilation-dependencies.svg) - -Regenerate both diagrams from the repository root: - -```sh -yarn workspace @hashintel/petrinaut-core doc:dependency-diagram -``` - -The checked-in `.d2` files are the readable graph sources; the `.svg` files are -generated views. Test and Storybook files are intentionally excluded. diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/engine.html b/libs/@hashintel/petrinaut-core/docs/architecture/engine.html deleted file mode 100644 index d135b93ba2c..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/engine.html +++ /dev/null @@ -1,770 +0,0 @@ - - - - - - Simulation Architecture — Engine - - - - - -

Engine

-

- engine/ + frames/ — builds an SDCPN into a - runnable SimulationInstance and advances it one immutable - binary frame at a time. -

- -
-
Entry points
-
- buildSimulation(input) · - computeNextFrame(simulation) -
-
Inputs
-
- SDCPN snapshot, InitialMarking, parameter values, seed, dt, - maxTime (number | null; null = no limit), extensions -
-
Outputs
-
- New EngineFrame per step + - completionReason: "maxTime" | "deadlock" | null -
-
Events
-
- None — pure return values; the worker turns - them into protocol messages -
-
Runs on
-
- Worker thread (quick sim). The same engine functions are reused by - Monte Carlo with different frame - storage -
-
- -

Lifecycle

- -
-
- buildSimulation(input) - Flattens component instances, compiles user code (authoring): lambdas only where available, kernels only for coloured outputs, - dynamics only for colours with at least one real element. - Packs the initial marking into frame 0 (coercing token values by - element type). -
-
- SimulationInstance - frames: EngineFrame[] (history, frame 0 included) · - frameLayout · compiledTransitions · - differentialEquationFns · parameterValues · - dt · maxTime · currentTime · currentFrameNumber · rngState -
-
- computeNextFrame(simulation) - Returns a new instance value with one more frame appended — - the previous frames are never mutated. -
-
- -

One step, in order

- -
-
- 1 · maxTime? - If maxTime !== null and - currentTime >= maxTime, return - "maxTime" without computing. -
- -
- 2 · Dynamics - Per place with a differential equation: slice its token region, - decode tokens, call the user Dynamics fn, apply Euler - x += dx·dt to real slots (discrete derivatives - are forced to 0). Builds an intermediate frame. -
- -
- 3 · Transitions - executeTransitions: for each transition — enablement - (arc weights, inhibitor/read arcs), token-combination enumeration, - lambda (predicate / stochastic rate vs seeded RNG), kernel for - coloured outputs. Removals compact the buffer; additions - append. -
- -
- 4 · Timers - If nothing fired, advance every transition’s - timeSinceLastFiringMs by dt. -
- -
- 5 · Complete? - maxTime reached, or deadlock (nothing fired and no - transition is enabled). -
-
- -
- Every stage that changes state allocates a new - ArrayBuffer via createEngineFrame() — dynamics, - token removal, and token insertion each rebuild the frame. Immutability - makes history trivially correct at the cost of allocation churn per step. -
- -

EngineFrame — the binary format

-

- One ArrayBuffer, read through section-typed views. The frame - stores no IDs and no time — decoding requires the - EngineFrameLayout (place/transition order, per-place - strideBytes and TokenSlotLayout) derived from - the SDCPN, and time travels as payload metadata. -

-

- Source of truth: - libs/@hashintel/petrinaut-core/src/simulation/frames/internal-frame.ts - — createEngineFrame() computes every offset below and is the - only frame constructor; readEngineFrame() recreates the same - views for reading; the header constants live in the - HeaderOffset enum. -

- -
- - - - - - - - - - Memory map — example instance - - - byte offsets on the left · one typed-array view per section on the - right - - - - - - header — 64 B fixed - - magic "PFRM" · version · P · T · - - - section offsets · byteLength - - DataView, little-endian - - - - place token counts - u32 × P = 8 B - Uint32Array(buf, 64, P) - - tokens currently in each place - - - - - place value offsets - u32 × P = 8 B - Uint32Array(buf, 72, P) - - each place’s token run, relative to the token region start - - - - - transition elapsed - f64 × T = 16 B - Float64Array(buf, 80, T) - - ms since each transition last fired - - - - - transition firing counts - u32 × T = 8 B - Uint32Array(buf, 96, T) - - cumulative firings since frame 0 - - - - - - fired flags - - — u8 × T = 2 B - - - Uint8Array(buf, 104, T) - - - - padding — 6 B - - alignTo(…, 8) so f64 views stay aligned - - - - - - tokenBytes / tokenF64 views (b112…) - - - packed token structs — place-major, token-major; - - - this all-real example is pure f64 (booleans = u8) - - - - - - slot 0 · b112 · p1 · tok0 · x - - slot 1 · b120 · p1 · tok0 · y - - slot 2 · b128 · p1 · tok0 · z - - slot 3 · b136 · p1 · tok1 · x - - slot 4 · b144 · p1 · tok1 · y - - slot 5 · b152 · p1 · tok1 · z - - slot 6 · b160 · p2 · tok0 · a - - slot 7 · b168 · p2 · tok0 · b - - - - - p1 — byteOffset 0 - - 2 tokens × stride 24 B (x,y,z — all real) - - - p2 — byteOffset 48 - - 1 token × stride 16 B (a,b) → byte 112 + 48 - - - - - 0 - 64 - 72 - 80 - 96 - 104 - 106 - 112 - 176 - - - - byteLength = 176 (recorded in the header, checked on read) - - -
- Exact offsets for a concrete instance: P = 2 places (p1 - = 2 tokens × 24-byte stride, p2 = 1 token × 16-byte stride — all - dimensions real here, so every field is one f64; a boolean - dimension would appear as a single u8 byte inside its token’s stride), - T = 2 transitions → 176-byte frame. Sections are laid - out by createEngineFrame(); sections and strides are padded - to 8-byte boundaries by alignTo(). Block heights are not to - scale. -
-
- -

Header (offsets in bytes, little-endian, via DataView)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OffsetFieldTypeValue / meaning
0magicu320x5046524d (“PFRM”) — corrupt-frame guard
4versionu162 — the current (only) format; readers assert this on decode
6headerBytesu1664
8 / 12placeCount / transitionCountu32must match the layout (checked on read)
16tokenByteLengthu32token region length in bytes
20–40section offsetsu32 × 6byte offsets of each section above
44byteLengthu32whole-frame length (checked on read)
- -

Token region

-

- Each coloured place owns a contiguous run of - count × strideBytes bytes starting at its byte offset - (uncoloured places have stride 0 and only a count), giving O(1) access to - any place via the layout. Within a token, each element sits at its - layout-computed byte offset: real/integer as - f64, boolean as one u8 byte. Value coercion (integers - rounded, booleans 0/1) lives in engine/token-values.ts; byte - placement in engine/token-layout.ts. -

- -

Who reads and writes the buffer

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathDirectionView usedNotes
createEngineFrame(layout, snapshot)write (allocate) - DataView header + section views, bulk - Uint8Array.set - - Only constructor of frames; recomputes byte offsets from counts × - strides. -
- readEngineFrame(layout, frame) → - EngineFrameView - read - Uint32Array sections + tokenBytes (u8) / - tokenF64 views - - Zero-copy; toSnapshot() copies the token region into a - fresh Uint8Array. -
Dynamics (computePlaceNextState)read + writef64 view over a fresh byte copy - Euler touches only realFieldF64Offsets; discrete bytes - copied through untouched. -
- Transition firing (compute-possible-transition, - execute-transitions, remove-tokens…) - read + write - readTokenRecord / - encodeTokenValuesToBytes + byte-range copies - - Input tokens decoded to TokenRecords for user - lambdas/kernels; kernel outputs packed into per-token - Uint8Array blocks. -
SimulationFrameReader (main thread)readSame section views over the cloned buffer - getPlaceTokens() = decoded records via the place’s - TokenSlotLayout; getTransitionState() = - timers/flags. -
- -

Token attribute typing (discrete types)

-
    -
  • - Storage is schema-driven: real and - integer elements are f64 fields; - boolean elements are single u8 bytes, placed by - computeTokenSlotLayout. -
  • -
  • - Coercion at the write boundary - (coerceTokenRecord, - encodeTokenAttributeValue): integers round, booleans become - 0/1; initial markings, scenario rows, and kernel outputs all pass - through it before writeTokenValue / - encodeTokenToBytes place the bytes. -
  • -
  • - Decode at the read boundary - (readTokenRecord): user code and the UI always see - number | boolean values. -
  • -
  • - Only transition kernels write discrete values. Dynamics - apply to real elements only — Euler integrates - realFieldF64Offsets exclusively, discrete bytes are copied - through untouched, and colours without real elements skip dynamics - compilation entirely. -
  • -
  • - Distributions (Distribution.Gaussian…) are allowed for - real/integer kernel outputs and rejected for boolean. -
  • -
- -

Token memory layout — packed structs

-

- Since FRAME_VERSION = 2, tokens are stored as schema-driven - packed structs (array-of-structs; column layout was - considered and rejected because the workload — kernels, copies, UI reads — - is token-oriented). - engine/token-layout.ts (computeTokenSlotLayout) - is the single source of truth: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Logical typePhysical typeNotes
realf64 — 8 Bunchanged
integerf64 — 8 B, rounded on read/write - Exact only within ±2^53 (documented in the schema). - i32 rejected (silent wraparound at ±2^31); i64 rejected (bigint is - contagious into user code: fortune * 1.05 would throw). - If >2^53 is ever needed, add a separate opt-in - int64 element type instead of changing - integer. -
booleanu8 — 1 B - Not bit-packed (would break byte-granular copies for marginal - savings). -
uuid (128-bit, FE-1121)u64 × 2 — 16 B - Two little-endian lanes read via the shared - BigUint64Array view, combined to one - bigint at the boundary (~28 ns; lane-compare without - combining for equality, ~4× faster). Never routed through - number (NaN-payload hazard). Kernel outputs are - optional — omitted values auto-generate from the seeded RNG. -
string (FE-769)u64 pool reference — 8 B - The frame stores an ID into an append-only per-run string intern - pool (engine/string-pool.ts); the pool lives on - SimulationInstance, not on the frame, so frames stay - fixed-stride and byte-copyable. Equal strings share one ID; id 0 is - the pre-seeded "", so zeroed buffers decode cleanly. -
-
    -
  • - Layout rule: per colour, fields are ordered by - decreasing alignment (stable) and the stride is rounded up to 8 B — - every f64 field stays 8-aligned so hot paths use plain typed-array views - (no DataView). Places are byte-addressed: - { byteOffset, count, strideBytes }. -
  • -
  • - Access rule: token-layout.ts (readTokenRecord - / writeTokenValue / encodeTokenToBytes) is the - only code that indexes token bytes; all whole-token moves are byte-range - copies (Uint8Array.set). The raw - getPlaceTokenValues reader was removed from the public API - — raw f64 access is meaningless under mixed widths. Because the string - pool never crosses the worker boundary with the frames, each frame - payload ships an append-only newStrings delta that the - main-thread frame store accumulates and hands to the frame reader for - decoding. -
  • -
- -

Determinism

-

- seeded-rng.ts provides a pure - nextRandom(state) → [value, nextState]. The RNG state lives - on SimulationInstance.rngState and is threaded through - stochastic lambda sampling and distribution draws, so a given - (SDCPN, marking, parameters, seed, dt) always reproduces the - same frame sequence. -

- -
- Refactoring seams. - (1) Per-step allocation: each stage rebuilds the whole buffer — a - double-buffer strategy (as Monte Carlo already does) would remove most - churn. (2) 128-bit types (UUID, FE-1121) slot into the layout as - u64×2 fields (align 8, read via BigUint64Array) - — the layout machinery is ready; the value plumbing (bigint - boundary, v5 coercion, seeded generation) is not. (3) The worker keeps the - full frames[] history although only the latest frame is - needed to advance — history retention belongs to the main-thread store. -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/index.html b/libs/@hashintel/petrinaut-core/docs/architecture/index.html deleted file mode 100644 index 0df5a6d6e35..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/index.html +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - Petrinaut Simulation — Architecture Overview - - - - - -

Simulation Architecture — Overview

-

- How Petrinaut runs SDCPN simulations: who owns the memory, what crosses - thread boundaries, and how results are read back. Code lives in - petrinaut-core/src/simulation/ (engine, authoring, worker, - runtime, monte-carlo). These pages describe the headless core only — - consumers appear as "the host application" using the public API; the React - integration is documented separately in - @hashintel/petrinaut/ARCHITECTURE.md. Generated import maps spanning core, React, and UI modules are in the - dependency diagrams. -

- -
- Engine (stepping & frames) - Compilation (user code) - Worker & protocol - Monte Carlo - Memory / buffers - Host application -
- -

The two execution paths

-

- There are two independent ways to execute a net. - Quick Simulation runs one interactive simulation and - keeps every frame so the host can scrub. - Experiments run many Monte Carlo simulations with - bounded memory (two reusable buffers per run) and only ship - metric aggregates to the host — frame buffers never leave the worker. -

- -
-
-

Main thread — host application

-
- Run configuration - owns InitialMarking, parameter values, seed/dt/maxTime - (null = unbounded); calls createSimulation() -
-
- Playback driver - core playback module — picks the viewed frame - (getFrame(i)), drives ack/backpressure per play - mode -
-
- Experiment consumer - one MonteCarloExperiment handle per experiment, - subscribed to its stores -
-
- Rendering & metrics - read via SimulationFrameReader / metric frames -
-
-
-

Main thread — core runtime

-
- createSimulation() - runtime/simulation.ts — sanitizes SDCPN for extensions, flattens - component instances, owns lifecycle stores + events -
-
- SimulationFrameStore - runtime/frame-store.ts — retains every - SimulationFramePayload (ArrayBuffer + time) -
-
- SimulationFrameReader - frames/frame-reader.ts — typed-array views over a stored frame, - zero-copy reads -
-
- createMonteCarloExperiment() - monte-carlo/runtime/experiment.ts — status / progress / metrics - stores -
-
-
-

Worker threads

-
- simulation.worker - init → buildSimulation(); loop → - computeNextFrame(); streams frames under ack - backpressure -
-
- SimulationInstance.frames[] - full EngineFrame history — the compute source of truth -
-
- monte-carlo.worker - MonteCarloSimulator — round-robin - advanceAll(), metrics observed in-worker -
-
- 2 × ArrayBuffer per run - current / next, swapped each step; no history -
-
-
- -

Where the memory actually lives

-

- Everything the simulation computes is stored in raw - ArrayBuffers read through typed-array views. There is - no object graph of tokens — tokens are packed structs - (real/integer as f64 fields, - boolean as one u8 byte, stride rounded to 8 B), decoded to JS - objects only at the read boundary. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MemoryStructureThreadLifetime / role
SDCPN documentPlain JS objects (editor state)Main - Snapshot is structured-cloned into the worker at init; - later edits don’t affect a running simulation. -
Initial marking - JSON (InitialMarking: count or - TokenRecord[] per place) - Main (host state) - Packed into frame 0 by - buildSimulation → packInitialPlaceMarking. -
EngineFrame (quick sim) - ArrayBuffer: 64-byte header + - Uint32/Float64/Uint8 sections - + packed token structs (byte-addressed places) - Worker - Immutable snapshot per step. The worker appends every frame to - SimulationInstance.frames[] — compute source of truth. -
SimulationFramePayload{ time, frame: ArrayBuffer }Worker → Main - Structured-clone copy per frame (no transfer list). - Retained forever by the in-memory frame store for scrubbing. -
SimulationFrameReader - Uint32Array/Float64Array views over the - stored buffer - Main - Zero-copy views. getPlaceTokens() materializes typed - TokenRecord objects via the place’s - TokenSlotLayout. -
Monte Carlo run buffers - 2 × ArrayBuffer per run (current / next), token-value - region has capacity + growth policy - MC worker - Swapped each step. Never sent to the main thread — only progress - counters and metric frames (small JSON) cross the boundary. -
Compiled user codeVersioned HIR buffer programs; scenarios remain plain JSWorkers (HIR programs) · Main (scenarios, timeline metrics) - Produced by authoring at init time. -
- -
- Refactoring seam — triple retention. Each quick-sim frame - currently exists three times: in the worker history - (SimulationInstance.frames[]), as a structured-clone copy in - the main-thread frame store, and transiently in the - postMessage queue. Frames are never transferred - (Transferable) and the worker history is only read by the - stepping loop’s latest entry. Retention policy is deliberately isolated - behind runtime/frame-store.ts so this can change without - touching consumers. -
- -

Quick Simulation — end-to-end data flow

- -
-
- SDCPN snapshot + InitialMarking + parameters + - seed/dt/maxTime - Assembled by the host (main thread). Scenario compilation may have - produced the marking and parameter overrides first. -
-
- simulation.worker — buildSimulation() - Sanitize by extensions → compile lambdas / kernels / dynamics → pack - frame 0. Replies frame(0) + ready. -
-
- computeNextFrame() × batch - Dynamics (Euler) → transitions (enablement, lambda, kernel) → new - immutable EngineFrame appended; time += dt. -
-
- SimulationFrameStore (main) - Appends every payload; publishes {count, latest} through - the frames store. -
-
- SimulationFrameReader - compileSimulationFrameReader(sdcpn) specialises the - layout once; readers are created per frame on demand (latest() - / getFrame(i)). -
-
- Host reads - The playback driver picks frameIndex; the host renders - from getTransitionState(), getPlaceTokens(), - and getPlaceTokenCount(); metric code reads the raw - packed frame through a HIR evaluator. -
-
- -

Protocols at a glance

-

- Both workers speak a small typed message protocol over - postMessage, wrapped in a - SimulationTransport (queues messages until the worker boots). - Full payloads and sequence diagrams are on the - Worker and - Monte Carlo pages. -

- - - - - - - - - - - - - - - - - - - - - - - - -
Host → WorkerWorker → HostFlow control
- Quick sim
worker/messages.ts -
- init · start · pause · stop · setBackpressure · ack - - ready · frame · frames · paused · complete · error - - Ack-based backpressure: worker computes at most - maxFramesAhead past the last acked frame. -
- Monte Carlo
monte-carlo/worker/messages.ts -
init · start · cancel - ready · progress · metricFrames · complete · cancelled · - error - - Fire-and-forget batches (advanceAll() × batchSize per - loop tick); cancellation checked between batches. -
- -

Module map

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AreaPathOwnsDetails
Engineengine/, frames/Build + stepping, EngineFrame binary format, frame readersengine.html
Compilationauthoring/ - User code → JS functions (lambda, kernel, dynamics, metric, - scenario), sandbox hardening - authoring.html
- Worker + runtime - worker/, runtime/ - Transport protocol, backpressure, lifecycle stores, frame retention - worker.html
Monte Carlomonte-carlo/ - Batch runs, bounded buffers, metric pipeline, experiment handle - monte-carlo.html
- -

Design invariants worth keeping

-
    -
  • - Frames are opaque outside the engine. - EngineFrame is “not a public API or stable storage format”; - it can only be decoded with the SDCPN-derived - EngineFrameLayout. All consumers go through - SimulationFrameReader. -
  • -
  • - Simulation time lives outside the frame. The run - controller owns frame number and time; payloads carry time as metadata. -
  • -
  • - Runs are snapshots. A simulation never re-reads the - live document; extension sanitization + component flattening happen once - at init on both sides of the boundary (they must agree on the place - list, or the layout check throws). -
  • -
  • - Determinism. One seeded RNG - (seeded-rng.ts) threaded through lambda sampling and - distribution draws; same seed → same run. -
  • -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html b/libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html deleted file mode 100644 index 31a10a8eeba..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html +++ /dev/null @@ -1,376 +0,0 @@ - - - - - - Simulation Architecture — Monte Carlo (Experiments) - - - - - -

Monte Carlo (Experiments)

-

- monte-carlo/ — runs many independent simulations of the same - net with bounded memory, aggregates metrics inside the worker, - and ships only small JSON metric frames to the UI. Frame buffers never - cross the thread boundary. -

- -
-
Core
-
- createMonteCarloSimulator(config) → - MonteCarloSimulator (synchronous, thread-agnostic) -
-
Worker
-
- monte-carlo.worker.ts + worker/messages.ts -
-
Host handle
-
- createMonteCarloExperiment() → stores: - status · progress · metrics + events; - start/cancel/dispose -
-
Inputs
-
- SDCPN, marking, parameters, seed, dt, - maxTime (required), runCount, metric specs -
-
Outputs
-
- MonteCarloWorkerProgress (run counters) + - MonteCarloUserDefinedMetricFrame[] -
-
- -

Layers

- -
-
- Host application - One experiment record per run of - createMonteCarloExperiment(); subscribes to the handle’s - status/progress/metrics stores and renders the metric frames. -
-
- Experiment handle (main) - runtime/experiment.ts — worker mode - (createWorker/transport) or - local mode (runs the simulator on the calling thread, used by - tests/embedding). -
-
- monte-carlo.worker - Builds the simulator + compiles metric specs, then loops: - advanceAll() × batchSize (default 4), post - progress + pending metricFrames, yield, - repeat. cancel is honoured between batches. -
-
- MonteCarloSimulator - Owns N × MonteCarloRun; deterministic round-robin - advanceAll() advances every active run one frame per - call, so long runs don’t starve short ones. -
-
- Engine functions, reused - Same enablement/lambda/kernel/dynamics code as quick sim (transition-effect.ts - adapts them to the MC buffers). -
-
- -

Run model

- - - - - - - - - - - - - - - - - - - - - - - - - -
Per runMeaning
- seed, parameterValues, - initialMarking - - Defaults derived from the experiment config; overridable per run - (runs[]), e.g. seed = base seed + index. -
status - ready → running → complete | error — errors are per - run, other runs continue. -
- frameNumber · currentTime · rngState · completionReason - - Progress; a run completes on its own deadlock or the shared - maxTime. -
- tokenByteCount · tokenByteCapacity · reallocations - - Buffer telemetry, exposed in MonteCarloRunSummary. -
- -

Memory — two buffers per run, swapped every step

- -
-
-

step k

-
- currentFrame (read) state at frame k -
-
- nextFrame (write) - dynamics + transitions write frame k+1 here -
-
-
-

after the step

-
- swap pointers - current ⇄ next — no allocation, no history. If the - next token count doesn’t fit, the target buffer alone reallocates: - nextCapacityBytes = max(requiredBytes, capacity × 2, 64). -
-
- metrics observe frame k+1 - then the data may be overwritten — readers are only valid during - observeFrame. -
-
-
- -

- The buffer layout (frame-buffer.ts) is a leaner sibling of - the quick-sim EngineFrame: same sections, - no 64-byte header, one extra - transitionElapsedFrames section, and a token region with - spare capacity: -

- -
-
- place countsu32 × P -
-
- place offsetsu32 × P -
-
- transition elapsedf64 × T -
-
- elapsed framesf64 × T -
-
- firing countsu32 × T -
-
- fired flagsu8 × T -
-
- token structsbytes, used ≤ capacity -
-
- sparecapacity -
-
-

- All views - (Uint32Array/Float64Array/Uint8Array) - are created once per buffer over a single ArrayBuffer; IDs - resolve to dense indices through the shared EngineFrameLayout - (layout.ts). -

- -

Metrics pipeline — computed where the data is

- -
-
- Specs - MonteCarloMetricSpec: expression (metric - code body) · placeTokenCountMean · - transitionFiringCount — serializable, sent in - init. -
- -
- Compile before worker start - Expression metrics carry HIR artifacts; built-in specs create a - measure(run) function directly. -
- -
- Sample per frame - observeFrame(ctx) visits every run’s current frame as a - SimulationFrameReader (forEachRunFrame); - sampleRuns filters active/completed/all. -
- -
- Aggregate - Across runs: mean·sum·min·max·last → scalar, or keep the - run axis and bin it → distribution (exact or bin - width). Optionally aggregate over time. -
- -
- Metric frames - MonteCarloUserDefinedMetricFrame — scalar - (value/frameValue/timeValue) or distribution (bins: [value, frequency][]) per frame. Small JSON. -
-
- -

Protocol (monte-carlo/worker/messages.ts)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DirectionTypePayload
Host → Workerinit - sdcpn, extensions?, initialMarking, parameterValues, seed, dt, - maxTime, runCount, batchSize?, metricSpecs? -
start
cancel— (checked between batches)
Worker → Hostready
progress - MonteCarloWorkerProgress = advance counters (advancedRuns, completedRuns, erroredRuns, activeRuns, - allFinished) + frameNumber, time, runCount -
metricFramesPending MonteCarloUserDefinedMetricFrame[] batch
complete / cancelledFinal (or last-known) progress
errormessage, itemId
- -
- Contrast with quick sim: no ack/backpressure — the worker - free-runs to completion in small batches; the only upstream control is - cancel. And the payloads are aggregates, not frames: the UI - never holds Monte Carlo simulation state. -
- -

What the host receives

-
    -
  • - The handle exposes status / progress / - metrics stores (frames + - latestByMetricId); completion disposes the worker while the - accumulated metric frames remain available for display. -
  • -
  • - Scalar metric frames carry value / - frameValue / timeValue — directly plottable as - a per-frame series or a time-aggregated number. -
  • -
  • - Distribution metric frames keep the run axis as - bins: [value, frequency][] — hosts can paint a bins × - frames heatmap and derive run aggregations (mean, median, percentiles) - from the bins, all without ever holding simulation state. -
  • -
  • - How Petrinaut’s React UI renders these is described in - @hashintel/petrinaut/ARCHITECTURE.md. -
  • -
- -
- Refactoring seams. - (1) Runs are round-robin on one worker thread; the run-state model was - designed so runs can shard across multiple workers later. (2) User code - still receives decoded TokenRecord objects per firing — the - README’s planned “IR compilation” would let lambdas/kernels operate - directly on the numeric buffers. (3) Buffer growth is a naive ×2 doubling; - arc-weight static analysis could size buffers up front and eliminate - reallocations. -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-compilation-dependencies.d2 b/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-compilation-dependencies.d2 deleted file mode 100644 index 44b4b9019e1..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-compilation-dependencies.d2 +++ /dev/null @@ -1,76 +0,0 @@ -# Generated by scripts/generate-dependency-diagrams.mjs. Do not edit. - -direction: right - -# modules - -"core / HIR artifacts": {class: core; tooltip: "1 source file"} -"core / HIR compiler": {class: core; tooltip: "7 source files"} -"core / HIR compiler API": {class: core; tooltip: "1 source file"} -"core / HIR emitters": {class: core; tooltip: "2 source files"} -"core / HIR runtime API": {class: core; tooltip: "2 source files"} -"core / LSP client & transport": {class: core; tooltip: "4 source files"} -"core / LSP services": {class: core; tooltip: "10 source files"} -"core / LSP worker": {class: core; tooltip: "3 source files"} -"core / Monte Carlo runtime": {class: core; tooltip: "22 source files"} -"core / simulation assembly": {class: core; tooltip: "1 source file"} -"core / simulation controller": {class: core; tooltip: "3 source files"} -"core / simulation frames & metrics": {class: core; tooltip: "4 source files"} -"core / simulation worker": {class: core; tooltip: "5 source files"} -"React / experiments provider": {class: react; tooltip: "2 source files"} -"React / LSP provider": {class: react; tooltip: "3 source files"} -"React / simulation provider": {class: react; tooltip: "4 source files"} -"UI / experiment authoring": {class: ui; tooltip: "6 source files"} -"UI / metric authoring": {class: ui; tooltip: "7 source files"} -"UI / scenario authoring": {class: ui; tooltip: "1 source file"} -"UI / simulation timeline": {class: ui; tooltip: "16 source files"} - -# dependencies - -"core / HIR compiler API" -> "core / HIR artifacts": {tooltip: "1 file-level dependency"} -"core / HIR compiler API" -> "core / HIR compiler": {tooltip: "7 file-level dependencies"} -"core / HIR compiler API" -> "core / HIR emitters": {tooltip: "2 file-level dependencies"} -"core / HIR compiler API" -> "core / HIR runtime API": {tooltip: "1 file-level dependency"} -"core / HIR compiler" -> "core / HIR artifacts": {tooltip: "1 file-level dependency"} -"core / HIR compiler" -> "core / HIR emitters": {tooltip: "2 file-level dependencies"} -"core / HIR compiler" -> "core / HIR runtime API": {tooltip: "1 file-level dependency"} -"core / HIR emitters" -> "core / HIR compiler": {tooltip: "5 file-level dependencies"} -"core / HIR runtime API" -> "core / HIR artifacts": {tooltip: "1 file-level dependency"} -"core / LSP client & transport" -> "core / HIR compiler API": {tooltip: "1 file-level dependency"} -"core / LSP client & transport" -> "core / LSP worker": {tooltip: "4 file-level dependencies"} -"core / LSP services" -> "core / HIR compiler API": {tooltip: "4 file-level dependencies"} -"core / LSP worker" -> "core / HIR compiler API": {tooltip: "1 file-level dependency"} -"core / LSP worker" -> "core / LSP services": {tooltip: "7 file-level dependencies"} -"core / Monte Carlo runtime" -> "core / HIR runtime API": {tooltip: "4 file-level dependencies"} -"core / Monte Carlo runtime" -> "core / simulation assembly": {tooltip: "1 file-level dependency"} -"core / Monte Carlo runtime" -> "core / simulation controller": {tooltip: "1 file-level dependency"} -"core / Monte Carlo runtime" -> "core / simulation frames & metrics": {tooltip: "4 file-level dependencies"} -"core / simulation assembly" -> "core / HIR runtime API": {tooltip: "2 file-level dependencies"} -"core / simulation assembly" -> "core / simulation frames & metrics": {tooltip: "1 file-level dependency"} -"core / simulation controller" -> "core / simulation frames & metrics": {tooltip: "1 file-level dependency"} -"core / simulation controller" -> "core / simulation worker": {tooltip: "3 file-level dependencies"} -"core / simulation frames & metrics" -> "core / HIR runtime API": {tooltip: "1 file-level dependency"} -"core / simulation worker" -> "core / HIR runtime API": {tooltip: "1 file-level dependency"} -"core / simulation worker" -> "core / simulation assembly": {tooltip: "1 file-level dependency"} -"core / simulation worker" -> "core / simulation frames & metrics": {tooltip: "1 file-level dependency"} -"React / experiments provider" -> "core / Monte Carlo runtime": {tooltip: "1 file-level dependency"} -"React / experiments provider" -> "React / LSP provider": {tooltip: "1 file-level dependency"} -"React / LSP provider" -> "core / LSP client & transport": {tooltip: "2 file-level dependencies"} -"React / simulation provider" -> "core / simulation worker": {tooltip: "1 file-level dependency"} -"React / simulation provider" -> "React / LSP provider": {tooltip: "1 file-level dependency"} -"UI / experiment authoring" -> "React / experiments provider": {tooltip: "5 file-level dependencies"} -"UI / experiment authoring" -> "React / LSP provider": {tooltip: "1 file-level dependency"} -"UI / experiment authoring" -> "UI / metric authoring": {tooltip: "2 file-level dependencies"} -"UI / metric authoring" -> "React / LSP provider": {tooltip: "4 file-level dependencies"} -"UI / simulation timeline" -> "core / HIR runtime API": {tooltip: "1 file-level dependency"} -"UI / simulation timeline" -> "React / LSP provider": {tooltip: "1 file-level dependency"} -"UI / simulation timeline" -> "React / simulation provider": {tooltip: "1 file-level dependency"} -"UI / simulation timeline" -> "UI / metric authoring": {tooltip: "2 file-level dependencies"} - -# styling - -classes: { - core: {style.fill: "#dcecff"; style.stroke: "#3676b8"} - react: {style.fill: "#e8e0ff"; style.stroke: "#7051b5"} - ui: {style.fill: "#e2f4e8"; style.stroke: "#3d8055"} -} diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-compilation-dependencies.svg b/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-compilation-dependencies.svg deleted file mode 100644 index 3773273135c..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-compilation-dependencies.svg +++ /dev/null @@ -1,358 +0,0 @@ -core / HIR artifacts1 source filecore / HIR compiler7 source filescore / HIR compiler API1 source filecore / HIR emitters2 source filescore / HIR runtime API2 source filescore / LSP client & transport4 source filescore / LSP services10 source filescore / LSP worker3 source filescore / Monte Carlo runtime22 source filescore / simulation assembly1 source filecore / simulation controller3 source filescore / simulation frames & metrics4 source filescore / simulation worker5 source filesReact / experiments provider2 source filesReact / LSP provider3 source filesReact / simulation provider4 source filesUI / experiment authoring6 source filesUI / metric authoring7 source filesUI / scenario authoring1 source fileUI / simulation timeline16 source files 1 source file - - - - - - - - - - - - -7 source files - - - - - - - - - - - - -1 source file - - - - - - - - - - - - -2 source files - - - - - - - - - - - - -2 source files - - - - - - - - - - - - -4 source files - - - - - - - - - - - - -10 source files - - - - - - - - - - - - -3 source files - - - - - - - - - - - - -22 source files - - - - - - - - - - - - -1 source file - - - - - - - - - - - - -3 source files - - - - - - - - - - - - -4 source files - - - - - - - - - - - - -5 source files - - - - - - - - - - - - -2 source files - - - - - - - - - - - - -3 source files - - - - - - - - - - - - -4 source files - - - - - - - - - - - - -6 source files - - - - - - - - - - - - -7 source files - - - - - - - - - - - - -1 source file - - - - - - - - - - - - -16 source files - - - - - - - - - - - - - - - - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-dependencies.d2 b/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-dependencies.d2 deleted file mode 100644 index 117dbf97a8a..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-dependencies.d2 +++ /dev/null @@ -1,118 +0,0 @@ -# Generated by scripts/generate-dependency-diagrams.mjs. Do not edit. - -direction: right - -# modules - -"core / AI tools": {class: core; tooltip: "1 source file"} -"core / editing & document state": {class: core; tooltip: "15 source files"} -"core / examples": {class: core; tooltip: "6 source files"} -"core / HIR compiler & runtime": {class: core; tooltip: "13 source files"} -"core / LSP": {class: core; tooltip: "17 source files"} -"core / model & persistence": {class: core; tooltip: "22 source files"} -"core / Monte Carlo runtime": {class: core; tooltip: "22 source files"} -"core / shared model API": {class: core; tooltip: "19 source files"} -"core / simulation engine": {class: core; tooltip: "29 source files"} -"core / simulation runtime & workers": {class: core; tooltip: "8 source files"} -"Petrinaut / public API": {class: ui; tooltip: "2 source files"} -"React / editor state": {class: react; tooltip: "40 source files"} -"React / LSP": {class: react; tooltip: "2 source files"} -"React / playback & actual mode": {class: react; tooltip: "5 source files"} -"React / simulation & experiments": {class: react; tooltip: "5 source files"} -"UI / canvas": {class: ui; tooltip: "22 source files"} -"UI / development tools": {class: ui; tooltip: "6 source files"} -"UI / editor": {class: ui; tooltip: "115 source files"} -"UI / shared components & infrastructure": {class: ui; tooltip: "53 source files"} -"UI / shared views": {class: ui; tooltip: "1 source file"} - -# dependencies - -"core / AI tools" -> "core / examples": {tooltip: "1 file-level dependency"} -"core / AI tools" -> "core / shared model API": {tooltip: "6 file-level dependencies"} -"core / editing & document state" -> "core / model & persistence": {tooltip: "1 file-level dependency"} -"core / editing & document state" -> "core / shared model API": {tooltip: "22 file-level dependencies"} -"core / examples" -> "core / shared model API": {tooltip: "8 file-level dependencies"} -"core / HIR compiler & runtime" -> "core / shared model API": {tooltip: "7 file-level dependencies"} -"core / HIR compiler & runtime" -> "core / simulation engine": {tooltip: "2 file-level dependencies"} -"core / LSP" -> "core / editing & document state": {tooltip: "1 file-level dependency"} -"core / LSP" -> "core / HIR compiler & runtime": {tooltip: "6 file-level dependencies"} -"core / LSP" -> "core / shared model API": {tooltip: "18 file-level dependencies"} -"core / LSP" -> "core / simulation engine": {tooltip: "1 file-level dependency"} -"core / model & persistence" -> "core / editing & document state": {tooltip: "1 file-level dependency"} -"core / model & persistence" -> "core / shared model API": {tooltip: "12 file-level dependencies"} -"core / model & persistence" -> "core / simulation engine": {tooltip: "3 file-level dependencies"} -"core / Monte Carlo runtime" -> "core / editing & document state": {tooltip: "1 file-level dependency"} -"core / Monte Carlo runtime" -> "core / HIR compiler & runtime": {tooltip: "4 file-level dependencies"} -"core / Monte Carlo runtime" -> "core / shared model API": {tooltip: "14 file-level dependencies"} -"core / Monte Carlo runtime" -> "core / simulation engine": {tooltip: "24 file-level dependencies"} -"core / Monte Carlo runtime" -> "core / simulation runtime & workers": {tooltip: "1 file-level dependency"} -"core / shared model API" -> "core / AI tools": {tooltip: "2 file-level dependencies"} -"core / shared model API" -> "core / editing & document state": {tooltip: "15 file-level dependencies"} -"core / shared model API" -> "core / HIR compiler & runtime": {tooltip: "1 file-level dependency"} -"core / shared model API" -> "core / LSP": {tooltip: "3 file-level dependencies"} -"core / shared model API" -> "core / model & persistence": {tooltip: "13 file-level dependencies"} -"core / shared model API" -> "core / simulation engine": {tooltip: "12 file-level dependencies"} -"core / simulation engine" -> "core / editing & document state": {tooltip: "1 file-level dependency"} -"core / simulation engine" -> "core / HIR compiler & runtime": {tooltip: "7 file-level dependencies"} -"core / simulation engine" -> "core / Monte Carlo runtime": {tooltip: "2 file-level dependencies"} -"core / simulation engine" -> "core / shared model API": {tooltip: "27 file-level dependencies"} -"core / simulation engine" -> "core / simulation runtime & workers": {tooltip: "2 file-level dependencies"} -"core / simulation runtime & workers" -> "core / editing & document state": {tooltip: "1 file-level dependency"} -"core / simulation runtime & workers" -> "core / HIR compiler & runtime": {tooltip: "1 file-level dependency"} -"core / simulation runtime & workers" -> "core / shared model API": {tooltip: "10 file-level dependencies"} -"core / simulation runtime & workers" -> "core / simulation engine": {tooltip: "10 file-level dependencies"} -"Petrinaut / public API" -> "core / shared model API": {tooltip: "1 file-level dependency"} -"Petrinaut / public API" -> "React / editor state": {tooltip: "2 file-level dependencies"} -"Petrinaut / public API" -> "UI / shared components & infrastructure": {tooltip: "4 file-level dependencies"} -"React / editor state" -> "core / shared model API": {tooltip: "23 file-level dependencies"} -"React / editor state" -> "React / LSP": {tooltip: "2 file-level dependencies"} -"React / editor state" -> "React / playback & actual mode": {tooltip: "6 file-level dependencies"} -"React / editor state" -> "React / simulation & experiments": {tooltip: "6 file-level dependencies"} -"React / LSP" -> "core / LSP": {tooltip: "2 file-level dependencies"} -"React / LSP" -> "core / shared model API": {tooltip: "2 file-level dependencies"} -"React / LSP" -> "React / editor state": {tooltip: "3 file-level dependencies"} -"React / playback & actual mode" -> "core / shared model API": {tooltip: "7 file-level dependencies"} -"React / playback & actual mode" -> "React / editor state": {tooltip: "5 file-level dependencies"} -"React / playback & actual mode" -> "React / simulation & experiments": {tooltip: "3 file-level dependencies"} -"React / simulation & experiments" -> "core / Monte Carlo runtime": {tooltip: "1 file-level dependency"} -"React / simulation & experiments" -> "core / shared model API": {tooltip: "5 file-level dependencies"} -"React / simulation & experiments" -> "core / simulation runtime & workers": {tooltip: "1 file-level dependency"} -"React / simulation & experiments" -> "React / editor state": {tooltip: "11 file-level dependencies"} -"React / simulation & experiments" -> "React / LSP": {tooltip: "2 file-level dependencies"} -"UI / canvas" -> "core / shared model API": {tooltip: "5 file-level dependencies"} -"UI / canvas" -> "React / editor state": {tooltip: "30 file-level dependencies"} -"UI / canvas" -> "React / playback & actual mode": {tooltip: "3 file-level dependencies"} -"UI / canvas" -> "React / simulation & experiments": {tooltip: "3 file-level dependencies"} -"UI / canvas" -> "UI / shared components & infrastructure": {tooltip: "13 file-level dependencies"} -"UI / canvas" -> "UI / shared views": {tooltip: "1 file-level dependency"} -"UI / development tools" -> "core / shared model API": {tooltip: "6 file-level dependencies"} -"UI / development tools" -> "UI / shared components & infrastructure": {tooltip: "3 file-level dependencies"} -"UI / editor" -> "core / examples": {tooltip: "2 file-level dependencies"} -"UI / editor" -> "core / HIR compiler & runtime": {tooltip: "1 file-level dependency"} -"UI / editor" -> "core / shared model API": {tooltip: "63 file-level dependencies"} -"UI / editor" -> "React / editor state": {tooltip: "139 file-level dependencies"} -"UI / editor" -> "React / LSP": {tooltip: "13 file-level dependencies"} -"UI / editor" -> "React / playback & actual mode": {tooltip: "11 file-level dependencies"} -"UI / editor" -> "React / simulation & experiments": {tooltip: "16 file-level dependencies"} -"UI / editor" -> "UI / canvas": {tooltip: "2 file-level dependencies"} -"UI / editor" -> "UI / shared components & infrastructure": {tooltip: "147 file-level dependencies"} -"UI / editor" -> "UI / shared views": {tooltip: "1 file-level dependency"} -"UI / shared components & infrastructure" -> "core / shared model API": {tooltip: "15 file-level dependencies"} -"UI / shared components & infrastructure" -> "React / editor state": {tooltip: "6 file-level dependencies"} -"UI / shared components & infrastructure" -> "React / LSP": {tooltip: "4 file-level dependencies"} -"UI / shared components & infrastructure" -> "UI / canvas": {tooltip: "1 file-level dependency"} -"UI / shared components & infrastructure" -> "UI / editor": {tooltip: "14 file-level dependencies"} -"UI / shared views" -> "core / shared model API": {tooltip: "2 file-level dependencies"} -"UI / shared views" -> "React / editor state": {tooltip: "1 file-level dependency"} -"UI / shared views" -> "React / playback & actual mode": {tooltip: "1 file-level dependency"} -"UI / shared views" -> "React / simulation & experiments": {tooltip: "1 file-level dependency"} -"UI / shared views" -> "UI / editor": {tooltip: "1 file-level dependency"} -"UI / shared views" -> "UI / shared components & infrastructure": {tooltip: "1 file-level dependency"} - -# styling - -classes: { - core: {style.fill: "#dcecff"; style.stroke: "#3676b8"} - react: {style.fill: "#e8e0ff"; style.stroke: "#7051b5"} - ui: {style.fill: "#e2f4e8"; style.stroke: "#3d8055"} -} diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-dependencies.svg b/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-dependencies.svg deleted file mode 100644 index 3b1203b4487..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/petrinaut-dependencies.svg +++ /dev/null @@ -1,358 +0,0 @@ -core / AI tools1 source filecore / editing & document state15 source filescore / examples6 source filescore / HIR compiler & runtime13 source filescore / LSP17 source filescore / model & persistence22 source filescore / Monte Carlo runtime22 source filescore / shared model API19 source filescore / simulation engine29 source filescore / simulation runtime & workers8 source filesPetrinaut / public API2 source filesReact / editor state40 source filesReact / LSP2 source filesReact / playback & actual mode5 source filesReact / simulation & experiments5 source filesUI / canvas22 source filesUI / development tools6 source filesUI / editor115 source filesUI / shared components & infrastructure53 source filesUI / shared views1 source file 1 source file - - - - - - - - - - - - -15 source files - - - - - - - - - - - - -6 source files - - - - - - - - - - - - -13 source files - - - - - - - - - - - - -17 source files - - - - - - - - - - - - -22 source files - - - - - - - - - - - - -22 source files - - - - - - - - - - - - -19 source files - - - - - - - - - - - - -29 source files - - - - - - - - - - - - -8 source files - - - - - - - - - - - - -2 source files - - - - - - - - - - - - -40 source files - - - - - - - - - - - - -2 source files - - - - - - - - - - - - -5 source files - - - - - - - - - - - - -5 source files - - - - - - - - - - - - -22 source files - - - - - - - - - - - - -6 source files - - - - - - - - - - - - -115 source files - - - - - - - - - - - - -53 source files - - - - - - - - - - - - -1 source file - - - - - - - - - - - - - - - - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/worker.html b/libs/@hashintel/petrinaut-core/docs/architecture/worker.html deleted file mode 100644 index 0a898518bc3..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/worker.html +++ /dev/null @@ -1,752 +0,0 @@ - - - - - - Simulation Architecture — Worker & Protocol - - - - - -

Worker & Protocol

-

- worker/ (protocol + worker entrypoint) and - runtime/ - (main-thread lifecycle, transport, frame retention). Everything between - the host application and the engine. -

- -
-
Protocol
-
- worker/messages.ts — discriminated unions over - postMessage -
-
Transport
-
- runtime/transport.ts — wraps a Worker factory; - queues messages until the worker boots -
-
Host handle
-
- createSimulation()Simulation (stores: - status, frames; events; - run/pause/reset/ack/getFrame/dispose) -
-
Flow control
-
- Ack-based backpressure — the consumer’s pace bounds worker - memory/compute -
-
- -

Message protocol

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Host → Worker (ToWorkerMessage) -
TypePayloadEffect in the worker
init - sdcpn, extensions?, initialMarking, parameterValues, seed, dt, - maxTime (number | null), maxFramesAhead?, batchSize? - - Runs buildSimulation() (compiles user code, packs frame - 0), replies frame(0) then ready. Resets - lastAckedFrame = -1. -
start - Starts the async compute loop (no-op if running; refuses from - complete/error). -
pauseStops the loop, keeps all state, replies paused.
stopDiscards the SimulationInstance entirely.
setBackpressuremaxFramesAhead?, batchSize?Live-updates loop tuning.
ackframeNumber - lastAckedFrame = max(lastAckedFrame, n) — unblocks - computation. -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Worker → Host (ToMainMessage) -
TypePayloadEffect on the main thread
readyinitialFrameCount - Resolves the createSimulation promise; status → - Ready. -
frame / frames - SimulationFramePayload = - { time, frame: ArrayBuffer } (single / batch) - - Appended to the frame store; frames store publishes - {count, latest: reader}. -
pausedframeNumberStatus → Paused.
completereason: "deadlock" | "maxTime", frameNumberStatus → Complete; emitted on the event stream.
errormessage, itemId (offending SDCPN item if known) - Status → Error; rejects init if still initializing. -
- -

Sequence — one quick simulation

- -
- - - - - - - - - - - Host (playback driver) - - - - createSimulation (main) - - - - simulation.worker - - - - - - - - - - createSimulation(config) - - - - init {sdcpn, initialMarking, seed, dt…} - - - buildSimulation() - - - compile + frame 0 - - - - - frame {t:0, ArrayBuffer²} - - ready {initialFrameCount:1} - - Promise resolves · status "Ready" - - - - simulation.run() - - start - - - - - loop blocked: lastAckedFrame = -1 - - - → nothing computes until first ack - - - - - - - loop — while frames remain to compute - - - - - - ack(frameNumber) — playback mode decides when - - - - - - computeNextFrame() - - - × batchSize while - - - n - acked < ahead - - - - - frames [{time, ArrayBuffer²}, …] - - frames store → {count, latest reader} - - - - complete {reason: deadlock | maxTime} - - status "Complete" + event emitted - -
- ² = the frame ArrayBuffer is - structured-clone copied - across the boundary (no transfer list); the worker keeps its own copy in - SimulationInstance.frames[]. -
-
- -

Backpressure — the ack contract

- -
// worker compute loop (simplified from simulation.worker.ts)
-while (isRunning) {
-  if (lastAckedFrame < 0 || currentFrame - lastAckedFrame >= maxFramesAhead) {
-    await delay(10); continue;          // wait for the consumer
-  }
-  batch = compute up to batchSize frames (stop on complete/error)
-  post "frame" | "frames"
-  await delay(0);                        // let pause/stop/ack messages in
-}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Play mode (core playback module)maxFramesAheadbatchSizeAck behaviour
viewOnlyn/a - No core backpressure profile — the React playback provider pauses - the worker and never acks, so nothing new is computed. -
computeBuffer4010 - Acks when playback is within ~0.5 s of the last computed frame — - keeps a small rolling buffer ahead of the playhead. -
computeMax10000500Acks every arrival → compute as fast as possible.
Worker defaults10001000Used when init omits the settings.
- -

Lifecycle

- -
- - - - - - - - - Initializing - - Ready - - Running - - Paused - - Complete - - Error - - - ready msg - - run() - - pause() - - run() - - deadlock / maxTime - - error msg - - - reset(): stop + clear store → Ready (from any state) - - -
- Main-thread SimulationState. The worker mirrors a smaller - internal status (ready / running / complete / error); - complete and error are terminal for the worker — - reset() sends stop and requires a fresh - init to run again. -
-
- -

- Main-thread runtime responsibilities (createSimulation) -

-
    -
  • - Snapshot preparation: sanitizes the SDCPN for the - enabled extensions and flattens component instances so the - main-thread frame layout matches what the worker produces - (place-count mismatch throws on read). -
  • -
  • - Transport: - createWorkerTransport(createWorker) queues outbound - messages until the worker resolves; test code can inject a pre-built - SimulationTransport instead. -
  • -
  • - Retention: - createInMemorySimulationFrameStore(sdcpn) - compiles the reader factory once and appends every payload — it keeps - all frames so playback can scrub. Alternative stores - (latest-only, sliding window, persisted) can be swapped in without - changing consumers. -
  • -
  • - Publication: status and - frames readable stores + events stream; hosts - subscribe with store.subscribe(listener). -
  • -
- -
- Refactoring seams. - (1) Frames could be posted with a transfer list to halve copies — - but not as-is: computeNextFrame() re-reads the latest stored - frame to compute the next one, so transferring (detaching) the posted - buffer first requires stepping from a retained working copy (e.g. - latest-only retention or a double buffer). (2) reset() keeps - the main-thread status Ready but the worker has discarded its - simulation — a subsequent run() sends start to a - worker with nothing to run; a full re-init is what actually happens in the - UI flow. (3) The 10 ms poll while waiting for acks is a busy-wait; a - promise-per-ack would be exact. -
- - diff --git a/libs/@hashintel/petrinaut-core/package.json b/libs/@hashintel/petrinaut-core/package.json index 5f45d28c7c8..300fd370c0f 100644 --- a/libs/@hashintel/petrinaut-core/package.json +++ b/libs/@hashintel/petrinaut-core/package.json @@ -69,7 +69,6 @@ }, "scripts": { "build": "vite build", - "doc:dependency-diagram": "node scripts/generate-dependency-diagrams.mjs", "fix:eslint": "oxlint --fix --type-aware --report-unused-disable-directives-severity=error .", "lint:eslint": "oxlint --type-aware --report-unused-disable-directives-severity=error .", "lint:tsc": "tsgo --noEmit", @@ -86,7 +85,6 @@ "devDependencies": { "@types/node": "22.18.13", "@typescript/native-preview": "7.0.0-dev.20260511.1", - "dependency-cruiser": "18.0.0", "oxlint": "1.63.0", "oxlint-tsgolint": "0.22.1", "rolldown": "1.1.2", diff --git a/libs/@hashintel/petrinaut-core/scripts/generate-dependency-diagrams.mjs b/libs/@hashintel/petrinaut-core/scripts/generate-dependency-diagrams.mjs deleted file mode 100644 index da80c52d321..00000000000 --- a/libs/@hashintel/petrinaut-core/scripts/generate-dependency-diagrams.mjs +++ /dev/null @@ -1,356 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { chmod, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; - -import { cruise } from "dependency-cruiser"; -import extractTSConfig from "dependency-cruiser/config-utl/extract-ts-config"; - -const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url)); -const outputDirectory = fileURLToPath( - new URL("../docs/architecture/", import.meta.url), -); -const tsconfigPath = fileURLToPath( - new URL("../dependency-cruiser.tsconfig.json", import.meta.url), -); - -const corePrefix = "libs/@hashintel/petrinaut-core/src/"; -const petrinautPrefix = "libs/@hashintel/petrinaut/src/"; -const coreSource = join(repoRoot, corePrefix); - -const coreAliases = [ - ["@hashintel/petrinaut-core/examples", "examples/index.ts"], - ["@hashintel/petrinaut-core/hir-runtime", "hir-runtime.ts"], - ["@hashintel/petrinaut-core/hir", "hir.ts"], - ["@hashintel/petrinaut-core/workers/lsp", "workers/lsp.ts"], - ["@hashintel/petrinaut-core/workers/monte-carlo", "workers/monte-carlo.ts"], - ["@hashintel/petrinaut-core/workers/simulation", "workers/simulation.ts"], - ["@hashintel/petrinaut-core", "index.ts"], -].map(([name, path]) => ({ - alias: join(coreSource, path), - name, - onlyModule: true, -})); - -const cruiseResult = await cruise( - ["libs/@hashintel/petrinaut-core/src", "libs/@hashintel/petrinaut/src"], - { - baseDir: repoRoot, - exclude: - "(?:[.](?:test|stories)[.][cm]?[jt]sx?$|/(?:__fixtures__|__snapshots__)/)", - includeOnly: "^libs/@hashintel/petrinaut(?:-core)?/src/", - moduleSystems: ["es6"], - tsPreCompilationDeps: true, - }, - { - alias: coreAliases, - conditionNames: ["types", "import", "default"], - extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], - }, - { tsConfig: extractTSConfig(tsconfigPath) }, -); - -if (typeof cruiseResult.output === "string") { - throw new TypeError("dependency-cruiser returned formatted output"); -} - -/** @type {import("dependency-cruiser").ICruiseResult["modules"]} */ -const modules = cruiseResult.output.modules; - -/** - * @param {string} source - * @returns {string | null} - */ -function broadModule(source) { - if (source.startsWith(corePrefix)) { - const path = source.slice(corePrefix.length); - const [directory, child] = path.split("/"); - - if (directory === "hir" || directory.startsWith("hir")) { - return "core / HIR compiler & runtime"; - } - if (directory === "lsp" || path === "workers/lsp.ts") { - return "core / LSP"; - } - if (directory === "simulation" || directory === "workers") { - if (child === "monte-carlo" || path === "workers/monte-carlo.ts") { - return "core / Monte Carlo runtime"; - } - if ( - ["runtime", "worker"].includes(child) || - path === "workers/simulation.ts" - ) { - return "core / simulation runtime & workers"; - } - return "core / simulation engine"; - } - if ( - [ - "actions.ts", - "clipboard", - "commands.ts", - "handle", - "layout", - "store", - ].includes(directory) - ) { - return "core / editing & document state"; - } - if ( - [ - "actual-mode", - "file-format", - "playback", - "schemas", - "validation", - ].includes(directory) - ) { - return "core / model & persistence"; - } - if (directory === "examples") { - return "core / examples"; - } - if (directory === "ai.ts") { - return "core / AI tools"; - } - return "core / shared model API"; - } - - if (source.startsWith(petrinautPrefix)) { - const path = source.slice(petrinautPrefix.length); - const [directory, child, grandchild] = path.split("/"); - - if (!child) { - return "Petrinaut / public API"; - } - if (directory === "react") { - if (child === "lsp") { - return "React / LSP"; - } - if (["experiments", "simulation"].includes(child)) { - return "React / simulation & experiments"; - } - if ( - ["actual-mode-context.ts", "execution-frame", "playback"].includes( - child, - ) - ) { - return "React / playback & actual mode"; - } - return "React / editor state"; - } - if (directory === "ui" && child === "views") { - if (grandchild === "Editor") { - return "UI / editor"; - } - if (grandchild === "SDCPN") { - return "UI / canvas"; - } - return "UI / shared views"; - } - if (directory === "ui" && child === "dev") { - return "UI / development tools"; - } - return "UI / shared components & infrastructure"; - } - - return null; -} - -/** - * @param {string} source - * @returns {string | null} - */ -function compilationModule(source) { - if (source.startsWith(corePrefix)) { - const path = source.slice(corePrefix.length); - - if (path.startsWith("lsp/worker/")) { - return "core / LSP worker"; - } - if (path.startsWith("lsp/lib/")) { - return "core / LSP services"; - } - if (path.startsWith("lsp/") || path === "workers/lsp.ts") { - return "core / LSP client & transport"; - } - if (path === "hir.ts") { - return "core / HIR compiler API"; - } - if (path === "hir-runtime.ts" || path === "hir/instantiate.ts") { - return "core / HIR runtime API"; - } - if (path.startsWith("hir/emit-")) { - return "core / HIR emitters"; - } - if (path === "hir/artifact-fingerprint.ts") { - return "core / HIR artifacts"; - } - if (path.startsWith("hir/")) { - return "core / HIR compiler"; - } - if (path === "simulation/engine/build-simulation.ts") { - return "core / simulation assembly"; - } - if (path.startsWith("simulation/frames/")) { - return "core / simulation frames & metrics"; - } - if (path.startsWith("simulation/runtime/")) { - return "core / simulation controller"; - } - if ( - path.startsWith("simulation/worker/") || - path === "workers/simulation.ts" - ) { - return "core / simulation worker"; - } - if ( - path.startsWith("simulation/monte-carlo/") || - path === "workers/monte-carlo.ts" - ) { - return "core / Monte Carlo runtime"; - } - return null; - } - - if (!source.startsWith(petrinautPrefix)) { - return null; - } - - const path = source.slice(petrinautPrefix.length); - if (path.startsWith("react/lsp/") || path === "react/hooks/use-lsp.ts") { - return "React / LSP provider"; - } - if ( - path.startsWith("react/simulation/") || - path === "react/hooks/use-simulation.ts" - ) { - return "React / simulation provider"; - } - if (path.startsWith("react/experiments/")) { - return "React / experiments provider"; - } - if (path.includes("/SimulateView/metrics/")) { - return "UI / metric authoring"; - } - if (path.includes("/SimulateView/experiments/")) { - return "UI / experiment authoring"; - } - if (path.includes("/simulation-timeline/")) { - return "UI / simulation timeline"; - } - if (path.endsWith("/scenario-lsp.ts")) { - return "UI / scenario authoring"; - } - return null; -} - -/** @param {string} name */ -function moduleClass(name) { - if (name.startsWith("core /")) { - return "core"; - } - if (name.startsWith("React /")) { - return "react"; - } - return "ui"; -} - -/** @param {(source: string) => string | null} classify */ -function buildGraph(classify) { - /** @type {Map>} */ - const nodes = new Map(); - /** @type {Map} */ - const edges = new Map(); - - for (const module of modules) { - const from = classify(module.source); - if (from) { - const sources = nodes.get(from) ?? new Set(); - sources.add(module.source); - nodes.set(from, sources); - } - - for (const dependency of module.dependencies) { - const to = classify(dependency.resolved); - if (!from || !to || from === to) { - continue; - } - - const sources = nodes.get(to) ?? new Set(); - sources.add(dependency.resolved); - nodes.set(to, sources); - - const edge = `${from}\u0000${to}`; - edges.set(edge, (edges.get(edge) ?? 0) + 1); - } - } - - const nodeLines = [...nodes.entries()] - .sort(([left], [right]) => left.localeCompare(right)) - .map(([name, sources]) => { - const fileLabel = `${sources.size} source ${sources.size === 1 ? "file" : "files"}`; - return `${JSON.stringify(name)}: {class: ${moduleClass(name)}; tooltip: ${JSON.stringify(fileLabel)}}`; - }); - const edgeLines = [...edges.entries()] - .sort(([left], [right]) => left.localeCompare(right)) - .map(([edge, count]) => { - const [from, to] = edge.split("\u0000"); - return `${JSON.stringify(from)} -> ${JSON.stringify(to)}: {tooltip: ${JSON.stringify(`${count} file-level ${count === 1 ? "dependency" : "dependencies"}`)}}`; - }); - - return ( - `# Generated by scripts/generate-dependency-diagrams.mjs. Do not edit.\n\n` + - `direction: right\n\n` + - `# modules\n\n${nodeLines.join("\n")}\n\n` + - `# dependencies\n\n${edgeLines.join("\n")}\n\n` + - `# styling\n\nclasses: {\n` + - ` core: {style.fill: "#dcecff"; style.stroke: "#3676b8"}\n` + - ` react: {style.fill: "#e8e0ff"; style.stroke: "#7051b5"}\n` + - ` ui: {style.fill: "#e2f4e8"; style.stroke: "#3d8055"}\n` + - `}\n` - ); -} - -/** - * @param {string} sourcePath - * @param {string} outputPath - */ -function renderD2(sourcePath, outputPath) { - const result = spawnSync( - "mise", - [ - "exec", - "--env", - "dev", - "--", - "d2", - "--layout", - "elk", - sourcePath, - outputPath, - ], - { cwd: repoRoot, encoding: "utf8" }, - ); - - if (result.status !== 0) { - throw new Error(result.stderr || result.stdout || "D2 rendering failed"); - } -} - -/** @type {Array<[string, (source: string) => string | null]>} */ -const diagrams = [ - ["petrinaut-dependencies", broadModule], - ["petrinaut-compilation-dependencies", compilationModule], -]; - -for (const [name, classify] of diagrams) { - const sourcePath = `${outputDirectory}${name}.d2`; - const outputPath = `${outputDirectory}${name}.svg`; - await writeFile(sourcePath, buildGraph(classify)); - renderD2(sourcePath, outputPath); - await chmod(outputPath, 0o644); - process.stdout.write( - `Generated ${sourcePath.slice(repoRoot.length)} and ${outputPath.slice(repoRoot.length)}\n`, - ); -} diff --git a/libs/@hashintel/petrinaut-core/src/actual-mode/README.md b/libs/@hashintel/petrinaut-core/src/actual-mode/README.md index 27b501d83cb..821e5b33709 100644 --- a/libs/@hashintel/petrinaut-core/src/actual-mode/README.md +++ b/libs/@hashintel/petrinaut-core/src/actual-mode/README.md @@ -1,3 +1,14 @@ +--- +layer: core.actual-mode +name: Actual mode +role: Renders an execution supplied by an external source rather than by simulation +boundaries: + - kind: network + note: Execution events arrive from a host-supplied transport; this layer stays transport-neutral +invariants: + - Experimental — not a stable Petrinaut protocol, so no external consumer may depend on its shape +--- + # Actual Mode Core This folder contains the experimental, transport-neutral pieces of Petrinaut diff --git a/libs/@hashintel/petrinaut-core/src/clipboard/paste.ts b/libs/@hashintel/petrinaut-core/src/clipboard/paste.ts index 00c5fd626b3..a624bdd1ad3 100644 --- a/libs/@hashintel/petrinaut-core/src/clipboard/paste.ts +++ b/libs/@hashintel/petrinaut-core/src/clipboard/paste.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.clipboard + * @layerName Clipboard + * @role Serialises a selection and pastes it back, resolving name collisions + * @invariant Pasted elements are renamed rather than overwriting an existing element of the same name + */ + import { v4 as generateUuid } from "uuid"; import { getArcEndpointPlaceId } from "../arc-endpoints"; diff --git a/libs/@hashintel/petrinaut-core/src/examples/index.ts b/libs/@hashintel/petrinaut-core/src/examples/index.ts index 9992c4d0e83..e3b02f3970a 100644 --- a/libs/@hashintel/petrinaut-core/src/examples/index.ts +++ b/libs/@hashintel/petrinaut-core/src/examples/index.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.examples + * @layerName Example models + * @role Ready-made SDCPN documents shipped for onboarding and demos + * @entryPoint @hashintel/petrinaut-core/examples + */ + export { productionMachines } from "./production-with-machine-failure"; export { deploymentPipelineSDCPN } from "./deployment-pipeline"; export { probabilisticSatellitesSDCPN } from "./satellites-launcher"; diff --git a/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.ts b/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.ts index d9327873a4b..29c1b569607 100644 --- a/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.ts +++ b/libs/@hashintel/petrinaut-core/src/file-format/parse-sdcpn-file.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.file-format + * @layerName File format + * @role Reads and writes the on-disk SDCPN document format, plus export converters + * @invariant Parsing is the only entry point for untrusted document input, so it validates rather than trusting shape + */ + import { legacySdcpnFileSchema, SDCPN_FILE_FORMAT_VERSION, diff --git a/libs/@hashintel/petrinaut-core/src/handle/index.ts b/libs/@hashintel/petrinaut-core/src/handle/index.ts index 032e79a1835..4603dd3758e 100644 --- a/libs/@hashintel/petrinaut-core/src/handle/index.ts +++ b/libs/@hashintel/petrinaut-core/src/handle/index.ts @@ -1,3 +1,9 @@ +/** + * @layerRoot core.handle + * @layerName Document handle + * @role Stateful handle wrapping a document, emitting change events to subscribers + */ + export { createJsonDocHandle, type CreateJsonDocHandleOptions, diff --git a/libs/@hashintel/petrinaut-core/src/hir/README.md b/libs/@hashintel/petrinaut-core/src/hir/README.md index 6d06a46ed70..420ed5547b8 100644 --- a/libs/@hashintel/petrinaut-core/src/hir/README.md +++ b/libs/@hashintel/petrinaut-core/src/hir/README.md @@ -1,3 +1,18 @@ +--- +layer: core.hir +name: HIR compiler +role: Lowers user-authored TypeScript to a source-spanned IR, then typechecks, lints and emits it +entryPoints: + - "@hashintel/petrinaut-core/hir" + - "@hashintel/petrinaut-core/hir-runtime" +boundaries: + - kind: sandbox + note: Emitted user code runs against a fixed buffer ABI, not against arbitrary host globals +invariants: + - HIR nodes carry no inferred types inline; types live in a side table keyed by node id + - This pipeline is the only runtime path for dynamics, transition lambdas, kernels and expression metrics +--- + # Petrinaut HIR The HIR is Petrinaut's source-spanned intermediate representation for diff --git a/libs/@hashintel/petrinaut-core/src/index.ts b/libs/@hashintel/petrinaut-core/src/index.ts index 3e493eb241f..cca8f6e22a3 100644 --- a/libs/@hashintel/petrinaut-core/src/index.ts +++ b/libs/@hashintel/petrinaut-core/src/index.ts @@ -1,7 +1,16 @@ -// Public surface for `@hashintel/petrinaut-core` — the headless engine. -// -// No React, no DOM, no Monaco. Stateful handles, streams, and pure logic for -// SDCPN documents, simulation, LSP, and playback. +/** + * Public surface for `@hashintel/petrinaut-core` — the headless engine. + * + * No React, no DOM, no Monaco. Stateful handles, streams, and pure logic for + * SDCPN documents, simulation, LSP, and playback. + * + * @layerRoot core + * @layerName Headless core + * @role SDCPN document model, compiler, simulation runtimes and LSP, with no UI framework + * @entryPoint @hashintel/petrinaut-core + * @boundary package — everything re-exported here is public API covered by semver + * @invariant No React, no DOM and no Monaco imports, so the core runs unchanged in Node and in workers + */ // --- Document --- export { diff --git a/libs/@hashintel/petrinaut-core/src/layout/index.ts b/libs/@hashintel/petrinaut-core/src/layout/index.ts index 630c48d4435..269f147a5b7 100644 --- a/libs/@hashintel/petrinaut-core/src/layout/index.ts +++ b/libs/@hashintel/petrinaut-core/src/layout/index.ts @@ -1,3 +1,9 @@ +/** + * @layerRoot core.layout + * @layerName Graph layout + * @role Computes node positions for a net, so auto-layout does not require the canvas + */ + export { calculateGraphLayout, type LayoutDimensions, diff --git a/libs/@hashintel/petrinaut-core/src/lsp/index.ts b/libs/@hashintel/petrinaut-core/src/lsp/index.ts index 415bd3a45c9..a3140ca534d 100644 --- a/libs/@hashintel/petrinaut-core/src/lsp/index.ts +++ b/libs/@hashintel/petrinaut-core/src/lsp/index.ts @@ -1,3 +1,11 @@ +/** + * @layerRoot core.lsp + * @layerName LSP client + * @role Language-server client and transport for editing user code in the net + * @entryPoint @hashintel/petrinaut-core/workers/lsp + * @boundary thread — requests reach the language server over a worker transport, so every call is async + */ + export { createLanguageClient, type CreateLanguageClientConfig, diff --git a/libs/@hashintel/petrinaut-core/src/lsp/worker/language-server.worker.ts b/libs/@hashintel/petrinaut-core/src/lsp/worker/language-server.worker.ts index 4d95f74d5c9..c01b2b830e0 100644 --- a/libs/@hashintel/petrinaut-core/src/lsp/worker/language-server.worker.ts +++ b/libs/@hashintel/petrinaut-core/src/lsp/worker/language-server.worker.ts @@ -7,6 +7,12 @@ * - Server push: `textDocument/publishDiagnostics` * * The LanguageService is created once and reused across SDCPN changes. + * + * @layerRoot core.lsp.worker + * @layerName LSP worker + * @role Hosts the TypeScript language server off the main thread + * @entryPoint @hashintel/petrinaut-core/workers/lsp + * @boundary worker — The language server runs in its own thread; the client reaches it only over the documented protocol */ import ts from "typescript"; import { diff --git a/libs/@hashintel/petrinaut-core/src/playback/index.ts b/libs/@hashintel/petrinaut-core/src/playback/index.ts index b382d70a22b..865639daaf2 100644 --- a/libs/@hashintel/petrinaut-core/src/playback/index.ts +++ b/libs/@hashintel/petrinaut-core/src/playback/index.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.playback + * @layerName Playback + * @role Picks the viewed frame over time and defines the per-play-mode backpressure profiles + * @invariant Pure state machine — it decides which frame should be shown but never fetches one + */ + export { createPlayback, formatPlaybackSpeed, diff --git a/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts b/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts index fd48c8705db..a860083581a 100644 --- a/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts +++ b/libs/@hashintel/petrinaut-core/src/schemas/entity-schemas.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.schemas + * @layerName Schemas + * @role Zod schemas for document entities, metrics and scenarios, and the descriptions the AI tools read + * @invariant Every entity schema ends in `satisfies z.ZodType` against the canonical type, so a schema cannot drift from the type it validates + */ + import { z } from "zod"; import { getParameterValueError } from "../parameter-values"; diff --git a/libs/@hashintel/petrinaut-core/src/simulation/ARCHITECTURE.md b/libs/@hashintel/petrinaut-core/src/simulation/ARCHITECTURE.md index d4ab5c66f91..623139bdbe8 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/ARCHITECTURE.md +++ b/libs/@hashintel/petrinaut-core/src/simulation/ARCHITECTURE.md @@ -1,8 +1,8 @@ # Simulation Architecture -> For the illustrated deep-dive (memory maps, sequence diagrams, protocols), -> open [`../../docs/architecture/index.html`](../../docs/architecture/index.html) -> in a browser — no build step needed. +> For the deep-dive (memory model, frame format, protocols, Monte Carlo), see +> the architecture docs: `turbo run dev --filter @apps/petrinaut-docs`, or read +> [`libs/@local/petrinaut-arch-docs/content/simulation/`](../../../../@local/petrinaut-arch-docs/content/simulation). The simulation module is split into five boundaries: diff --git a/libs/@hashintel/petrinaut-core/src/simulation/README.md b/libs/@hashintel/petrinaut-core/src/simulation/README.md index 3e300cd2718..1251b4e9b02 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/README.md +++ b/libs/@hashintel/petrinaut-core/src/simulation/README.md @@ -1,11 +1,21 @@ +--- +layer: core.simulation +name: Simulation +role: Executes SDCPN nets — stepping, frames, workers and batch statistics +entryPoints: + - "@hashintel/petrinaut-core" +invariants: + - No UI-framework dependency, so the runtime is usable from Node and from workers +--- + # Simulation Module Headless SDCPN simulation runtime. -> Illustrated architecture documentation (memory layouts, sequence diagrams, -> protocols) lives in -> [`../../docs/architecture/index.html`](../../docs/architecture/index.html) — -> self-contained HTML, open it in a browser. +> Deep-dive documentation (memory layouts, the frame format, worker protocol, +> Monte Carlo) lives in the architecture docs. Browse them with +> `turbo run dev --filter @apps/petrinaut-docs`, or read the sources in +> [`libs/@local/petrinaut-arch-docs/content/simulation/`](../../../../@local/petrinaut-arch-docs/content/simulation). ## Overview diff --git a/libs/@hashintel/petrinaut-core/src/simulation/authoring/sandbox.ts b/libs/@hashintel/petrinaut-core/src/simulation/authoring/sandbox.ts index 38ffbc81edc..c5a3342ade2 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/authoring/sandbox.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/authoring/sandbox.ts @@ -2,6 +2,11 @@ * Shared hardening helpers for evaluating user-authored JS expressions * (scenario expressions, metric bodies, …). Co-located so the compilers * can't drift on what they consider "safe enough". + * + * @layerRoot core.simulation.authoring + * @layerName User-code authoring + * @role Compiles and sandboxes the code users write inside a net + * @boundary sandbox — User code is evaluated with restricted globals; it never receives the host scope */ /** diff --git a/libs/@hashintel/petrinaut-core/src/simulation/engine/README.md b/libs/@hashintel/petrinaut-core/src/simulation/engine/README.md index 58551a8f40e..9afb4df1f2a 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/engine/README.md +++ b/libs/@hashintel/petrinaut-core/src/simulation/engine/README.md @@ -1,3 +1,11 @@ +--- +layer: core.simulation.engine +name: Simulation engine +role: Builds an SDCPN definition into a runnable instance and computes frames +invariants: + - Owns the runtime stepping state and frame layout; user-code compilation is delegated to the authoring layer +--- + # Simulation Engine Core simulation logic for SDCPN Petri net execution. diff --git a/libs/@hashintel/petrinaut-core/src/simulation/frames/frame-reader.ts b/libs/@hashintel/petrinaut-core/src/simulation/frames/frame-reader.ts index c5f7bf5cb5d..3e0c8e12aa9 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/frames/frame-reader.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/frames/frame-reader.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.simulation.frames + * @layerName Frames & metrics + * @role The frame layout and the readers hosts use to inspect one frame + * @invariant Readers are views over an existing buffer, not copies, so reading a frame does not allocate per place or per transition + */ + import { readTokenRecord } from "../engine/token-layout"; import { createEngineFrameLayout, diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/README.md b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/README.md index 84651a2c99b..e9b04f8cd3d 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/README.md +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/README.md @@ -1,3 +1,17 @@ +--- +layer: core.simulation.monte-carlo +name: Monte Carlo runtime +role: Runs many independent simulations with bounded frame memory, reporting metric aggregates +entryPoints: + - "@hashintel/petrinaut-core/workers/monte-carlo" +boundaries: + - kind: worker + note: Frame buffers stay inside the worker; only metric aggregates are posted to the host +invariants: + - Frame memory is bounded regardless of run length — no frame history is retained + - Uses the same extension-aware SDCPN sanitization as the interactive simulator, so disabled surfaces are not compiled here either +--- + # Monte Carlo Simulator ## Goal diff --git a/libs/@hashintel/petrinaut-core/src/simulation/runtime/simulation.ts b/libs/@hashintel/petrinaut-core/src/simulation/runtime/simulation.ts index e2de82ea7a3..1116a869f92 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/runtime/simulation.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/runtime/simulation.ts @@ -1,3 +1,11 @@ +/** + * @layerRoot core.simulation.runtime + * @layerName Simulation controller + * @role Host-side controller for a run — owns the transport, the frame store and the status streams + * @entryPoint @hashintel/petrinaut-core + * @boundary thread — Talks to the worker only through the transport; it holds no reference to engine state + */ + import { DEFAULT_PETRINAUT_EXTENSIONS, sanitizeSDCPNForExtensions, diff --git a/libs/@hashintel/petrinaut-core/src/simulation/worker/README.md b/libs/@hashintel/petrinaut-core/src/simulation/worker/README.md index 4184d8900f2..4755639ccc7 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/worker/README.md +++ b/libs/@hashintel/petrinaut-core/src/simulation/worker/README.md @@ -1,3 +1,16 @@ +--- +layer: core.simulation.worker +name: Simulation worker +role: Computes simulation frames off the main thread under host backpressure +entryPoints: + - "@hashintel/petrinaut-core/workers/simulation" +boundaries: + - kind: worker + note: Host and worker communicate only by the documented message protocol +invariants: + - Frames are computed in batches gated by host backpressure, so a fast worker cannot outrun its consumer +--- + # Simulation Worker Worker runtime for off-main-thread SDCPN simulation computation. diff --git a/libs/@hashintel/petrinaut-core/src/store/index.ts b/libs/@hashintel/petrinaut-core/src/store/index.ts index c319b60aae6..0cc8561fff1 100644 --- a/libs/@hashintel/petrinaut-core/src/store/index.ts +++ b/libs/@hashintel/petrinaut-core/src/store/index.ts @@ -1 +1,8 @@ +/** + * @layerRoot core.store + * @layerName Readable store + * @role Minimal subscribable store primitive the core exposes instead of a framework dependency + * @invariant Framework-agnostic — consumers adapt it (for example via `useStore`) rather than the core importing React + */ + export { createReadableStore, type ReadableStore } from "./readable-store"; diff --git a/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts b/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts index 99bcbcee03c..c34e206ad12 100644 --- a/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts +++ b/libs/@hashintel/petrinaut-core/src/types/sdcpn.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot core.types + * @layerName Document types + * @role The canonical TypeScript types describing an SDCPN document + * @invariant Authoritative over the Zod schemas — schemas are checked against these types, never the reverse + */ + export type ID = string; export type ColorElementType = diff --git a/libs/@hashintel/petrinaut-core/src/validation/README.md b/libs/@hashintel/petrinaut-core/src/validation/README.md index 1ff45ab267f..3ba75f8e6f6 100644 --- a/libs/@hashintel/petrinaut-core/src/validation/README.md +++ b/libs/@hashintel/petrinaut-core/src/validation/README.md @@ -1,3 +1,11 @@ +--- +layer: core.validation +name: Validation +role: Structural integrity validators for SDCPN entities, enforcing naming conventions +invariants: + - Pure-function wrappers over Zod schemas, so validators are callable from any layer without side effects +--- + # validation/ Structural integrity validators for SDCPN entities. These enforce naming diff --git a/libs/@hashintel/petrinaut-core/src/workers/README.md b/libs/@hashintel/petrinaut-core/src/workers/README.md new file mode 100644 index 00000000000..45eb4b8772b --- /dev/null +++ b/libs/@hashintel/petrinaut-core/src/workers/README.md @@ -0,0 +1,28 @@ +--- +layer: core.workers +name: Worker entry points +role: The module entry points hosts instantiate as Web Workers +entryPoints: + - "@hashintel/petrinaut-core/workers/lsp" + - "@hashintel/petrinaut-core/workers/monte-carlo" + - "@hashintel/petrinaut-core/workers/simulation" +boundaries: + - kind: worker + note: Each file here is the top of a separate thread; only structured-cloneable messages cross +--- + +# Worker entry points + +One file per worker the host can spawn. Each is a thin entry point wiring a +message port to the runtime that does the work, holding no logic of its own, so +the runtimes stay testable on the main thread. + +| Entry point | Runtime it hosts | +| ---------------- | ----------------------------------- | +| `lsp.ts` | the language server | +| `simulation.ts` | frame computation for a single run | +| `monte-carlo.ts` | batched runs reporting only metrics | + +Separate export subpaths rather than one worker, so a host pays only for the +threads it uses — an editor with no experiments open never loads the Monte +Carlo runtime. diff --git a/libs/@hashintel/petrinaut/ARCHITECTURE.md b/libs/@hashintel/petrinaut/ARCHITECTURE.md index 5d5ce442d82..1bdc7abbbc3 100644 --- a/libs/@hashintel/petrinaut/ARCHITECTURE.md +++ b/libs/@hashintel/petrinaut/ARCHITECTURE.md @@ -2,9 +2,9 @@ How `@hashintel/petrinaut`'s React layer consumes the headless simulation runtime from `@hashintel/petrinaut-core`. The core's architecture (engine, -frame format, worker protocol, Monte Carlo) is documented in -[`petrinaut-core/docs/architecture/`](../petrinaut-core/docs/architecture/index.html) -— this page covers only the React side of the boundary. +frame format, worker protocol, Monte Carlo) is documented in the architecture +docs — browse with `turbo run dev --filter @apps/petrinaut-docs` — and this page +covers only the React side of the boundary. > This file is internal engineering documentation. It deliberately does NOT > live in `docs/` — that folder is the end-user guide, consumed at runtime by diff --git a/libs/@hashintel/petrinaut/src/main.ts b/libs/@hashintel/petrinaut/src/main.ts index afd1269f459..51d58d98731 100644 --- a/libs/@hashintel/petrinaut/src/main.ts +++ b/libs/@hashintel/petrinaut/src/main.ts @@ -1,3 +1,18 @@ +/** + * Public surface for `@hashintel/petrinaut` — the host-facing entry point. + * + * Re-exports the handful of contexts and types a host needs to embed the editor + * and inject its own capabilities (error tracking, optimization, slots). The + * editor itself is reached through `/ui`, and the React bindings through + * `/react`. + * + * @layerRoot petrinaut + * @layerName Package surface + * @role The host-facing entry point: the contexts and types an embedder wires up + * @entryPoint @hashintel/petrinaut + * @boundary package — everything re-exported here is public API covered by semver + */ + export type { ErrorTracker } from "./react/error-tracker-context"; export { ErrorTrackerContext } from "./react/error-tracker-context"; export type { PetrinautOptimization } from "./react/optimization-context"; diff --git a/libs/@hashintel/petrinaut/src/react/execution-frame/provider.tsx b/libs/@hashintel/petrinaut/src/react/execution-frame/provider.tsx index b44171305fa..4ced261a87f 100644 --- a/libs/@hashintel/petrinaut/src/react/execution-frame/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/execution-frame/provider.tsx @@ -1,3 +1,10 @@ +/** + * @layerRoot react.execution-frame + * @layerName Execution frame source + * @role Abstracts where frames come from, so canvas and timeline work for live runs and recordings alike + * @invariant Consumers depend only on this interface, never on whether the frames are live or replayed + */ + import { use, useState, type FC, type PropsWithChildren } from "react"; import { diff --git a/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx b/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx index ed7aff3caa0..4618a57226e 100644 --- a/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx @@ -1,3 +1,10 @@ +/** + * @layerRoot react.experiments + * @layerName Experiments provider + * @role Tracks Monte Carlo experiment handles and their streamed metric results + * @boundary worker — Only metric aggregates arrive from the experiment worker; frame buffers never reach this layer + */ + import { use, useEffect, useRef, useState } from "react"; import { v4 as generateUuid } from "uuid"; diff --git a/libs/@hashintel/petrinaut/src/react/hooks/index.ts b/libs/@hashintel/petrinaut/src/react/hooks/index.ts index 69fd67a3a96..ed5fd112011 100644 --- a/libs/@hashintel/petrinaut/src/react/hooks/index.ts +++ b/libs/@hashintel/petrinaut/src/react/hooks/index.ts @@ -1,3 +1,10 @@ +/** + * @layerRoot react.hooks + * @layerName Shared hooks + * @role Cross-cutting hooks over the providers — documents, parameters, window lifecycle + * @invariant Each hook reads from an existing context; none creates state of its own, so hook order never affects ownership + */ + // Public hook surface for `@hashintel/petrinaut/react`. // // Each hook reads from an existing React context (SDCPN, Simulation, Playback, diff --git a/libs/@hashintel/petrinaut/src/react/index.ts b/libs/@hashintel/petrinaut/src/react/index.ts index 67ebe507cde..b2439beabd5 100644 --- a/libs/@hashintel/petrinaut/src/react/index.ts +++ b/libs/@hashintel/petrinaut/src/react/index.ts @@ -1,7 +1,15 @@ -// Public surface for `@hashintel/petrinaut/react` — React bindings. -// -// Hooks, contexts, and bridge providers that synchronize a Core instance with -// React. No visual widgets — `/ui` builds on top of this. +/** + * Public surface for `@hashintel/petrinaut/react` — React bindings. + * + * Hooks, contexts, and bridge providers that synchronize a Core instance with + * React. No visual widgets — `/ui` builds on top of this. + * + * @layerRoot react + * @layerName React bindings + * @role Contexts, hooks and providers that mirror core state into React + * @entryPoint @hashintel/petrinaut/react + * @invariant No imports from `ui/` — this layer must be mountable without rendering the editor, which is what makes the providers testable in isolation + */ // --- Instance access + low-level adapters --- export { PetrinautInstanceContext } from "./instance-context"; diff --git a/libs/@hashintel/petrinaut/src/react/lsp/provider.tsx b/libs/@hashintel/petrinaut/src/react/lsp/provider.tsx index 0cf2e64836c..0e118515f94 100644 --- a/libs/@hashintel/petrinaut/src/react/lsp/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/lsp/provider.tsx @@ -1,3 +1,10 @@ +/** + * @layerRoot react.lsp + * @layerName LSP provider + * @role Exposes the core language client to the editor as React context + * @boundary thread — Every completion, hover and diagnostic is an async round trip to the language-server worker + */ + import { use, useEffect, useRef, useState, useSyncExternalStore } from "react"; import { diff --git a/libs/@hashintel/petrinaut/src/react/playback/README.md b/libs/@hashintel/petrinaut/src/react/playback/README.md index b3ad0e91cd7..4178b8719d3 100644 --- a/libs/@hashintel/petrinaut/src/react/playback/README.md +++ b/libs/@hashintel/petrinaut/src/react/playback/README.md @@ -1,3 +1,11 @@ +--- +layer: react.playback +name: Playback provider +role: Drives the viewed frame with a requestAnimationFrame loop and applies the per-mode ack policy +invariants: + - Owns the ack/backpressure decision for the whole app — view-only never acks, so nothing is computed while merely scrubbing +--- + # Playback Module React context for viewing simulation frames at controlled speeds. diff --git a/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx b/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx index 3461a78e77f..5be5702a1fd 100644 --- a/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx @@ -1,3 +1,11 @@ +/** + * @layerRoot react.simulation + * @layerName Simulation provider + * @role Owns the run configuration and mirrors the core simulation handle into React + * @boundary thread — Wraps the core's worker transport; every frame the UI reads has crossed a thread boundary + * @invariant The initial marking is session state, configuration for the next run, and survives a reset + */ + import { use, useEffect, useRef, useState } from "react"; import { diff --git a/libs/@hashintel/petrinaut/src/react/state/README.md b/libs/@hashintel/petrinaut/src/react/state/README.md new file mode 100644 index 00000000000..cbb9d45dd88 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/react/state/README.md @@ -0,0 +1,14 @@ +--- +layer: react.state +name: Editor state +role: Contexts owning editor-session state — active net, selection, settings, undo/redo, read-only mode +invariants: + - Read-only mode is derived here and consumed everywhere, so no component decides for itself whether editing is allowed +--- + +State belonging to an editing session rather than to a document or a run. + +Read-only is the load-bearing piece: simulate mode locks editing except for an +allow-list, and `use-is-read-only` is the single source of that answer. +Components ask rather than working it out from mode flags, because a component +that guesses will eventually guess differently from its neighbour. diff --git a/libs/@hashintel/petrinaut/src/ui/index.ts b/libs/@hashintel/petrinaut/src/ui/index.ts index 9bbd5a12027..26f10927476 100644 --- a/libs/@hashintel/petrinaut/src/ui/index.ts +++ b/libs/@hashintel/petrinaut/src/ui/index.ts @@ -1,8 +1,16 @@ -// Public surface for `@hashintel/petrinaut/ui` — the opinionated visual editor. -// -// `` is the single editor entry: it takes a -// `PetrinautDocHandle` and renders the full editor on top of -// `` (`/react`). +/** + * Public surface for `@hashintel/petrinaut/ui` — the opinionated visual editor. + * + * `` is the single editor entry: it takes a + * `PetrinautDocHandle` and renders the full editor on top of + * `` (`/react`). + * + * @layerRoot ui + * @layerName Editor UI + * @role The visual editor: canvas, panels, dialogs and the Monaco integration + * @entryPoint @hashintel/petrinaut/ui + * @invariant Consumes the React layer's contexts rather than reaching into the core directly, so state ownership stays in one place + */ export { Petrinaut } from "./petrinaut"; export type { PetrinautAiMessage } from "./views/Editor/panels/ai-assistant-panel"; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/provider.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/provider.tsx index 3fef0372d82..28e953e5fce 100644 --- a/libs/@hashintel/petrinaut/src/ui/monaco/provider.tsx +++ b/libs/@hashintel/petrinaut/src/ui/monaco/provider.tsx @@ -1,3 +1,10 @@ +/** + * @layerRoot ui.monaco + * @layerName Monaco integration + * @role Wires the Monaco editor to the language server for authoring user code + * @boundary thread — Each sync component bridges a Monaco provider to an async worker request + */ + import { CompletionSync } from "./completion-sync"; import { MonacoContext } from "./context"; import { DiagnosticsSync } from "./diagnostics-sync"; diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx index a32c21a68b0..876f0acc382 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx @@ -1,3 +1,9 @@ +/** + * @layerRoot ui.views.editor + * @layerName Editor shell + * @role Arranges the panels, toolbars and dialogs around the canvas + */ + import { use, useState } from "react"; import { type MenuItem } from "@hashintel/ds-components"; diff --git a/libs/@hashintel/petrinaut/src/ui/views/README.md b/libs/@hashintel/petrinaut/src/ui/views/README.md new file mode 100644 index 00000000000..b1568a01a2e --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/views/README.md @@ -0,0 +1,9 @@ +--- +layer: ui.views +name: Views +role: The top-level screens the editor composes — the editor shell and the net canvas +--- + +Each subfolder is a screen rather than a widget. The split matters because the +canvas is reused outside the full editor — Actual mode renders a net with no +editing affordances — so it cannot depend on the editor shell around it. diff --git a/libs/@hashintel/petrinaut/src/ui/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/ui/views/SDCPN/sdcpn-view.tsx index 611d0eef4fe..a4f500aca7d 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/SDCPN/sdcpn-view.tsx @@ -1,3 +1,10 @@ +/** + * @layerRoot ui.views.canvas + * @layerName Canvas + * @role Renders the net as an interactive graph, with node and arc interaction + * @invariant Reads frame state through the execution-frame interface, so the same canvas renders live runs and recordings + */ + import "@xyflow/react/dist/style.css"; import { Background, ReactFlow, SelectionMode } from "@xyflow/react"; import { diff --git a/libs/@local/petrinaut-arch-docs/.gitignore b/libs/@local/petrinaut-arch-docs/.gitignore new file mode 100644 index 00000000000..5a2f7068984 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/.gitignore @@ -0,0 +1,9 @@ +# Build output. Regenerate with `mise run doc:architecture`. +# +# Not versioned: it is derived entirely from the annotations in the source and +# the authored pages in `content/`, so committing it would mean reviewing the +# same change twice and resolving conflicts in generated files. +bundle/ + +# Scratch copies from mutation-testing runs. Never part of the package. +.mut/ diff --git a/libs/@local/petrinaut-arch-docs/.oxlintrc.json b/libs/@local/petrinaut-arch-docs/.oxlintrc.json new file mode 100644 index 00000000000..979f705f360 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/.oxlintrc.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../../node_modules/oxlint/configuration_schema.json", + "plugins": ["import", "unicorn", "typescript"], + "categories": { + "correctness": "error" + }, + "env": { + "builtin": true, + "es2026": true + }, + "rules": { + "default-case-last": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "no-console": "error", + "no-param-reassign": ["error", { "props": true }], + "no-shadow": "error", + "no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_+", + "varsIgnorePattern": "^_+" + } + ], + "no-use-before-define": "error", + "import/no-cycle": "error", + "import/no-duplicates": "error", + "import/no-mutable-exports": "error", + "import/no-self-import": "error", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unsafe-function-type": "error", + "unicorn/filename-case": "error" + }, + "ignorePatterns": ["bundle/**", "content/**", ".turbo/**"] +} diff --git a/libs/@local/petrinaut-arch-docs/LICENSE-APACHE.md b/libs/@local/petrinaut-arch-docs/LICENSE-APACHE.md new file mode 100644 index 00000000000..4b43328a923 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/LICENSE-APACHE.md @@ -0,0 +1,189 @@ +# Apache License + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +- **(a)** You must give any other recipients of the Work or Derivative Works a copy of + this License; and +- **(b)** You must cause any modified files to carry prominent notices stating that You + changed the files; and +- **(c)** You must retain, in the Source form of any Derivative Works that You distribute, + all copyright, patent, trademark, and attribution notices from the Source form + of the Work, excluding those notices that do not pertain to any part of the + Derivative Works; and +- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any + Derivative Works that You distribute must include a readable copy of the + attribution notices contained within such NOTICE file, excluding those notices + that do not pertain to any part of the Derivative Works, in at least one of the + following places: within a NOTICE text file distributed as part of the + Derivative Works; within the Source form or documentation, if provided along + with the Derivative Works; or, within a display generated by the Derivative + Works, if and wherever such third-party notices normally appear. The contents of + the NOTICE file are for informational purposes only and do not modify the + License. You may add Your own attribution notices within Derivative Works that + You distribute, alongside or as an addendum to the NOTICE text from the Work, + provided that such additional attribution notices cannot be construed as + modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: Apply the Apache License to a specific file + +To apply the Apache License to an individual file, attach the following notice. +The text should be enclosed in the appropriate comment syntax for the file +format. + + Copyright © 2025–, HASH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/libs/@local/petrinaut-arch-docs/LICENSE-MIT.md b/libs/@local/petrinaut-arch-docs/LICENSE-MIT.md new file mode 100644 index 00000000000..d85585ee20d --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/LICENSE-MIT.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright © 2025–, HASH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/@local/petrinaut-arch-docs/LICENSE.md b/libs/@local/petrinaut-arch-docs/LICENSE.md new file mode 100644 index 00000000000..6dad94c0e5a --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/LICENSE.md @@ -0,0 +1,3 @@ +# License + +Licensed under either of the [Apache License, Version 2.0](LICENSE-APACHE.md) or [MIT license](LICENSE-MIT.md) at your option. diff --git a/libs/@local/petrinaut-arch-docs/README.md b/libs/@local/petrinaut-arch-docs/README.md new file mode 100644 index 00000000000..3986f5d873d --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/README.md @@ -0,0 +1,312 @@ +# `@local/petrinaut-arch-docs` + +Generates the Petrinaut architecture documentation from annotations in the +source, and bundles it with hand-written MDX into one portable artefact. + +```sh +# Regenerate the bundle after changing annotations or code +yarn workspace @local/petrinaut-arch-docs doc:architecture + +# Check the annotations without writing anything (what CI runs) +yarn workspace @local/petrinaut-arch-docs lint:arch-docs +``` + +## Why this exists + +Architecture docs rot because nothing fails when they stop being true. Two +specific failures motivated this package: + +- `petrinaut-core/scripts/generate-dependency-diagrams.mjs` held the architecture + as ~180 lines of `if (path.startsWith("simulation/monte-carlo/"))` mappings, + far from the code they described, 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` + were missing from the diagram entirely. +- `petrinaut-core/docs/architecture/*.html` was 3,100 lines of hand-written HTML + that nothing verified against the code. Its content has been migrated into + authored MDX under `content/simulation/`, where it sits beside the generated + pages; migrating it surfaced claims that had already gone stale. + +Here the architecture is declared next to the code it describes, and CI fails +when a declaration stops matching reality. + +## Declaring a layer + +A declaration is two lines — an id and a one-line role: + +```ts +/** + * @layerRoot core.simulation.monte-carlo + * @role Runs many simulations with bounded frame memory + */ +``` + +That is a complete declaration. Everything below is optional, and worth adding +only where the fact is real. + +| Tag | Adds | +| ------------- | ---------------------------------------------------------- | +| `@layerName` | Display name. Defaults to the last id segment. | +| `@entryPoint` | A public import specifier reaching this layer. Repeatable. | +| `@boundary` | ``. Repeatable. | +| `@invariant` | Something that must stay true here. Repeatable. | +| `@layer` | Assigns **this file alone** to an already-declared layer. | + +`@boundary` kinds are `thread`, `worker`, `process`, `network`, `package` and +`sandbox`; the separator may be `—`, `-` or `:`. + +`@layer` is the escape hatch for a single misplaced file. Reach for it rarely — a +file that needs it is usually a file in the wrong folder. + +Tags are read from any block comment, only at the start of a line, so mentioning +`@layerRoot` in prose declares nothing. A tag's text wraps across lines until the +next tag or a blank line. + +### Declaring from a README instead + +A folder `README.md` can declare the same thing in frontmatter, and its prose +becomes the layer's page body — so a folder README that already explains itself +becomes an architecture page for free. Use it when the folder has real prose to +carry, or when no single file is the obvious host. Otherwise prefer the doc +comment: it needs no new file. + +```yaml +--- +layer: core.simulation.monte-carlo +role: Runs many simulations with bounded frame memory +name: Monte Carlo runtime +entryPoints: + - "@hashintel/petrinaut-core/workers/monte-carlo" +boundaries: + - kind: worker + note: Frame buffers stay inside the worker +invariants: + - Frame memory is bounded regardless of run length +--- +``` + +Use one or the other on a folder, never both. A README with no `layer` key is +left alone as an ordinary document, and is linked from its layer page under +"Further reading". + +Layer ids are dotted and hierarchical, and every ancestor must itself be +declared — `core.simulation.monte-carlo` requires `core.simulation` and `core`. + +### Inheritance is what keeps this small + +A file with no tags belongs to the nearest ancestor folder that declares a layer. +That is why ~40 declarations cover 412 files: you declare a layer where the +architecture actually changes, not on every file. + +## The output: a portable bundle + +Written to `bundle/`, which is **git-ignored build output** — it is derived +entirely from the annotations and from `content/`, so committing it would mean +reviewing the same change twice and resolving conflicts in generated files. +Regenerate it whenever you need it; nothing depends on a stored copy. + +The bundle is framework-neutral by design — the Starlight site in +`apps/petrinaut-docs` and hash.dev are both just consumers. + +| File | What it is | +| ------------------------------------ | ---------------------------------------------------------------- | +| `architecture.json` | The model: layers, edges, boundaries, invariants, enforced rules | +| `architecture.md` | The whole architecture as one file — the cheapest read for an AI | +| `manifest.json` | Page tree for building navigation without crawling `pages/` | +| `pages/**.mdx` | Generated layer pages, plus authored pages merged in | +| `components/*.tsx` | React diagram components imported by authored pages | +| `components/architecture-layouts.ts` | Pre-computed diagram geometry (generated) | +| `diagrams/*.d2` | Diagram sources (diffable) | +| `diagrams/*.svg` | Rendered diagrams | + +**Generated** MDX is YAML frontmatter plus plain CommonMark — no JSX, no +imports, no framework components — which is what lets it render in Astro, in +hash.dev's Next.js MDX pipeline, and as plain text. + +**Authored** pages may additionally import the diagram components below, which is +where every requirement the bundle places on a host comes from: + +| A host must provide | For | +| ---------------------------------- | ------------------------------------------------------------------- | +| A React-capable MDX pipeline | Any authored page that imports a diagram component | +| `d3-zoom` and `d3-selection` | Pan and zoom on the layer map, and nothing else | +| A hydration directive on that page | Folding the layer map; without one it still renders as a static SVG | + +Nothing else. In particular the bundle asks for **no Markdown or Rehype +plugins** — a host renders it with its Markdown pipeline exactly as configured, +which is what keeps "render the bundle" a small job rather than a negotiation. + +### Embedding the bundle elsewhere + +A host reads `manifest.json`, maps each page's `slug` onto its own URL space, and +renders `pages/`. + +One contract to honour: **links between generated pages are relative and assume +slugs map to URLs without a trailing slash.** A host that serves +`/architecture/core/simulation/` rather than `/architecture/core/simulation` +must rewrite them; `manifest.json` gives you every slug to do so. The Starlight +consumer sets `trailingSlash: "never"` for this reason. + +## The interactive layer map + +`components/architecture-graph.tsx` renders the layer tree as a diagram a reader +can fold and unfold. It is the one part of the bundle with a dependency beyond +React — `d3-zoom`, which a host must install — so it is worth knowing what that +buys and what it costs. + +**One renderer, not two.** The same component draws the diagram on the server and +in the browser; hydrating adds a zoom transform and working fold controls, and +changes nothing else. An earlier version handed the interactive view to a +node-editor library, which meant two code paths that had to agree about the same +picture and quietly stopped agreeing — that library marks a node +`pointer-events: none` unless it is selectable, draggable or connectable, so a +read-only node full of links became unclickable. A read-only diagram wants a +viewer, not an editor: there is nothing here to select, drag or connect. + +**No layout runs in the browser.** The reachable fold states are enumerable — 30 +for the current model, because folding a layer makes its descendants' own states +unobservable — so the build lays out every one of them with ELK and emits the +coordinates as `components/architecture-layouts.ts`. That keeps `elkjs` a +devDependency of this package, never shipped, which matters because it is +EPL-2.0 rather than MIT/Apache. + +**It renders without JavaScript.** The server emits a plain inline `` from +those coordinates, with a real `` per layer and ELK's routed edges. A +host that never hydrates the island still gets a correct, navigable diagram — +just a fixed one, since folding and panning need a script. + +Two props are the host's business, because the bundle cannot know them: + +```jsx + +``` + +Markdown links in the pages are rewritten by the host's own pipeline; these are +built in JSX and are not. `hrefSuffix` matches however the host addresses pages — +`.html` for the Starlight consumer, usually empty elsewhere. + +### What is drawn, and what is not + +Folding re-points every edge at whatever is still visible. Two cases stop being +drawable and are reported on the node as _internal_ instead: + +- an edge between two layers folded into the same box, and +- an edge between a layer and something nested inside it — an arrow from a box + into itself. + +Reciprocal pairs are merged into one edge with two arrowheads, keeping both +counts. None of this invents a dependency: an aggregated edge sums real +`fileDependencies`, and an internal count reports real imports. + +`src/emit/collapse.ts` holds all of it as pure functions over the model, with +tests. Nothing about folding is decided in the browser. + +## Hand-written pages (optional) + +`content/` is entirely optional. With no `content/` directory at all, the +generator emits a bundle of generated pages only, and the docs site renders it — +`/` redirects to the generated overview instead of an authored home page. Add +pages when you have something to say that an import graph cannot express; delete +them freely. + +Anything in `content/` is copied into the bundle and merged into the same +manifest as the generated pages. Slugs mirror the directory layout; `title`, +`description` and `sidebar_order` come from frontmatter. + +### Attaching a page to a layer + +By default an authored page sits at the top level, as a standalone narrative +entry. Add `attachTo` and it moves _inside_ the generated tree instead, beneath +the page for the layer it explains: + +```yaml +--- +title: Memory model +description: Where simulation state actually lives. +attachTo: core.simulation # a layer declared in the source +sidebar_order: 10 +--- +``` + +The page's slug becomes `architecture/core/simulation/memory-model`, the layer's +page gains a **Guides** section linking to it, and any host that nests by slug +shows the guide beside the generated reference for the same code. + +`attachTo` is not a layer declaration — it references a layer declared in a +package, and CI fails if that layer does not exist. Declaring layers from +`content/` remains forbidden. + +Generated pages occupy `sidebar_order` 1000 and above, so within a layer the +attached guides (low numbers) sort ahead of its sub-layers. + +### Diagram components + +`content/components/` holds React components that authored pages import: + +```mdx +import { ByteMap } from "@diagrams/byte-map"; + + +``` + +The `@diagrams/` alias 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`. +The components ship _inside_ the bundle (`components/`), so a host renders them +from the artefact rather than needing its own copy. + +Two rules keep them portable, and both are load-bearing: + +- **Plain React, no dependencies.** No design system, no Astro, no `next/*`. + Styling lives in `components/diagram.css`, which derives its colours from the + host's `currentColor` so it works on light and dark themes it has never seen. +- **String props, never JSX.** JSX written inside MDX is compiled by the _host's_ + MDX renderer, and passing that to a React component fails at render. Props are + strings, and `` `backticks` `` render as ``. + +This is the one place the bundle asks something of its host: rendering these +pages needs a React-capable MDX pipeline. Generated pages remain plain +CommonMark and need nothing, and `architecture.md` — the single-file artefact +for agents — contains no components at all. + +### Linking between pages + +Because `attachTo` decides where a page ends up, an authored page cannot know its +own depth and so cannot write a correct relative link by hand. Name the target +instead, and the generator computes the path: + +| Syntax | Resolves to | +| -------------------------------------- | --------------------------------------------------- | +| `[text](layer:core.simulation.engine)` | that layer's generated page | +| `[text](doc:simulation/memory-model)` | another authored page, by its path under `content/` | + +Fragments are preserved (`layer:core.hir#invariants`). A target that does not +resolve is a build error rather than a broken link nobody notices. Ordinary +relative and absolute links are left untouched, so a top-level page that will +never move can still use them. + +Authored pages carry the reasoning — why a boundary is where it is, what was +tried before — which no import graph can supply. They may not declare a layer; +layer declarations belong in the packages. + +## What CI enforces + +`lint:arch-docs` fails on: + +- a source file that no declaration covers +- a layer whose dotted id implies an undeclared ancestor +- a duplicate layer id, or two declarations on one folder +- an unknown `@boundary` kind, a boundary with no note, a duplicated singular tag +- a `@entryPoint` that is not an export of any covered package +- a dependency violating a rule in `architecture.config.ts` +- an `attachTo` naming a layer that does not exist +- a `layer:` or `doc:` link target that does not resolve +- an `@diagrams/` import naming a component that does not exist + +Warnings (reported, non-fatal): a layer with no files and no sub-layers, an +`exports` subpath with no resolvable source entry. + +## Adding a package + +Add it to `packages` in `architecture.config.ts`, declare a root layer in its +source, and run a build. `sourceDirectory` defaults to `src`, so build +configuration outside it is deliberately not part of any layer. diff --git a/libs/@local/petrinaut-arch-docs/architecture.config.ts b/libs/@local/petrinaut-arch-docs/architecture.config.ts new file mode 100644 index 00000000000..9f7c183b23b --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/architecture.config.ts @@ -0,0 +1,107 @@ +/** + * What the architecture bundle covers, and what it forbids. + * + * This file holds only things that genuinely cannot be read from the code: + * which packages participate, what to skip, and the layer-crossing rules. The + * layer taxonomy itself deliberately lives *in* the packages, as README + * frontmatter and `@layerRoot` annotations — that is the whole point of the + * exercise. Resist the temptation to add a path→layer mapping here. + */ + +import type { ArchitecturePackageInput } from "./src/model"; + +export interface LayerRule { + /** Layer id or ancestor prefix the rule applies to. */ + from: string; + /** Layer id or ancestor prefix that must not be reached. */ + to: string; + reason: string; +} + +export interface ArchitectureConfig { + packages: ArchitecturePackageInput[]; + rules: LayerRule[]; + ignoredDirectories: string[]; + ignoredFilePattern: RegExp; + /** Where the generated bundle is written, repo-relative. */ + outputDirectory: string; + /** Where hand-written MDX is read from, repo-relative. */ + contentDirectory: string; + /** Base URL for source links in generated pages. */ + sourceUrlPrefix: string; +} + +export const config: ArchitectureConfig = { + packages: [ + { + name: "@hashintel/petrinaut-core", + path: "libs/@hashintel/petrinaut-core", + description: + "Headless SDCPN engine: document model, HIR compiler, simulation runtimes, LSP. No React, no DOM.", + language: "typescript", + }, + { + name: "@hashintel/petrinaut", + path: "libs/@hashintel/petrinaut", + description: + "React editor built on the headless core: providers, canvas, panels, Monaco integration.", + language: "typescript", + }, + ], + + /** + * Each rule is a claim the drift check enforces against the real import + * graph, so adding one here without fixing the code fails CI. A rule matches + * an edge when the edge's endpoints are the named layer or a descendant of it. + */ + rules: [ + { + from: "core", + to: "react", + reason: + "the headless core is published without React and must stay usable from Node and workers", + }, + { + from: "core", + to: "ui", + reason: "the headless core must not reach into editor components", + }, + { + from: "core", + to: "petrinaut", + reason: + "the core is the lower package of the pair and cannot depend on its consumer", + }, + { + from: "react", + to: "ui", + reason: + "state providers must not depend on the components that render them, so the React layer stays testable without mounting the editor", + }, + ], + + ignoredDirectories: [ + "node_modules", + "dist", + "__fixtures__", + "__snapshots__", + "docs", + ], + + /** + * Tests, stories and ambient declarations are excluded: they describe the + * architecture's *use*, not its shape, and including them inflates every + * layer's file count with fixtures. + */ + ignoredFilePattern: + /(?:\.(?:test|spec|stories)\.[cm]?[jt]sx?$|\.d\.ts$|\/CHANGELOG\.md$|\/LICENSE[^/]*\.md$)/u, + + /** + * The bundle is the product; the Starlight site in `apps/petrinaut-docs` and + * hash.dev are both just consumers of it. It therefore lives with the + * generator that owns its schema, not inside either renderer. + */ + outputDirectory: "libs/@local/petrinaut-arch-docs/bundle", + contentDirectory: "libs/@local/petrinaut-arch-docs/content", + sourceUrlPrefix: "https://github.com/hashintel/hash/blob/main/", +}; diff --git a/libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx b/libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx new file mode 100644 index 00000000000..b8541da8f06 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx @@ -0,0 +1,88 @@ +/** + * A byte-addressed memory map: offset gutter on the left, one row per section. + * + * This is the diagram the hand-written HTML pages did best — reading the frame + * format as a table loses the sense of a single contiguous buffer, which is the + * whole point of the format. Offsets are supplied rather than computed, because + * the map illustrates one worked example rather than a live layout. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +export interface SegmentSpec { + /** Byte offset where this section starts. */ + offset: number; + name: string; + /** Element type and count, e.g. `u32 × P`. */ + type?: string; + /** The typed-array view used to read it. */ + view?: string; + accent?: "core" | "worker" | "none"; +} + +export interface ByteMapProps { + title?: string; + segments: SegmentSpec[]; + /** Total length, rendered as the closing offset. */ + byteLength?: number; + caption?: string; +} + +const accentVariable = (accent: SegmentSpec["accent"]): string | undefined => + accent === undefined || accent === "none" + ? undefined + : `var(--pnd-${accent})`; + +export const ByteMap = ({ + title, + segments, + byteLength, + caption, +}: ByteMapProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {segments.map((segment) => ( + + ))} + {byteLength === undefined ? null : ( + <> +
{byteLength}
+
+ + )} +
+ {caption === undefined ? null : ( +
+ +
+ )} +
+); + +const Segment = ({ segment }: { segment: SegmentSpec }) => ( + <> +
{segment.offset}
+
+ + + + {segment.type === undefined ? null : ( + + + + )} + {segment.view === undefined ? null : ( + + + + )} +
+ +); diff --git a/libs/@local/petrinaut-arch-docs/content/components/diagram.css b/libs/@local/petrinaut-arch-docs/content/components/diagram.css new file mode 100644 index 00000000000..ea746b45df2 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/diagram.css @@ -0,0 +1,233 @@ +/* + * Shared styling for the architecture diagram components. + * + * Deliberately self-contained: plain CSS with no preprocessor, no design-system + * import, and no framework assumptions, so the same file works in the Starlight + * site and in any host that renders the bundle. Colours derive from the host's + * text colour where possible, and everything falls back sensibly if the host + * defines none of the custom properties. + */ + +.pnd { + --pnd-line: color-mix(in srgb, currentColor 25%, transparent); + --pnd-muted: color-mix(in srgb, currentColor 65%, transparent); + --pnd-surface: color-mix(in srgb, currentColor 5%, transparent); + + --pnd-core: #3676b8; + --pnd-react: #7051b5; + --pnd-ui: #3d8055; + --pnd-worker: #b5762f; + + margin: 1.5rem 0; + font-size: 0.85rem; + line-height: 1.45; +} + +.pnd-title { + margin-bottom: 0.5rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--pnd-muted); +} + +.pnd-lanes { + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(var(--pnd-columns, 1), minmax(0, 1fr)); +} + +.pnd-lane { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + border: 1px solid var(--pnd-line); + border-radius: 0.5rem; + background: var(--pnd-surface); +} + +.pnd-lane-title { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--pnd-muted); +} + +.pnd-box { + padding: 0.5rem 0.6rem; + border: 1px solid var(--pnd-line); + border-left: 3px solid var(--pnd-accent, var(--pnd-line)); + border-radius: 0.35rem; + background: var(--pnd-surface); +} + +.pnd-box-label { + font-weight: 600; +} + +.pnd-box-note { + margin-top: 0.15rem; + color: var(--pnd-muted); +} + +.pnd-box code, +.pnd-lane code, +.pnd-step code { + font-size: 0.9em; +} + +/* Pipeline ------------------------------------------------------------- */ + +.pnd-pipeline { + display: flex; + flex-wrap: wrap; + align-items: stretch; + gap: 0.4rem; +} + +.pnd-step { + flex: 1 1 8rem; + min-width: 8rem; + padding: 0.5rem 0.6rem; + border: 1px solid var(--pnd-line); + border-radius: 0.35rem; + background: var(--pnd-surface); +} + +.pnd-step-index { + font-size: 0.7rem; + font-weight: 700; + color: var(--pnd-muted); +} + +.pnd-arrow { + align-self: center; + color: var(--pnd-muted); +} + +/* Byte map ------------------------------------------------------------- */ + +.pnd-bytes { + display: grid; + grid-template-columns: auto 1fr; + gap: 0 0.6rem; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.pnd-offset { + padding-top: 0.45rem; + text-align: right; + color: var(--pnd-muted); + font-size: 0.75rem; + white-space: nowrap; +} + +.pnd-seg { + display: flex; + flex-wrap: wrap; + gap: 0.25rem 0.6rem; + align-items: baseline; + padding: 0.4rem 0.6rem; + margin-bottom: 0.25rem; + border: 1px solid var(--pnd-line); + border-left: 3px solid var(--pnd-accent, var(--pnd-line)); + border-radius: 0.3rem; + background: var(--pnd-surface); +} + +.pnd-seg-name { + font-weight: 600; +} + +.pnd-seg-type, +.pnd-seg-view { + color: var(--pnd-muted); + font-size: 0.78rem; +} + +.pnd-seg-view { + margin-left: auto; +} + +/* Sequence ------------------------------------------------------------- */ + +.pnd-sequence { + display: grid; + gap: 0.35rem; +} + +.pnd-msg { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + gap: 0.5rem; + align-items: center; +} + +.pnd-msg-label { + padding: 0.35rem 0.55rem; + border: 1px solid var(--pnd-line); + border-radius: 0.3rem; + background: var(--pnd-surface); +} + +.pnd-msg-dir { + color: var(--pnd-muted); + font-size: 1rem; +} + +.pnd-msg-note { + color: var(--pnd-muted); +} + +.pnd-msg-spacer { + min-width: 0; +} + +.pnd-actors { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + gap: 0.5rem; + margin-bottom: 0.5rem; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--pnd-muted); +} + +.pnd-actors span:last-child { + text-align: right; +} + +/* Legend --------------------------------------------------------------- */ + +.pnd-legend { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0.6rem; + font-size: 0.75rem; + color: var(--pnd-muted); +} + +.pnd-legend span::before { + content: ""; + display: inline-block; + width: 0.6rem; + height: 0.6rem; + margin-right: 0.3rem; + border-radius: 0.15rem; + background: var(--pnd-accent, currentColor); +} + +@media (max-width: 640px) { + .pnd-lanes { + grid-template-columns: minmax(0, 1fr); + } + + .pnd-msg, + .pnd-actors { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/libs/@local/petrinaut-arch-docs/content/components/inline.tsx b/libs/@local/petrinaut-arch-docs/content/components/inline.tsx new file mode 100644 index 00000000000..0777ef723e4 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/inline.tsx @@ -0,0 +1,27 @@ +/** + * Renders a plain string, turning `backticked` spans into ``. + * + * Diagram components take strings rather than nodes on purpose. JSX written + * inside MDX is compiled by the host's MDX renderer — Astro's, here — and + * handing that to a React component fails at render time with "Objects are not + * valid as a React child". Strings avoid the interop entirely, which also means + * a host embedding the bundle needs no special MDX/JSX configuration. + * + * Backticks are the one piece of formatting these diagrams actually need, so + * they are supported directly rather than by accepting arbitrary markup. + */ + +import type { ReactElement } from "react"; + +export const Inline = ({ text }: { text: string }): ReactElement => ( + <> + {text.split("`").map((part, index) => + // Odd indices are the spans that sat between a pair of backticks. + index % 2 === 1 ? ( + {part} + ) : ( + {part} + ), + )} + +); diff --git a/libs/@local/petrinaut-arch-docs/content/components/lanes.tsx b/libs/@local/petrinaut-arch-docs/content/components/lanes.tsx new file mode 100644 index 00000000000..c447ddb95b0 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/lanes.tsx @@ -0,0 +1,92 @@ +/** + * Lane-and-box diagrams: parallel columns of boxes, one column per thread or + * mode. + * + * Data-driven rather than hand-marked-up, so a page describes the content and + * the component owns the presentation. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +/** Accent colours, keyed to the layer families the diagrams talk about. */ +export type Accent = "core" | "react" | "ui" | "worker" | "none"; + +const accentVariable = (accent: Accent | undefined): string | undefined => + accent === undefined || accent === "none" + ? undefined + : `var(--pnd-${accent})`; + +export interface BoxSpec { + label: string; + note?: string; + accent?: Accent; +} + +export interface LaneSpec { + title: string; + accent?: Accent; + boxes: BoxSpec[]; +} + +export const Box = ({ label, note, accent }: BoxSpec) => ( +
+
+ +
+ {note === undefined ? null : ( +
+ +
+ )} +
+); + +export interface LanesProps { + title?: string; + lanes: LaneSpec[]; + /** Colour key rendered beneath the lanes. */ + legend?: { label: string; accent: Accent }[]; +} + +export const Lanes = ({ title, lanes, legend }: LanesProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {lanes.map((lane) => ( +
+
{lane.title}
+ {lane.boxes.map((box, index) => ( + + ))} +
+ ))} +
+ {legend === undefined ? null : ( +
+ {legend.map((entry) => ( + + {entry.label} + + ))} +
+ )} +
+); diff --git a/libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx b/libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx new file mode 100644 index 00000000000..7a2d95765ba --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx @@ -0,0 +1,72 @@ +/** + * A numbered left-to-right chain of steps, wrapping on narrow screens. + * + * Used where the old HTML pages drew an arrow chain: the compilation pipeline, + * the fixed order of a simulation step, the metrics pipeline. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +export interface StepSpec { + label: string; + note?: string; +} + +export interface PipelineProps { + title?: string; + steps: StepSpec[]; + /** Numbers the steps. Off for pipelines where order is not a sequence. */ + numbered?: boolean; +} + +export const Pipeline = ({ title, steps, numbered = false }: PipelineProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {steps.map((step, index) => ( + // A fragment per step so the separator sits between, not inside, boxes. + + ))} +
+
+); + +const Step = ({ + index, + last, + numbered, + step, +}: { + index: number; + last: boolean; + numbered: boolean; + step: StepSpec; +}) => ( + <> +
+ {numbered ?
{index + 1}
: null} +
+ +
+ {step.note === undefined ? null : ( +
+ +
+ )} +
+ {last ? null : ( + + )} + +); diff --git a/libs/@local/petrinaut-arch-docs/content/components/sequence.tsx b/libs/@local/petrinaut-arch-docs/content/components/sequence.tsx new file mode 100644 index 00000000000..b3a59db76e5 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/sequence.tsx @@ -0,0 +1,83 @@ +/** + * A two-actor message sequence: who sends what, in order, with side notes. + * + * Kept to two actors deliberately. The hand-written HTML had three columns + * (host, main-thread runtime, worker), which read as three lifelines but made + * every message ambiguous about which hop it described. Two actors and an + * explicit direction says the same thing without the ambiguity. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +export interface MessageSpec { + /** `right` is left-actor → right-actor. */ + direction: "right" | "left"; + label: string; + note?: string; +} + +/** A phase separator, rendered as a full-width note rather than a message. */ +export interface PhaseSpec { + phase: string; +} + +export type SequenceRow = MessageSpec | PhaseSpec; + +export interface SequenceProps { + title?: string; + actors: [string, string]; + rows: SequenceRow[]; +} + +const isPhase = (row: SequenceRow): row is PhaseSpec => "phase" in row; + +export const Sequence = ({ title, actors, rows }: SequenceProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {actors[0]} +
+
+ {rows.map((row, index) => + isPhase(row) ? ( +
+ +
+ ) : ( +
+ {row.direction === "right" ? ( + <> +
+ +
+ +
+ {row.note === undefined ? null : } +
+ + ) : ( + <> +
+ {row.note === undefined ? null : } +
+ +
+ +
+ + )} +
+ ), + )} +
+
+); diff --git a/libs/@local/petrinaut-arch-docs/content/index.mdx b/libs/@local/petrinaut-arch-docs/content/index.mdx new file mode 100644 index 00000000000..4fb384ebb39 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/index.mdx @@ -0,0 +1,78 @@ +--- +title: Petrinaut architecture +description: How the Petrinaut packages fit together, and where to start reading. +sidebar_order: 0 +--- + +Petrinaut is a visual editor and simulation runtime for Stochastic Dynamic +Coloured Petri Nets (SDCPN). This documentation is in two halves that are +maintained differently, and it is worth knowing which you are reading. + +## The diagrams + +Every box is a layer and every arrow is real imports, counted from the +dependency graph rather than drawn by hand. There are three kinds, each showing +a different slice: + +- The [Architecture](architecture) page opens with the **top-level layers** — + the whole system at a glance, with everything below the first level folded + into its parent. +- Every layer page opens with its **neighbourhood**: what that layer depends on, + and what depends on it. This is usually the one you want. +- A layer that has sub-layers also gets a **drill-down** of its direct children. + +Where a layer has more neighbours than fit legibly, the remainder is drawn as a +single "further layers" node rather than omitted, and the full list is always in +the tables on the page. + +**Generated pages** — everything under [Architecture](architecture) — are +extracted from annotations in the source on every build, and CI fails if they +drift. Layer sizes, dependency counts, boundaries and invariants all come from +the code. If a generated page disagrees with the code, that is a bug in the +annotations, not in the page. + +**Authored pages** — this one, and its siblings — carry the reasoning that no +import graph can supply: why a boundary sits where it does, what the alternatives +were, which mistakes are easy to make. They are hand-written and can go stale, so +they avoid restating facts the generated pages already own. + +## Where to start + +- [Two execution paths](two-execution-paths) — the single most important thing to + understand before changing anything in the simulation layers. +- [Architecture](architecture) — the generated map: layers, dependencies, + boundaries. +- [Working on the architecture](working-on-the-architecture) — how to declare a + layer, and what CI will hold you to. + +## Simulation internals + +Detail that the generated pages do not carry — binary formats, message +protocols, and the costs behind each design choice. + +- [Memory model](doc:simulation/memory-model) — which buffer lives on which thread, + and the frame format. +- [How a step is computed](doc:simulation/stepping) — the fixed stage order, and + determinism. +- [Compiling user code](doc:simulation/user-code) — the HIR pipeline, scenarios, and + what the sandbox does not protect against. +- [Worker protocol and backpressure](doc:simulation/protocol) — messages, and + the ack contract. +- [Monte Carlo experiments](doc:simulation/experiments) — bounded memory, and why + only aggregates reach the UI. + +## The packages at a glance + +`@hashintel/petrinaut-core` is the headless half: the document model, the HIR +compiler for user-authored code, the simulation runtimes, and the language +server. It has no React, no DOM and no Monaco, which is what lets it run +unchanged in Node and inside workers. + +`@hashintel/petrinaut` is the editor built on top. It splits into a React layer +that owns state and a UI layer that renders it — and the dependency only goes one +way, which is enforced rather than merely intended. + +Two further packages are not yet covered by the generated model: +`@hashintel/petrinaut-cli` and `apps/petrinaut-opt` (the Python Optuna service). +Adding them is a matter of declaring layers in their source; see +[Working on the architecture](working-on-the-architecture). diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx new file mode 100644 index 00000000000..5e6d4cb9b51 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx @@ -0,0 +1,121 @@ +--- +title: Monte Carlo experiments +description: How many runs execute in bounded memory, and why only aggregates reach the UI. +sidebar_order: 10 +attachTo: core.simulation.monte-carlo +--- + +import { Lanes } from "@diagrams/lanes"; + +An experiment runs many independent simulations of the same net with bounded +memory, aggregates metrics inside the worker, and ships only small JSON metric +frames to the host. **Frame buffers never cross the thread boundary.** + +The engine itself is shared with the interactive path — the same enablement, +lambda, kernel and dynamics code — adapted to different frame storage. + +## The layers + +The host holds one experiment handle per experiment, subscribed to its status, +progress and metric stores. The handle runs either in worker mode or in local +mode, where the simulator runs on the calling thread; local mode is what tests +and embedders use. + +Inside the worker, the loop is: advance every run one frame, post progress and +any pending metric frames, yield, repeat. Cancellation is checked between +batches, and the default batch is 4. + +The simulator owns N runs and advances them **round-robin**, one frame each per +call. That is deliberate: it stops long runs from starving short ones, so +progress reporting stays meaningful and a single pathological run cannot stall +the batch. + +## Per run + +Each run carries its own seed, parameter values and initial marking — defaults +come from the experiment config and are overridable per run, typically as +`base seed + index`. + +Errors are **per run**: one run failing does not stop the others, which is what +makes a large sweep survive a parameter combination that happens to throw. A run +completes on its own deadlock or on the shared max time. + +Each run also reports buffer telemetry — bytes used, capacity, and reallocation +count — which is the first thing to look at when an experiment is slower than +expected. + +## Two buffers, swapped every step + + + +The consequence to internalise: **frame readers handed to metric code are only +valid during observation.** They are views over a buffer that the next step will +overwrite. Anything a metric needs later must be extracted into the value it +returns. + +If the next token count does not fit, only the target buffer reallocates, taking +the larger of what is required and double the current capacity. + +The Monte Carlo buffer layout is a leaner sibling of the interactive frame: the +same sections, **no 64-byte header**, one extra section for elapsed frames, and a +token region with spare capacity beyond what is used. Views are created once per +buffer rather than per read. + +## Metrics are computed where the data is + +```text +specs → compile before start → sample per frame → aggregate → metric frames +``` + +Metric specs are serializable and sent at init: either an expression (carrying +HIR artifacts) or a built-in such as mean place-token count or transition firing +count. + +Sampling visits every run's current frame through the same reader interface the +interactive path uses. Aggregation happens across runs — mean, sum, min, max, +last for a scalar — or keeps the run axis and bins it into a distribution. + +What reaches the host is therefore small JSON: a scalar frame carries a value +per frame and per time, and a distribution frame carries bins as value/frequency +pairs. A host can paint a bins-by-frames heatmap, or derive medians and +percentiles from the bins, **without ever holding simulation state**. + +## Contrast with the interactive path + +No acks, no backpressure: the worker free-runs to completion in small batches, +and the only upstream control is cancel. That is affordable precisely because the +payloads are aggregates rather than frames — there is no consumer pace to respect +because there is no unbounded data to hold back. + +## Known costs + +Runs are round-robin on one worker thread, though the run-state model was +designed so runs can shard across workers later. Buffer growth is naive doubling; +static analysis of arc weights could size buffers up front and remove +reallocations entirely. diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx new file mode 100644 index 00000000000..c236ed3e511 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx @@ -0,0 +1,191 @@ +--- +title: Memory model +description: Where simulation state actually lives, and the binary frame format it lives in. +sidebar_order: 10 +attachTo: core.simulation +--- + +import { ByteMap } from "@diagrams/byte-map"; +import { Lanes } from "@diagrams/lanes"; + +Everything the simulation computes is stored in raw `ArrayBuffer`s read through +typed-array views. There is no object graph of tokens: tokens are packed structs, +decoded to JavaScript objects only at the read boundary. + +Knowing which buffer lives on which thread is the thing that makes the rest of +the simulation layers make sense. + +## Where each piece lives + + + +| Memory | Structure | Thread | Lifetime | +| ------------------- | -------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------ | +| SDCPN document | Plain JS objects (editor state) | Main | Structured-cloned into the worker at init. Later edits do not affect a running simulation. | +| Initial marking | JSON — count or `TokenRecord[]` per place | Main | Packed into frame 0 by `buildSimulation`. | +| `EngineFrame` | `ArrayBuffer` — 64-byte header plus typed sections and packed tokens | Worker | Immutable snapshot per step, appended to the instance's frame history. | +| Frame payload | `{ time, frame: ArrayBuffer }` | Worker → Main | Structured-clone copy per frame. Retained by the frame store for scrubbing. | +| Frame reader | Typed-array views over the stored buffer | Main | Zero-copy. Materialises `TokenRecord`s only when asked. | +| Monte Carlo buffers | 2 × `ArrayBuffer` per run | MC worker | Swapped each step. Never cross to the main thread. | +| Compiled user code | Versioned HIR buffer programs | Workers | Produced once at init. | + +The asymmetry between rows 4 and 6 is the whole difference between the two +execution paths — see [Two execution paths](doc:two-execution-paths). + +## The frame format + +One `ArrayBuffer` per frame, read through section-typed views. The frame stores +**no IDs and no time**: decoding requires the layout derived from the SDCPN +(place and transition order, per-place stride and slot layout), and time travels +alongside as payload metadata. + +`createEngineFrame()` computes every offset and is the only frame constructor; +`readEngineFrame()` recreates the same views for reading. + + + +Header fields worth knowing: `magic` (`0x5046524d`, a corrupt-frame guard), +`version` (currently `2`, asserted on decode), `placeCount`/`transitionCount` +(checked against the layout on read), and `byteLength` (checked on read). + +### Token packing + +Each coloured place owns a contiguous run of `count × strideBytes` starting at +its byte offset, giving O(1) access to any place. Uncoloured places have stride +0 and only a count. + +| Element type | Physical | Notes | +| ------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `real` | `f64` — 8 B | | +| `integer` | `f64` — 8 B, rounded | Exact within ±2^53. `i32` was rejected for silent wraparound; `i64` because bigint is contagious into user code. | +| `boolean` | `u8` — 1 B | Not bit-packed: that would break byte-granular copies for marginal savings. | +| `uuid` | `u64 × 2` — 16 B | Two little-endian lanes via a shared `BigUint64Array`, combined to one `bigint` at the boundary. Never routed through `number`. | +| `string` | `u64` pool reference — 8 B | The frame stores an ID into an append-only per-run intern pool. The pool lives on the instance, not the frame, so frames stay fixed-stride and byte-copyable. | + +Fields are ordered per colour by decreasing alignment, and the stride is rounded +up to 8 bytes. One module owns all token-byte indexing; every whole-token move is +a byte-range copy. + +Because the string pool never crosses the worker boundary with the frames, each +frame payload ships an append-only delta of new strings that the main-thread +store accumulates and hands to the reader for decoding. + +## Known cost: triple retention + +Each quick-simulation frame currently exists three times — in the worker's frame +history, as a structured-clone copy in the main-thread store, and transiently in +the `postMessage` queue. Frames are never transferred, and the worker's history +is only ever read at its latest entry. + +Retention is deliberately isolated behind the frame store, so this can change +without touching consumers. Transferring buffers instead of copying is not a drop-in +fix: computing the next frame re-reads the latest stored frame, so detaching the +posted buffer first requires stepping from a retained working copy. diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx new file mode 100644 index 00000000000..50e507cbdb6 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx @@ -0,0 +1,158 @@ +--- +title: Worker protocol and backpressure +description: The messages between host and simulation worker, and the ack contract that bounds memory. +sidebar_order: 10 +attachTo: core.simulation.worker +--- + +import { Sequence } from "@diagrams/sequence"; + +Both simulation workers speak a small typed message protocol over `postMessage`, +wrapped in a transport that queues messages until the worker boots. This page +covers the quick-simulation worker; the experiment worker is on +[Monte Carlo](doc:simulation/experiments). + +## Messages + +**Host → worker** + +| Type | Payload | Effect | +| ----------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `init` | net, marking, parameters, seed, `dt`, `maxTime`, backpressure settings | Builds the simulation, packs frame 0, replies `frame(0)` then `ready`. Resets the ack cursor. | +| `start` | — | Starts the compute loop. No-op if running; refused after completion or error. | +| `pause` | — | Stops the loop, keeps all state, replies `paused`. | +| `stop` | — | Discards the simulation entirely. | +| `setBackpressure` | `maxFramesAhead`, `batchSize` | Live-updates loop tuning. | +| `ack` | `frameNumber` | Raises the ack cursor, unblocking computation. | + +**Worker → host** + +| Type | Payload | Effect | +| ------------------ | ----------------------------------------- | ---------------------------------------------------------------- | +| `ready` | initial frame count | Resolves the creation promise; status becomes Ready. | +| `frame` / `frames` | `{ time, frame }`, single or batch | Appended to the frame store, which republishes count and latest. | +| `paused` | frame number | Status becomes Paused. | +| `complete` | `"deadlock"` or `"maxTime"`, frame number | Status becomes Complete, emitted on the event stream. | +| `error` | message, offending item id if known | Status becomes Error; rejects creation if still initialising. | + +Errors carry the SDCPN item id where known, which is what lets the editor +highlight the transition whose lambda threw rather than showing a bare message. + +## One run, end to end + + + +## The ack contract + +**Nothing is computed until the consumer asks for it.** The worker's loop blocks +while the ack cursor is behind, so the consumer's pace bounds the worker's memory +and CPU: + +```js +while (isRunning) { + if (lastAckedFrame < 0 || currentFrame - lastAckedFrame >= maxFramesAhead) { + await delay(10); // wait for the consumer + continue; + } + // compute up to batchSize frames, stopping on complete or error + post("frames", batch); + await delay(0); // let pause / stop / ack messages in +} +``` + +Note the first condition: after `init`, the cursor starts below zero, so a fresh +worker computes **nothing** until the first ack. Starting a run is therefore two +steps — `start`, then acks — and a host that starts but never acks looks hung +while behaving exactly as designed. + +The per-mode profiles the core exposes: + +| Play mode | `maxFramesAhead` | `batchSize` | Behaviour | +| --------------- | ---------------- | ----------- | ----------------------------------------------------------------------------------------- | +| view only | — | — | No profile: the React layer pauses the worker and never acks, so nothing new is computed. | +| compute buffer | 40 | 10 | Acks when playback is within ~0.5 s of the last computed frame. | +| compute max | 10000 | 500 | Acks on every arrival — compute as fast as possible. | +| worker defaults | 1000 | 1000 | Used when `init` omits the settings. | + +## Lifecycle + +```text +Initializing ──ready──▶ Ready ──run()──▶ Running ──pause()──▶ Paused + │ │ + └──── run() ◀────────┘ + Running ──deadlock | maxTime──▶ Complete + any ──error msg───────────▶ Error +``` + +That is the **main-thread** state. The worker mirrors a smaller internal status, +in which complete and error are terminal. + +A sharp edge: `reset()` returns the main-thread status to Ready, but the worker +has discarded its simulation, so a subsequent `run()` sends `start` to a worker +with nothing to run. What actually happens in the UI flow is a full re-init. +Treat reset as "throw this away", not "rewind". + +## Main-thread responsibilities + +The host-side runtime does four things worth knowing about: + +- **Snapshot preparation** — sanitizes the net for enabled extensions and + flattens component instances, so the main-thread layout matches what the worker + produces. A place-count mismatch throws on read rather than decoding garbage. +- **Transport** — queues outbound messages until the worker resolves; tests can + inject a pre-built transport instead of a real worker. +- **Retention** — the in-memory store keeps every frame so playback can scrub. + Alternative stores (latest-only, sliding window, persisted) can be swapped in + without changing consumers. +- **Publication** — status and frames as readable stores, plus an event stream. + +## Known costs + +The 10 ms poll while waiting for acks is a busy-wait; a promise per ack would be +exact. And frames could be posted with a transfer list to halve copying, but only +once stepping no longer re-reads the latest stored frame — see +[Memory model](doc:simulation/memory-model). diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx new file mode 100644 index 00000000000..92a65982498 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx @@ -0,0 +1,71 @@ +--- +title: How a step is computed +description: The fixed order of one simulation step, and what makes a run reproducible. +sidebar_order: 10 +attachTo: core.simulation.engine +--- + +import { Pipeline } from "@diagrams/pipeline"; + +`buildSimulation(input)` turns an SDCPN into a runnable instance; +`computeNextFrame(simulation)` advances it by one immutable frame. Both are pure +return values — the worker is what turns them into protocol messages. + +Building an instance flattens component instances, compiles user code, and packs +the initial marking into frame 0. Compilation is selective: lambdas only where +available, kernels only for coloured outputs, dynamics only for colours with at +least one real element. + +## One step, in order + +The order is fixed, and several surprising behaviours follow from it. + + + +Discrete derivatives are forced to zero in step 2; removals compact the buffer +and additions append in step 3. + +Dynamics run before transitions, so a transition sees token values already +advanced by this step's integration. Timers only advance on a step where nothing +fired, so they measure idle time rather than wall time. + +## Immutability, and what it costs + +Every stage that changes state allocates a new buffer: dynamics, token removal, +and token insertion each rebuild the frame. That makes history trivially correct +— previous frames are never mutated — at the cost of allocation churn per step. + +This is the single clearest optimisation opportunity in the engine. Monte Carlo already +demonstrates the alternative: two buffers swapped per step, no history. Bringing +that to the interactive path means moving history retention wholly onto the main +thread, which the frame store already isolates. + +## Determinism + +One seeded RNG is threaded through stochastic lambda sampling and distribution +draws, as a pure `nextRandom(state) → [value, nextState]` whose state lives on +the instance. + +The guarantee: the same `(SDCPN, marking, parameters, seed, dt)` always +reproduces the same frame sequence. This is what makes experiments comparable +across runs, and it is why the RNG state is threaded rather than global — a +global would couple runs sharing a worker. diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx new file mode 100644 index 00000000000..284bea7fd1e --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx @@ -0,0 +1,106 @@ +--- +title: Compiling user code +description: How authored TypeScript becomes buffer programs, and what the sandbox does and does not protect. +sidebar_order: 10 +attachTo: core.simulation.authoring +--- + +import { Pipeline } from "@diagrams/pipeline"; + +Dynamics, lambdas, kernels and metrics all compile through the HIR: user +TypeScript is lowered to a source-spanned IR, checked against the net, and +emitted as buffer-native JavaScript with offsets and strides baked in. Scenario +code takes a separate, simpler path. + + + +The editor's type checking is the same pipeline seen from the other end: the LSP +generates the `Color_*`, `Parameters`, `Dynamics` and kernel declarations Monaco +checks against, so editor diagnostics and runtime artifacts come from the same +HIR checks and emitters. + +## What compiles where + +| Surface | Authored as | Executed in | +| ----------------- | -------------------------------------------- | -------------------------------------------------- | +| Transition lambda | module, `Lambda(fn)` | Worker, per enablement check | +| Transition kernel | module, `TransitionKernel(fn)` | Worker, per firing | +| Dynamics | module, `Dynamics(fn)` | Worker, per step per place | +| Metric | plain function body | Main thread (timeline) and MC worker (experiments) | +| Scenario | per-place expressions, or one code-mode body | Main thread, before the run starts | +| Place visualizer | module, `Visualization(fn)` returning JSX | Main thread, at render | + +The visualizer is the one inherently React-based surface — it authors JSX — which +is why it compiles in the UI package rather than in the headless core. + +Metrics must return a finite number; NaN and infinity throw rather than +propagating into a chart. + +## Scenarios produce a marking, not a run + +Scenario compilation is separate from the HIR pipeline and produces an initial +marking (plus parameter overrides) before the simulation is created. + +Two modes, with one asymmetry worth remembering: **per-place mode is keyed by +place ID, code mode is keyed by place name.** In code mode, unknown names are +silently dropped. + +In per-place mode an uncoloured place takes an expression evaluated with +parameters in scope, rounded and clamped at zero; a coloured place takes row +arrays in colour-element order, each coerced with typed defaults for missing +columns, and extra columns throw. + +## What the sandbox is for + +Scenario expressions run with a list of globals shadowed — `window`, `document`, +`globalThis`, `fetch`, `importScripts`, `Function`, timers and similar — so those +name lookups resolve to `undefined`. Because shadowing only stops identifier +lookup, the sandbox additionally blocks the constructor-chain escape +(`({}).constructor.constructor` reaching `Function`) for the duration of the +call, by swapping the `.constructor` descriptors on the built-in prototypes and +restoring them afterwards. + +**Treat this as robustness hardening, not isolation.** User code executes with +`new Function` in the same realm as the host. It protects against typos and +accidental API use, not against an attacker. + +## Distributions are sampled once + +Kernels can construct distribution values rather than numbers. They defer +sampling through the ABI sink, and the engine samples each value once per token +with the seeded RNG. That is what lets chained `.map(fn)` transforms share a +single draw and keeps runs reproducible. + +When stochasticity is disabled the runtime is not injected at all, and plain +values are required. + +## Compatibility + +HIR artifacts carry a version and a fingerprint of their sanitized compilation +input. Any schema, code, or extension change requires recompilation before the +engine will run them — a stale artifact is refused rather than silently +mismatched against a changed net. diff --git a/libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx b/libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx new file mode 100644 index 00000000000..455b491dd71 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx @@ -0,0 +1,52 @@ +--- +title: Two execution paths +description: Why Quick Simulation and Experiments share an engine but not a memory model. +sidebar_order: 1 +--- + +There are two independent ways to execute a net, and almost every surprising +thing about the simulation layers follows from the difference between them. + +**Quick Simulation** runs one interactive simulation and keeps _every frame_, so +the user can scrub backwards and forwards through the run. Frames accumulate in +a store on the main thread. + +**Experiments** run many Monte Carlo simulations with _bounded memory_ — two +reusable buffers per run — and ship only metric aggregates back to the host. +Frame buffers never leave the worker. + +## Why not one path + +The obvious simplification is to make experiments reuse the interactive +simulator and throw away frames afterwards. It does not work: an experiment is +hundreds or thousands of runs, and retaining frames even transiently makes peak +memory scale with run length times concurrency. Bounding memory is not an +optimisation on the experiment path, it is the reason that path exists. + +The consequence to keep in mind: **anything you add to the frame format costs +memory on the interactive path and nothing on the experiment path, while +anything you add to metric aggregation costs on both.** A per-frame field is +cheap to add and expensive to keep; an aggregate is the reverse. + +## What this means when changing code + +The engine is shared, so a change to stepping or to the frame layout affects both +paths. The _controllers_ are not shared — the interactive path goes through the +simulation controller with its frame store, the experiment path through the Monte +Carlo runtime — so a change to run lifecycle usually needs doing twice, and +forgetting one is the classic bug here. + +If you are touching either, read the generated pages for +[the engine](layer:core.simulation.engine), +[the simulation controller](layer:core.simulation.runtime) and +[the Monte Carlo runtime](layer:core.simulation.monte-carlo) — their +declared invariants say which properties you must not break. + +## Actual mode is a third source, but not a third path + +Actual mode renders an execution that came from somewhere else entirely — a live +external system rather than a simulation. It does not run the engine at all. It +reaches the same canvas and timeline because both read frames through the +[execution-frame interface](layer:react.execution-frame), which abstracts +over "where frames come from". That indirection is why adding Actual mode did not require +touching the canvas. diff --git a/libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx b/libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx new file mode 100644 index 00000000000..6f10e87dad3 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx @@ -0,0 +1,136 @@ +--- +title: Working on the architecture +description: How to declare a layer, refine one, and what CI holds you to. +sidebar_order: 2 +--- + +The generated pages come from annotations in the Petrinaut source. This page is +the short version of how to change them; the full reference lives in +[`libs/@local/petrinaut-arch-docs/README.md`](https://github.com/hashintel/hash/blob/main/libs/@local/petrinaut-arch-docs/README.md). + +## Declaring a layer + +Add frontmatter to the folder's `README.md`: + +```yaml +--- +layer: core.simulation.monte-carlo +name: Monte Carlo runtime +role: Runs many simulations with bounded frame memory +--- +``` + +The prose below the frontmatter becomes the layer's page body, so an existing +folder README needs only the frontmatter added. + +For a folder with a barrel entry file and no README, annotate the entry file +instead: + +```ts +/** + * @layerRoot core.simulation.monte-carlo + * @layerName Monte Carlo runtime + * @role Runs many simulations with bounded frame memory + */ +``` + +Use one mechanism or the other, never both on the same folder. + +## You do not need to annotate every file + +A file with no annotation belongs to the nearest ancestor folder that declares a +layer. Roughly forty declarations cover four hundred files. Declare a layer where +the architecture genuinely changes — a new boundary, a different responsibility — +not on every directory that exists. + +## Recording facts that belong to code + +Boundaries and invariants can be declared either in the README frontmatter or on +the specific file they concern, and the file is usually the better home because +the claim then sits where someone would break it: + +```ts +/** + * @boundary worker — frame buffers never cross to the main thread + * @invariant Two reusable frame buffers per run; no per-frame allocation + */ +``` + +Write invariants as properties that could be violated. "Handles simulation state" +is a description and belongs in `@role`; "no per-frame allocation" is a claim a +reviewer can check, and that is what earns a place here. + +## Moving or renaming a folder + +Nothing to update, provided the folder keeps its declaration with it. That is the +main practical gain over the old path-matching script, where a move silently +re-bucketed files into a fallback group. + +Renaming a _layer id_ is a real change: it moves the layer's page and invalidates +inbound links, and every descendant id has to move with it. + +## Adding a new package + +Add it to `packages` in +[`architecture.config.ts`](https://github.com/hashintel/hash/blob/main/libs/@local/petrinaut-arch-docs/architecture.config.ts), +declare a root layer in its source, and rebuild. Build configuration outside the +package's source directory is deliberately excluded — it configures the build, it +is not part of the design. + +## What CI will hold you to + +Run the check before pushing: + +```bash +yarn workspace @local/petrinaut-arch-docs lint:arch-docs +``` + +It fails when a source file no layer covers appears, when a `@entryPoint` stops being a +real export, when a dependency violates a declared rule, and when the committed +bundle no longer matches the source. The last one is the common case: after +changing annotations or moving code, regenerate with + +```bash +yarn workspace @local/petrinaut-arch-docs doc:architecture +``` + +and commit the result alongside your change. + +## Adding a hand-written guide + +Authored pages live in `libs/@local/petrinaut-arch-docs/content/` and are +optional. To have one sit _inside_ the architecture tree rather than beside it, +name the layer it explains: + +```yaml +--- +title: Memory model +attachTo: core.simulation +sidebar_order: 10 +--- +``` + +The page then appears beneath that layer, and the layer's page links to it under +"Guides". + +Because `attachTo` decides where the page ends up, link to other pages by name +rather than by path — `[text](layer:core.simulation.engine)` for a generated +page, `[text](doc:simulation/memory-model)` for another authored one. A target +that does not resolve fails the check. + +## Declaring a rule + +Rules live in `architecture.config.ts` and are checked against the real import +graph: + +```ts +{ + from: "react", + to: "ui", + reason: "state providers must not depend on the components that render them", +} +``` + +Add one only when the code already satisfies it — a rule is a statement of fact +that CI keeps true, not an aspiration. Check first with a build; if there are +existing violations, either fix them in the same change or leave the rule out. diff --git a/libs/@hashintel/petrinaut-core/dependency-cruiser.tsconfig.json b/libs/@local/petrinaut-arch-docs/dependency-cruiser.tsconfig.json similarity index 54% rename from libs/@hashintel/petrinaut-core/dependency-cruiser.tsconfig.json rename to libs/@local/petrinaut-arch-docs/dependency-cruiser.tsconfig.json index cbff533fc46..afe5f0c20e9 100644 --- a/libs/@hashintel/petrinaut-core/dependency-cruiser.tsconfig.json +++ b/libs/@local/petrinaut-arch-docs/dependency-cruiser.tsconfig.json @@ -1,6 +1,7 @@ { + "//": "Used only by the architecture generator, to give dependency-cruiser's TypeScript resolver the workspace path mappings. Moved here from petrinaut-core when the hand-written diagram script was retired, so the config lives with its only consumer.", "compilerOptions": { - "baseUrl": "../../../..", + "baseUrl": "../../..", "paths": { "@hashintel/petrinaut-core": [ "libs/@hashintel/petrinaut-core/src/index.ts" @@ -14,6 +15,15 @@ "@hashintel/petrinaut-core/hir-runtime": [ "libs/@hashintel/petrinaut-core/src/hir-runtime.ts" ], + "@hashintel/petrinaut-core/ai": [ + "libs/@hashintel/petrinaut-core/src/ai.ts" + ], + "@hashintel/petrinaut-core/compiled-model": [ + "libs/@hashintel/petrinaut-core/src/compiled-model.ts" + ], + "@hashintel/petrinaut-core/optimization": [ + "libs/@hashintel/petrinaut-core/src/optimization.ts" + ], "@hashintel/petrinaut-core/workers/lsp": [ "libs/@hashintel/petrinaut-core/src/workers/lsp.ts" ], @@ -25,5 +35,8 @@ ] } }, - "include": ["src", "../petrinaut/src"] + "include": [ + "../../@hashintel/petrinaut-core/src", + "../../@hashintel/petrinaut/src" + ] } diff --git a/libs/@local/petrinaut-arch-docs/package.json b/libs/@local/petrinaut-arch-docs/package.json new file mode 100644 index 00000000000..ffc1a02e034 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/package.json @@ -0,0 +1,38 @@ +{ + "name": "@local/petrinaut-arch-docs", + "version": "0.0.0-private", + "private": true, + "description": "Generates the Petrinaut architecture bundle from in-code annotations", + "license": "(MIT OR Apache-2.0)", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "doc:architecture": "tsx src/cli.ts build", + "fix:eslint": "oxlint --fix --report-unused-disable-directives-severity=error .", + "lint:arch-docs": "tsx src/cli.ts check", + "lint:eslint": "oxlint --report-unused-disable-directives-severity=error .", + "lint:tsc": "tsc --noEmit", + "test:unit": "vitest --run src" + }, + "dependencies": { + "dependency-cruiser": "18.0.0", + "js-yaml": "4.3.0", + "zod": "4.4.3" + }, + "devDependencies": { + "@hashintel/petrinaut": "workspace:*", + "@hashintel/petrinaut-core": "workspace:*", + "@local/tsconfig": "workspace:*", + "@types/js-yaml": "^4", + "@types/node": "22.18.13", + "oxlint": "1.63.0", + "tsx": "4.20.6", + "typescript": "5.9.3", + "vitest": "4.1.10" + } +} diff --git a/libs/@local/petrinaut-arch-docs/src/build.ts b/libs/@local/petrinaut-arch-docs/src/build.ts new file mode 100644 index 00000000000..3ace7036f65 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/build.ts @@ -0,0 +1,336 @@ +/** + * Builds the bundle in memory. + * + * Kept separate from the CLI so `build` and `check` run the same pipeline — + * `check` builds and throws the files away, reporting only the diagnostics. + */ + +import { join } from "node:path"; + +import { config, type ArchitectureConfig } from "../architecture.config"; +import { runChecks } from "./check"; +import { + collectAuthoredContent, + type AuthoredComponent, + type AuthoredPage, +} from "./content"; +import { + buildManifest, + buildSingleFileArchitecture, + type BundleManifest, +} from "./emit/bundle-outputs"; +import { + buildNeighbourhoodDiagram, + buildOverviewDiagram, + buildSubtreeDiagram, +} from "./emit/d2"; +import { + buildPages, + resolveAuthoredLinks, + resolveComponentImports, + layerSlug, + type GeneratedPage, +} from "./emit/mdx"; +import { extract, type Diagnostic } from "./extract"; +import { buildGraph } from "./graph"; +import { + architectureModelSchema, + packageSchema, + type ArchitectureModel, +} from "./model"; + +export const GENERATOR_NAME = "@local/petrinaut-arch-docs"; + +export interface BuiltBundle { + model: ArchitectureModel; + manifest: BundleManifest; + generated: GeneratedPage[]; + authored: AuthoredPage[]; + /** Diagram name → D2 source. */ + diagramSources: Map; + /** Importable diagram components shipped with the bundle. */ + components: AuthoredComponent[]; + singleFile: string; + diagnostics: Diagnostic[]; +} + +/** Diagram embedded on the overview page. */ +const OVERVIEW_DIAGRAM = "overview"; + +/** + * Diagram names are namespaced by subdirectory rather than by a name prefix, + * because a layer id is only guaranteed unique among layer ids — a flat + * `around-${id}` scheme would collide with a top-level layer actually called + * `around-something`. + */ +const NEIGHBOURHOOD_PREFIX = "around"; +const SUBTREE_PREFIX = "within"; + +export const buildBundle = async (options: { + repoRoot: string; + overrides?: Partial; + /** + * Whether rendered SVGs will exist. When false, pages omit diagram images + * rather than pointing at files that were never written — a bundle that + * references a missing image fails the consuming site's build. + */ + includeDiagrams?: boolean; +}): Promise => { + const settings: ArchitectureConfig = { ...config, ...options.overrides }; + const { repoRoot } = options; + + // Normalise once up front so every stage sees defaulted fields such as + // `sourceDirectory` rather than each having to reapply them. + const packages = settings.packages.map((pkg) => packageSchema.parse(pkg)); + + const extraction = await extract({ + repoRoot, + packages, + ignoredDirectories: settings.ignoredDirectories, + ignoredFilePattern: settings.ignoredFilePattern, + }); + + const diagnostics: Diagnostic[] = [...extraction.diagnostics]; + + const graph = await buildGraph({ + repoRoot, + packages, + tsconfigPath: join( + repoRoot, + "libs/@local/petrinaut-arch-docs/dependency-cruiser.tsconfig.json", + ), + excludePattern: settings.ignoredFilePattern.source, + fileLayers: extraction.fileLayers, + layers: extraction.layers, + }); + + for (const warning of graph.warnings) { + diagnostics.push({ + file: "architecture.config.ts", + line: null, + severity: "warning", + message: warning, + }); + } + + const model = architectureModelSchema.parse({ + version: 1, + packages, + layers: extraction.layers, + edges: graph.edges, + rules: settings.rules, + } satisfies ArchitectureModel); + + diagnostics.push(...runChecks({ model, rules: settings.rules })); + + const diagramSources = new Map(); + + diagramSources.set( + OVERVIEW_DIAGRAM, + buildOverviewDiagram(model.layers, model.edges, GENERATOR_NAME), + ); + + // A neighbourhood diagram for *every* layer, including leaves — those are + // where a reader most often lands, and "what does this touch" is the question + // they arrive with. + for (const layer of model.layers) { + diagramSources.set( + `${NEIGHBOURHOOD_PREFIX}/${layer.id}`, + buildNeighbourhoodDiagram( + layer.id, + model.layers, + model.edges, + GENERATOR_NAME, + ), + ); + } + + // A drill-down diagram for every layer that has sub-layers, answering the + // other question: what is inside this one. + const parentLayerIds = model.layers + .filter((layer) => model.layers.some((other) => other.parent === layer.id)) + .map((layer) => layer.id); + + for (const parentId of parentLayerIds) { + diagramSources.set( + `${SUBTREE_PREFIX}/${parentId}`, + buildSubtreeDiagram(parentId, model.layers, model.edges, GENERATOR_NAME), + ); + } + + const authoredResult = await collectAuthoredContent({ + repoRoot, + contentDirectory: settings.contentDirectory, + }); + + for (const error of authoredResult.errors) { + diagnostics.push({ + file: error.file, + line: null, + severity: "error", + message: error.message, + }); + } + + const declaredLayerIds = new Set(model.layers.map((layer) => layer.id)); + const layerSlugsById = new Map( + model.layers.map((layer) => [layer.id, layerSlug(layer.id)]), + ); + + /** + * Resolve every authored page's final slug in one pass. + * + * An attached page moves beneath its layer's page so the guide and the + * generated reference for the same code sit together. Its file name becomes + * the last segment, which is why two guides attached to one layer must not + * share a file name. A page whose `attachTo` does not name a real layer is + * reported and stays where it was, so one bad guide does not move the rest. + */ + const authoredSlugs = new Map(); + const guidesByLayer = new Map< + string, + { slug: string; title: string; description: string }[] + >(); + + for (const page of authoredResult.pages) { + const attached = + page.attachTo !== null && declaredLayerIds.has(page.attachTo); + + if (page.attachTo !== null && !attached) { + diagnostics.push({ + file: page.sourceFile, + line: null, + severity: "error", + message: `attachTo \`${page.attachTo}\` is not a declared layer`, + }); + } + + if (!attached || page.attachTo === null) { + authoredSlugs.set(page.slug, page.slug); + continue; + } + + const leaf = page.slug.slice(page.slug.lastIndexOf("/") + 1); + const slug = `${layerSlug(page.attachTo)}/${leaf}`; + authoredSlugs.set(page.slug, slug); + guidesByLayer.set(page.attachTo, [ + ...(guidesByLayer.get(page.attachTo) ?? []), + { slug, title: page.title, description: page.description }, + ]); + } + + const componentNames = new Set( + authoredResult.components.map((component) => component.name), + ); + + // Authored pages address their targets by name (`layer:`, `doc:`, `@diagrams/`) + // rather than by path, because a page's depth is only known once `attachTo` is + // resolved. + const authored = authoredResult.pages.map((page) => { + const slug = authoredSlugs.get(page.slug) ?? page.slug; + const linked = resolveAuthoredLinks(page.contents, slug, { + layerSlugs: layerSlugsById, + docSlugs: authoredSlugs, + }); + const imported = resolveComponentImports( + linked.contents, + slug, + componentNames, + ); + + for (const target of [...linked.unresolved, ...imported.unresolved]) { + diagnostics.push({ + file: page.sourceFile, + line: null, + severity: "error", + message: `link target \`${target}\` does not resolve`, + }); + } + + return { + ...page, + slug, + path: `pages/${slug}.mdx`, + contents: imported.contents, + }; + }); + + const includeDiagrams = options.includeDiagrams ?? true; + + const generated = buildPages(model, { + sourceUrlPrefix: settings.sourceUrlPrefix, + overviewDiagram: includeDiagrams ? OVERVIEW_DIAGRAM : null, + neighbourhoodDiagrams: includeDiagrams + ? new Map( + model.layers.map((layer) => [ + layer.id, + `${NEIGHBOURHOOD_PREFIX}/${layer.id}`, + ]), + ) + : new Map(), + subtreeDiagrams: includeDiagrams + ? new Map(parentLayerIds.map((id) => [id, `${SUBTREE_PREFIX}/${id}`])) + : new Map(), + guidesByLayer, + }); + + const generatedSlugs = new Set(generated.map((page) => page.slug)); + for (const page of authored) { + if (generatedSlugs.has(page.slug)) { + diagnostics.push({ + file: page.sourceFile, + line: null, + severity: "error", + message: `authored page slug \`${page.slug}\` collides with a generated page; rename it`, + }); + } + } + + const manifest = buildManifest({ + generator: GENERATOR_NAME, + generated, + authored, + }); + + return { + model, + manifest, + generated, + authored, + diagramSources, + singleFile: buildSingleFileArchitecture(model), + components: authoredResult.components, + diagnostics, + }; +}; + +/** + * The bundle's text files, keyed by path relative to the bundle root. SVGs are + * excluded: they are produced by the external `d2` renderer, so comparing them + * would make drift detection depend on the renderer's exact version. + */ +export const bundleTextFiles = (bundle: BuiltBundle): Map => { + const files = new Map(); + + files.set("architecture.json", `${JSON.stringify(bundle.model, null, 2)}\n`); + files.set("manifest.json", `${JSON.stringify(bundle.manifest, null, 2)}\n`); + files.set("architecture.md", bundle.singleFile); + + for (const [name, source] of bundle.diagramSources) { + files.set(`diagrams/${name}.d2`, source); + } + + for (const page of bundle.generated) { + files.set(page.path, page.contents); + } + + for (const page of bundle.authored) { + files.set(page.path, page.contents); + } + + for (const component of bundle.components) { + files.set(component.path, component.contents); + } + + return files; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/check.test.ts b/libs/@local/petrinaut-arch-docs/src/check.test.ts new file mode 100644 index 00000000000..76619026ac8 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/check.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from "vitest"; + +import { + checkAncestorsDeclared, + checkEmptyLayers, + checkRules, + withinScope, +} from "./check"; + +import type { ArchitectureModel, Edge, Layer } from "./model"; + +/** + * These are the checks CI fails on, so each one is pinned in both directions: + * it fires when the rule is broken, and stays quiet when it is not. A check that + * silently stops firing is worse than no check, because the docs keep claiming to + * be verified. + */ + +const layer = (overrides: Partial & Pick): Layer => ({ + name: overrides.id, + parent: + overrides.id.lastIndexOf(".") === -1 + ? null + : overrides.id.slice(0, overrides.id.lastIndexOf(".")), + package: "@test/pkg", + role: "role", + declaredIn: `src/${overrides.id}/README.md`, + prose: null, + references: [], + files: ["src/a.ts"], + fileCount: 1, + lineCount: 1, + ...overrides, +}); + +const edge = (from: string, to: string, count = 1): Edge => ({ + from, + to, + fileDependencies: count, + examples: [{ from: `src/${from}.ts`, to: `src/${to}.ts` }], + crossesPackage: false, +}); + +const model = (layers: Layer[], edges: Edge[] = []): ArchitectureModel => ({ + version: 1, + packages: [], + layers, + edges, + rules: [], +}); + +describe("withinScope", () => { + it("matches the scope itself and its descendants", () => { + expect(withinScope("core", "core")).toBe(true); + expect(withinScope("core.simulation", "core")).toBe(true); + expect(withinScope("core.simulation.engine", "core")).toBe(true); + }); + + it("does not match a sibling that merely shares a prefix", () => { + expect(withinScope("coreutils", "core")).toBe(false); + expect(withinScope("react", "core")).toBe(false); + }); +}); + +describe("checkAncestorsDeclared", () => { + it("reports an implied ancestor that nobody declared", () => { + const diagnostics = checkAncestorsDeclared( + model([layer({ id: "core" }), layer({ id: "core.a.b" })]), + ); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toContain("`core.a`"); + expect(diagnostics[0]?.severity).toBe("error"); + }); + + it("stays quiet when the whole chain is declared", () => { + const diagnostics = checkAncestorsDeclared( + model([ + layer({ id: "core" }), + layer({ id: "core.a" }), + layer({ id: "core.a.b" }), + ]), + ); + + expect(diagnostics).toEqual([]); + }); +}); + +describe("checkRules", () => { + const layers = [ + layer({ id: "core" }), + layer({ id: "core.engine" }), + layer({ id: "ui" }), + layer({ id: "ui.views" }), + ]; + + it("fires when a descendant violates a rule stated at the parent", () => { + const diagnostics = checkRules( + model(layers, [edge("core.engine", "ui.views", 4)]), + [{ from: "core", to: "ui", reason: "core stays headless" }], + ); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toContain("core stays headless"); + expect(diagnostics[0]?.message).toContain("4 imports"); + // Names a real file so the fix does not begin with a search. + expect(diagnostics[0]?.file).toBe("src/core.engine.ts"); + }); + + it("stays quiet on the permitted direction", () => { + const diagnostics = checkRules( + model(layers, [edge("ui.views", "core.engine")]), + [{ from: "core", to: "ui", reason: "core stays headless" }], + ); + + expect(diagnostics).toEqual([]); + }); + + it("uses singular wording for a single import", () => { + const diagnostics = checkRules(model(layers, [edge("core", "ui", 1)]), [ + { from: "core", to: "ui", reason: "because" }, + ]); + + expect(diagnostics[0]?.message).toContain("1 import does"); + }); +}); + +describe("checkEmptyLayers", () => { + it("warns about a leaf layer with no files", () => { + const diagnostics = checkEmptyLayers( + model([layer({ id: "core", files: [], fileCount: 0 })]), + ); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.severity).toBe("warning"); + }); + + it("allows a grouping layer to hold no files of its own", () => { + const diagnostics = checkEmptyLayers( + model([ + layer({ id: "core", files: [], fileCount: 0 }), + layer({ id: "core.engine" }), + ]), + ); + + expect(diagnostics).toEqual([]); + }); +}); diff --git a/libs/@local/petrinaut-arch-docs/src/check.ts b/libs/@local/petrinaut-arch-docs/src/check.ts new file mode 100644 index 00000000000..810cf8f4f8d --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/check.ts @@ -0,0 +1,111 @@ +/** + * Checks that turn the documentation into something CI can hold to account. + * + * Documentation rots because nothing fails when it stops being true. Each check + * here makes one class of rot into a build error: a layer id implying an + * ancestor nobody declared, a rule the real import graph violates, a + * declaration whose folder has been emptied. + * + * Every check is a statement about the *graph* — which is the whole of what + * this version claims. It holds no unverifiable prose to account because it + * reads none. + */ + +import { ancestorLayerIds } from "./model"; + +import type { LayerRule } from "../architecture.config"; +import type { Diagnostic } from "./extract"; +import type { ArchitectureModel } from "./model"; + +/** True when `layerId` is the scope itself or nested inside it. */ +export const withinScope = (layerId: string, scope: string): boolean => + layerId === scope || layerId.startsWith(`${scope}.`); + +/** + * Every ancestor segment of a dotted layer id must itself be a declared layer. + * + * Without this, `core.simulation.monte-carlo` could exist while + * `core.simulation` does not, leaving the diagram with a container nobody + * described and the docs with a page that 404s from its own breadcrumb. + */ +export const checkAncestorsDeclared = ( + model: ArchitectureModel, +): Diagnostic[] => { + const declared = new Set(model.layers.map((layer) => layer.id)); + + return model.layers.flatMap((layer) => + ancestorLayerIds(layer.id) + .filter((ancestor) => !declared.has(ancestor)) + .map((ancestor) => ({ + file: layer.declaredIn, + line: null, + severity: "error" as const, + message: `layer \`${layer.id}\` implies an ancestor \`${ancestor}\` that is not declared anywhere`, + })), + ); +}; + +/** + * Rules are only worth stating if they are enforced, so a violation is an error + * rather than a note on a page. The message names representative files so the + * fix does not start with a search. + */ +export const checkRules = ( + model: ArchitectureModel, + rules: LayerRule[], +): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + + for (const rule of rules) { + for (const edge of model.edges) { + if ( + !withinScope(edge.from, rule.from) || + !withinScope(edge.to, rule.to) + ) { + continue; + } + + const example = edge.examples[0]; + + diagnostics.push({ + file: example?.from ?? edge.from, + line: null, + severity: "error", + message: `\`${edge.from}\` must not depend on \`${edge.to}\` (${rule.reason}); ${edge.fileDependencies} import${edge.fileDependencies === 1 ? "" : "s"} do${edge.fileDependencies === 1 ? "es" : ""}, e.g. ${example ? `${example.from} → ${example.to}` : "unknown"}`, + }); + } + } + + return diagnostics; +}; + +/** + * Layers with no files usually mean a declaration whose folder was emptied or + * renamed. Grouping layers legitimately hold no files of their own, so this is a + * warning: it is reported, but does not fail the build. + */ +export const checkEmptyLayers = (model: ArchitectureModel): Diagnostic[] => { + const hasChildren = new Set( + model.layers.flatMap((layer) => + layer.parent === null ? [] : [layer.parent], + ), + ); + + return model.layers + .filter((layer) => layer.fileCount === 0 && !hasChildren.has(layer.id)) + .map((layer) => ({ + file: layer.declaredIn, + line: null, + severity: "warning" as const, + message: `layer \`${layer.id}\` has no source files and no sub-layers — was its folder moved or emptied?`, + })); +}; + +export const runChecks = (options: { + model: ArchitectureModel; + rules: LayerRule[]; +}): Diagnostic[] => [ + ...checkAncestorsDeclared(options.model), + ...checkRules(options.model, options.rules), + ...checkEmptyLayers(options.model), +]; diff --git a/libs/@local/petrinaut-arch-docs/src/cli.ts b/libs/@local/petrinaut-arch-docs/src/cli.ts new file mode 100644 index 00000000000..523fcbc33e6 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/cli.ts @@ -0,0 +1,145 @@ +/** + * `arch-docs build` writes the bundle; `arch-docs check` reports on it. + * + * `check` is what CI runs: it builds everything, discards the files, and fails + * on any annotation error — an unannotated file, an undeclared ancestor, a + * violated rule — so the map cannot quietly stop matching the code. + */ + +import { chmod, mkdir, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { config } from "../architecture.config"; +import { buildBundle, bundleTextFiles, type BuiltBundle } from "./build"; +import { canRenderDiagrams, renderD2 } from "./emit/d2"; + +import type { Diagnostic } from "./extract"; + +const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url)); +const bundleRoot = join(repoRoot, config.outputDirectory); + +const supportsColour = process.stdout.isTTY === true; + +const colour = (code: string, text: string): string => + supportsColour ? `\u001B[${code}m${text}\u001B[0m` : text; + +const red = (text: string): string => colour("31", text); +const yellow = (text: string): string => colour("33", text); +const dim = (text: string): string => colour("2", text); + +const reportDiagnostics = (diagnostics: Diagnostic[]): { errors: number } => { + const errors = diagnostics.filter( + (diagnostic) => diagnostic.severity === "error", + ); + const warnings = diagnostics.filter( + (diagnostic) => diagnostic.severity === "warning", + ); + + for (const diagnostic of [...errors, ...warnings]) { + const location = + diagnostic.line === null + ? diagnostic.file + : `${diagnostic.file}:${diagnostic.line}`; + const label = + diagnostic.severity === "error" ? red("error") : yellow("warning"); + process.stderr.write(`${label} ${location}\n ${diagnostic.message}\n`); + } + + return { errors: errors.length }; +}; + +const summarise = (bundle: BuiltBundle): void => { + const files = bundle.model.layers.reduce( + (total, layer) => total + layer.fileCount, + 0, + ); + + process.stdout.write( + dim( + `${bundle.model.layers.length} layers \u00b7 ${bundle.model.edges.length} edges \u00b7 ${files} files \u00b7 ${bundle.generated.length} generated pages \u00b7 ${bundle.authored.length} authored pages\n`, + ), + ); +}; + +const writeBundle = async (bundle: BuiltBundle): Promise => { + // Generated pages, diagrams and components are rewritten wholesale so a + // renamed layer or a deleted component cannot leave an orphan behind, which + // would silently keep serving a description of code that no longer exists. + await rm(join(bundleRoot, "pages"), { recursive: true, force: true }); + await rm(join(bundleRoot, "diagrams"), { recursive: true, force: true }); + await rm(join(bundleRoot, "components"), { recursive: true, force: true }); + await mkdir(bundleRoot, { recursive: true }); + + for (const [path, contents] of bundleTextFiles(bundle)) { + const target = join(bundleRoot, path); + await mkdir(dirname(target), { recursive: true }); + await writeFile(target, contents, "utf8"); + } + + for (const name of bundle.diagramSources.keys()) { + const source = join(bundleRoot, `diagrams/${name}.d2`); + const output = join(bundleRoot, `diagrams/${name}.svg`); + const result = renderD2(repoRoot, source, output); + + if (!result.ok) { + process.stderr.write( + `${yellow("warning")} could not render ${name}.svg — the .d2 source was still written\n ${result.error}\n`, + ); + continue; + } + + // d2 writes 0600, which leaves the committed SVG unreadable to anything + // serving the bundle under a different user. + await chmod(output, 0o644); + } +}; + +const main = async (): Promise => { + const command = process.argv[2] ?? "build"; + + if (!["build", "check"].includes(command)) { + process.stderr.write( + `unknown command \`${command}\`; expected build or check\n`, + ); + return 2; + } + + // Probed before building so pages only reference diagrams that will exist. + // `check` never writes, so it does not care either way. + const diagramsAvailable = command === "check" || canRenderDiagrams(repoRoot); + + if (command === "build" && !diagramsAvailable) { + process.stderr.write( + `${yellow("warning")} d2 is unavailable, so the bundle is being written without rendered diagrams.\n Install it with \`mise install\` to include them.\n`, + ); + } + + const bundle = await buildBundle({ + repoRoot, + includeDiagrams: diagramsAvailable, + }); + const { errors } = reportDiagnostics(bundle.diagnostics); + summarise(bundle); + + if (command === "check") { + // `check` builds the whole bundle and discards the files. The build is + // what surfaces the diagnostics — an unannotated file, an undeclared + // ancestor, a violated rule — and because the bundle is never stored, there is no + // committed copy that could be out of date with the source. + return errors > 0 ? 1 : 0; + } + + if (errors > 0) { + process.stderr.write( + `\n${red("refusing to write")} the bundle while ${errors} annotation error${errors === 1 ? "" : "s"} remain\n`, + ); + return 1; + } + + await writeBundle(bundle); + process.stdout.write(`Wrote ${config.outputDirectory}\n`); + return 0; +}; + +process.exitCode = await main(); diff --git a/libs/@local/petrinaut-arch-docs/src/content.ts b/libs/@local/petrinaut-arch-docs/src/content.ts new file mode 100644 index 00000000000..1dcc51a51a9 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/content.ts @@ -0,0 +1,215 @@ +/** + * Hand-written MDX that ships in the same bundle as the generated pages. + * + * Authored pages carry the reasoning an import graph cannot supply. They end up + * in the same manifest as the generated pages, so a host renders one coherent + * set of docs. Slugs mirror the directory layout under `content/`. + */ + +import { readdir, readFile } from "node:fs/promises"; +import { join, relative } from "node:path"; + +import { parseFrontmatter } from "./frontmatter"; +import { toPosix } from "./paths"; + +import type { Dirent } from "node:fs"; + +export interface AuthoredPage { + /** Path within the bundle. */ + path: string; + slug: string; + title: string; + description: string; + contents: string; + order: number; + /** + * Layer id this page explains, from the `attachTo` frontmatter key. + * + * When set, the page is placed beneath that layer's page rather than at the + * top level, so a hand-written guide sits with the generated reference for the + * same code instead of in a separate section. Null means a standalone page. + * + * Note this is *not* a layer declaration — it attaches to a layer declared in + * the source. Declaring layers from `content/` stays forbidden. + */ + attachTo: string | null; + /** Repo-relative source file, for error messages. */ + sourceFile: string; +} + +/** A diagram component shipped in the bundle for authored pages to import. */ +export interface AuthoredComponent { + /** Path within the bundle, e.g. `components/lanes.tsx`. */ + path: string; + /** Import name authors use, e.g. `lanes` for `@diagrams/lanes`. */ + name: string; + contents: string; +} + +export interface AuthoredContentResult { + pages: AuthoredPage[]; + components: AuthoredComponent[]; + errors: { file: string; message: string }[]; +} + +const authoredExtensions = new Set([".md", ".mdx"]); +const componentExtensions = new Set([".tsx", ".ts", ".css"]); + +/** Directory under `content/` holding importable diagram components. */ +const componentDirectory = "components"; + +const extensionOf = (name: string): string => { + const dot = name.lastIndexOf("."); + return dot === -1 ? "" : name.slice(dot).toLowerCase(); +}; + +/** + * Reads `title`, `description` and `sidebar_order` out of frontmatter without + * requiring the architecture-declaration shape, which authored pages do not use. + */ +const readPageMeta = ( + markdown: string, +): { + title: string | null; + description: string; + order: number; + attachTo: string | null; +} => { + const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(markdown); + + if (!match) { + return { title: null, description: "", order: 100, attachTo: null }; + } + + const fields = match[1] ?? ""; + const read = (key: string): string | null => { + const found = new RegExp(`^${key}\\s*:\\s*(.+)$`, "mu").exec(fields); + if (!found) { + return null; + } + return (found[1] ?? "").trim().replace(/^["']|["']$/gu, ""); + }; + + const rawOrder = read("sidebar_order"); + const parsedOrder = rawOrder === null ? Number.NaN : Number(rawOrder); + + return { + title: read("title"), + description: read("description") ?? "", + order: Number.isFinite(parsedOrder) ? parsedOrder : 100, + attachTo: read("attachTo"), + }; +}; + +/** Derives a heading-based title when frontmatter omits one. */ +const firstHeading = (markdown: string): string | null => { + const withoutFrontmatter = markdown.replace( + /^---\r?\n[\s\S]*?\r?\n---\r?\n?/u, + "", + ); + const match = /^#\s+(.+)$/mu.exec(withoutFrontmatter); + return match ? (match[1] ?? "").trim() : null; +}; + +const titleFromSlug = (slug: string): string => { + const last = slug.split("/").pop() ?? slug; + return last + .split("-") + .map((word) => + word === "" ? word : word[0]?.toUpperCase() + word.slice(1), + ) + .join(" "); +}; + +export const collectAuthoredContent = async (options: { + repoRoot: string; + contentDirectory: string; +}): Promise => { + const root = join(options.repoRoot, options.contentDirectory); + const pages: AuthoredPage[] = []; + const components: AuthoredComponent[] = []; + const errors: { file: string; message: string }[] = []; + + let entries: Dirent[]; + try { + entries = await readdir(root, { withFileTypes: true, recursive: true }); + } catch { + // No content directory yet is a normal state, not an error. + return { pages, components, errors }; + } + + for (const entry of entries) { + if (!entry.isFile()) { + continue; + } + + const componentPath = toPosix( + relative(root, join(entry.parentPath, entry.name)), + ); + + if (componentPath.startsWith(`${componentDirectory}/`)) { + if (componentExtensions.has(extensionOf(entry.name))) { + components.push({ + path: componentPath, + name: componentPath + .slice(componentDirectory.length + 1) + .replace(/\.[^.]+$/u, ""), + contents: await readFile(join(entry.parentPath, entry.name), "utf8"), + }); + } + continue; + } + + if (!authoredExtensions.has(extensionOf(entry.name))) { + continue; + } + + const absolutePath = join(entry.parentPath, entry.name); + const relativePath = toPosix(relative(root, absolutePath)); + const contents = await readFile(absolutePath, "utf8"); + + const { declaration, errors: frontmatterErrors } = + parseFrontmatter(contents); + + if (declaration !== null) { + errors.push({ + file: toPosix(relative(options.repoRoot, absolutePath)), + message: + "authored pages must not declare a layer; layer declarations belong in the source packages", + }); + } + + // Only surface hard YAML failures — authored pages legitimately carry + // frontmatter that is not an architecture declaration. + for (const message of frontmatterErrors) { + if (message.startsWith("invalid YAML")) { + errors.push({ + file: toPosix(relative(options.repoRoot, absolutePath)), + message, + }); + } + } + + const slug = relativePath.replace(/\.(?:md|mdx)$/iu, ""); + const meta = readPageMeta(contents); + + pages.push({ + path: `pages/${slug}.mdx`, + slug, + title: meta.title ?? firstHeading(contents) ?? titleFromSlug(slug), + description: meta.description, + contents, + order: meta.order, + attachTo: meta.attachTo, + sourceFile: toPosix(relative(options.repoRoot, absolutePath)), + }); + } + + return { + pages: pages.sort((left, right) => left.slug.localeCompare(right.slug)), + components: components.sort((left, right) => + left.path.localeCompare(right.path), + ), + errors, + }; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/emit/bundle-outputs.ts b/libs/@local/petrinaut-arch-docs/src/emit/bundle-outputs.ts new file mode 100644 index 00000000000..1d0b10dcd43 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/emit/bundle-outputs.ts @@ -0,0 +1,139 @@ +/** + * The bundle's index files: `manifest.json` and `architecture.md`. + * + * `manifest.json` is what a host site reads to build navigation without having + * to crawl the pages directory. `architecture.md` is the whole architecture as a + * single file, which is cheaper for a model to read than fetching thirty pages. + */ + +import { ARCHITECTURE_MODEL_VERSION, type ArchitectureModel } from "../model"; + +import type { AuthoredPage } from "../content"; +import type { GeneratedPage } from "./mdx"; + +export const BUNDLE_MANIFEST_VERSION = 1; + +export interface ManifestPage { + /** Path within the bundle. */ + path: string; + slug: string; + title: string; + description: string; + /** `generated` pages are rewritten on every build; `authored` are hand-written. */ + kind: "generated" | "authored"; + order: number; +} + +export interface BundleManifest { + manifestVersion: number; + modelVersion: number; + generator: string; + /** Relative path to the machine-readable model. */ + model: string; + pages: ManifestPage[]; +} + +export const buildManifest = (options: { + generator: string; + generated: GeneratedPage[]; + authored: AuthoredPage[]; +}): BundleManifest => { + const pages: ManifestPage[] = [ + ...options.generated.map((page) => ({ + path: page.path, + slug: page.slug, + title: page.title, + description: page.description, + kind: "generated" as const, + order: page.order, + })), + ...options.authored.map((page) => ({ + path: page.path, + slug: page.slug, + title: page.title, + description: page.description, + kind: "authored" as const, + order: page.order, + })), + ].sort( + (left, right) => + left.order - right.order || left.slug.localeCompare(right.slug), + ); + + return { + manifestVersion: BUNDLE_MANIFEST_VERSION, + modelVersion: ARCHITECTURE_MODEL_VERSION, + generator: options.generator, + model: "architecture.json", + pages, + }; +}; + +/** + * The whole architecture as one Markdown file. + * + * Ordered so a reader (human or model) meets the system top-down: what the + * packages are, what the rules are, then each layer with its role and + * dependencies. Deliberately terse — this is a reference, and + * every token spent on prose here is one an agent pays on every read. + */ +export const buildSingleFileArchitecture = ( + model: ArchitectureModel, +): string => { + const lines: string[] = [ + "# Petrinaut architecture", + "", + "Generated from annotations in the source. Do not edit — change the `@layerRoot`/`@role` annotations or the declaring README frontmatter instead.", + "", + "## Packages", + "", + ]; + + for (const pkg of model.packages) { + lines.push(`- \`${pkg.name}\` (\`${pkg.path}\`) — ${pkg.description}`); + } + + if (model.rules.length > 0) { + lines.push("", "## Enforced rules", ""); + for (const rule of model.rules) { + lines.push( + `- \`${rule.from}\` must not depend on \`${rule.to}\` — ${rule.reason}`, + ); + } + } + + lines.push("", "## Layers", ""); + + for (const layer of model.layers) { + lines.push( + `### ${layer.name} (\`${layer.id}\`)`, + "", + layer.role, + "", + `- Package: \`${layer.package}\``, + `- Declared in: \`${layer.declaredIn}\``, + `- Size: ${layer.fileCount} files, ${layer.lineCount} lines`, + ); + + const outgoing = model.edges.filter((edge) => edge.from === layer.id); + + if (outgoing.length > 0) { + const rendered = outgoing + .slice() + .sort((left, right) => right.fileDependencies - left.fileDependencies) + .map((edge) => `\`${edge.to}\` (${edge.fileDependencies})`) + .join(", "); + lines.push(`- Depends on: ${rendered}`); + } + + if (layer.references.length > 0) { + lines.push( + `- Further reading: ${layer.references.map((reference) => `\`${reference}\``).join(", ")}`, + ); + } + + lines.push(""); + } + + return `${lines.join("\n")}\n`; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/emit/d2.ts b/libs/@local/petrinaut-arch-docs/src/emit/d2.ts new file mode 100644 index 00000000000..d00e962616e --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/emit/d2.ts @@ -0,0 +1,421 @@ +/** + * Diagram generation. + * + * Dotted layer ids map straight onto D2's container syntax, so + * `core.simulation.monte-carlo` nests inside `core.simulation` inside `core` + * with no extra bookkeeping — the taxonomy the annotations declare *is* the + * diagram's grouping. + * + * Three kinds of diagram, each bounding its node count a different way: + * + * - the overview is a level cut at depth 1 — the top-level layers only; + * - a layer with sub-layers gets a drill-down of its direct children; + * - every layer, leaf or not, gets a neighbourhood showing what it depends on + * and what depends on it. + * + * The neighbourhood is the one a reader usually wants: landing on a layer page, + * the question is what this layer touches, not what sits inside it. Leaves have + * nothing inside them and are exactly where readers land. + * + * Both `.d2` source and rendered `.svg` are written. + */ + +import { spawnSync } from "node:child_process"; + +import type { Edge, Layer } from "../model"; + +const quote = (text: string): string => JSON.stringify(text); + +/** Top-level ancestor of a dotted id, used for colour classing. */ +const rootSegment = (id: string): string => id.split(".")[0] ?? id; + +const styleClasses = [ + `classes: {`, + ` core: {style.fill: "#dcecff"; style.stroke: "#3676b8"}`, + ` react: {style.fill: "#e8e0ff"; style.stroke: "#7051b5"}`, + ` ui: {style.fill: "#e2f4e8"; style.stroke: "#3d8055"}`, + ` petrinaut: {style.fill: "#fdeedc"; style.stroke: "#b5762f"}`, + ` other: {style.fill: "#f2f2f2"; style.stroke: "#777777"}`, + ` boundary: {style.stroke-dash: 4}`, + ` focus: {style.stroke-width: 3; style.bold: true}`, + ` elided: {style.fill: "#ffffff"; style.stroke: "#aaaaaa"; style.stroke-dash: 3; style.italic: true}`, + `}`, +].join("\n"); + +const knownRoots = ["core", "react", "ui", "petrinaut"]; + +const classFor = (id: string): string => { + const root = rootSegment(id); + return knownRoots.includes(root) ? root : "other"; +}; + +const pluralise = (count: number, singular: string, plural?: string): string => + `${count} ${count === 1 ? singular : (plural ?? `${singular}s`)}`; + +const header = (generatedBy: string): string => + `# Generated by ${generatedBy}. Edit the annotations in the source, not this file.\n\ndirection: right\n`; + +/** + * D2 reads `.` as container nesting, so a dotted id used as a key would make it + * synthesise ancestor boxes. Flattening to `_` keeps every node a peer, which is + * what a neighbourhood diagram wants — the focus and its neighbours sit + * alongside each other regardless of where they live in the taxonomy. + */ +const flatKey = (id: string): string => id.replace(/\./gu, "_"); + +/** + * How many neighbours a neighbourhood diagram draws before eliding the rest. + * + * Bounded because a widely-depended-on layer (`core.types`) would otherwise + * produce a diagram with every consumer in the system on it, which is accurate + * and unreadable. The elided count is drawn as its own node rather than dropped, + * so the diagram never implies a layer has fewer neighbours than it does. + */ +const maxNeighbours = 12; + +/** + * One diagram per layer, showing what it depends on and what depends on it. + * + * Only edges *incident to the focus* are drawn. Edges among the neighbours + * themselves are real but belong to those layers' own pages; including them + * turns a readable star into the same tangle the overview already avoids. + */ +export const buildNeighbourhoodDiagram = ( + focusId: string, + layers: Layer[], + edges: Edge[], + generatedBy: string, +): string => { + const layersById = new Map(layers.map((layer) => [layer.id, layer])); + const focus = layersById.get(focusId); + + /** Neighbour id → dependencies in each direction. */ + const neighbours = new Map(); + + const record = ( + otherId: string, + direction: "out" | "in", + count: number, + ): void => { + const entry = neighbours.get(otherId) ?? { out: 0, in: 0 }; + entry[direction] += count; + neighbours.set(otherId, entry); + }; + + for (const edge of edges) { + if (edge.from === focusId && edge.to !== focusId) { + record(edge.to, "out", edge.fileDependencies); + } else if (edge.to === focusId && edge.from !== focusId) { + record(edge.from, "in", edge.fileDependencies); + } + } + + const ranked = [...neighbours.entries()].sort( + ([leftId, left], [rightId, right]) => + right.out + right.in - (left.out + left.in) || + leftId.localeCompare(rightId), + ); + + const shown = ranked.slice(0, maxNeighbours); + const elided = ranked.slice(maxNeighbours); + + const nodeLines = [ + `${flatKey(focusId)}: {class: [${classFor(focusId)}; focus]; label: ${quote( + focus?.name ?? focusId, + )}; tooltip: ${quote(focus?.role ?? focusId)}}`, + ...shown.map(([id]) => { + const layer = layersById.get(id); + return `${flatKey(id)}: {class: ${classFor(id)}; label: ${quote( + layer?.name ?? id, + )}; tooltip: ${quote(layer?.role ?? id)}}`; + }), + ]; + + const elidedOut = elided.reduce((sum, [, counts]) => sum + counts.out, 0); + const elidedIn = elided.reduce((sum, [, counts]) => sum + counts.in, 0); + + if (elided.length > 0) { + const total = elidedOut + elidedIn; + nodeLines.push( + `elided: {class: elided; label: ${quote( + `+${pluralise(elided.length, "further layer")}`, + )}; tooltip: ${quote( + `${pluralise(total, "file-level dependency", "file-level dependencies")}, omitted to keep the diagram readable — the full list is in the tables below`, + )}}`, + ); + } + + const edgeLines: string[] = []; + + for (const [id, counts] of shown) { + if (counts.out > 0) { + edgeLines.push( + `${flatKey(focusId)} -> ${flatKey(id)}: {tooltip: ${quote( + pluralise( + counts.out, + "file-level dependency", + "file-level dependencies", + ), + )}}`, + ); + } + if (counts.in > 0) { + edgeLines.push( + `${flatKey(id)} -> ${flatKey(focusId)}: {tooltip: ${quote( + pluralise( + counts.in, + "file-level dependency", + "file-level dependencies", + ), + )}}`, + ); + } + } + + // Drawn dashed and in whichever directions the elided layers actually use, so + // the summary node reads as part of the graph rather than as a stray legend. + if (elidedOut > 0) { + edgeLines.push( + `${flatKey(focusId)} -> elided: {class: boundary; tooltip: ${quote( + pluralise( + elidedOut, + "file-level dependency", + "file-level dependencies", + ), + )}}`, + ); + } + if (elidedIn > 0) { + edgeLines.push( + `elided -> ${flatKey(focusId)}: {class: boundary; tooltip: ${quote( + pluralise(elidedIn, "file-level dependency", "file-level dependencies"), + )}}`, + ); + } + + if (nodeLines.length === 1) { + nodeLines.push( + `isolated: {class: elided; label: ${quote("no dependencies either way")}}`, + ); + } + + return [ + header(generatedBy), + `# what ${focusId} depends on, and what depends on it\n`, + nodeLines.join("\n"), + "\n# dependencies\n", + edgeLines.join("\n"), + "\n# styling\n", + styleClasses, + "", + ].join("\n"); +}; + +/** + * One diagram per layer that has sub-layers, showing only its *direct* children. + * + * Nesting the entire sub-tree produced an unreadable tangle — 23 layers under + * `core` with every inter-layer edge drawn is technically accurate and useless. + * Showing one level at a time, with edges aggregated up to that level, keeps each + * diagram legible and lets a reader drill down through the pages instead. + */ +export const buildSubtreeDiagram = ( + parentId: string, + layers: Layer[], + edges: Edge[], + generatedBy: string, +): string => { + const children = layers.filter((layer) => layer.parent === parentId); + const childIds = children.map((layer) => layer.id); + + /** + * D2 reads `.` as container nesting, so a full dotted id as a key would make + * it synthesise the ancestor boxes this diagram exists to leave out. The leaf + * segment is unique among siblings, which is all a single-level diagram needs. + */ + const key = (layerId: string): string => layerId.slice(parentId.length + 1); + + /** The direct child of `parentId` that `layerId` sits under, if any. */ + const childScopeOf = (layerId: string): string | null => + childIds.find( + (childId) => layerId === childId || layerId.startsWith(`${childId}.`), + ) ?? null; + + /** Total files in a child, including its own descendants. */ + const totalFiles = (childId: string): number => + layers + .filter( + (layer) => layer.id === childId || layer.id.startsWith(`${childId}.`), + ) + .reduce((total, layer) => total + layer.fileCount, 0); + + const nodeLines = children.map((child) => { + const descendants = layers.filter((layer) => + layer.id.startsWith(`${child.id}.`), + ).length; + const detail = + descendants === 0 + ? pluralise(totalFiles(child.id), "source file") + : `${pluralise(totalFiles(child.id), "source file")}, ${pluralise(descendants, "sub-layer")}`; + + return `${key(child.id)}: {class: ${classFor(child.id)}; label: ${quote(child.name)}; tooltip: ${quote(`${detail} · ${child.role}`)}}`; + }); + + const aggregated = new Map(); + for (const edge of edges) { + const from = childScopeOf(edge.from); + const to = childScopeOf(edge.to); + if (from === null || to === null || from === to) { + continue; + } + const pairKey = `${from}\u0000${to}`; + aggregated.set( + pairKey, + (aggregated.get(pairKey) ?? 0) + edge.fileDependencies, + ); + } + + const edgeLines = [...aggregated.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([edgeKey, count]) => { + const [from = "", to = ""] = edgeKey.split("\u0000"); + return `${key(from)} -> ${key(to)}: {tooltip: ${quote( + pluralise(count, "file-level dependency", "file-level dependencies"), + )}}`; + }); + + return [ + header(generatedBy), + `# direct sub-layers of ${parentId}\n`, + nodeLines.join("\n"), + "\n# dependencies\n", + edgeLines.join("\n"), + "\n# styling\n", + styleClasses, + "", + ].join("\n"); +}; + +/** The collapsed graph: top-level layers only, edges aggregated. */ +export const buildOverviewDiagram = ( + layers: Layer[], + edges: Edge[], + generatedBy: string, +): string => { + interface Group { + name: string; + fileCount: number; + roles: string[]; + } + + const groups = new Map(); + + for (const layer of layers) { + const root = rootSegment(layer.id); + const group = groups.get(root) ?? { name: root, fileCount: 0, roles: [] }; + group.fileCount += layer.fileCount; + if (layer.id === root) { + group.name = layer.name; + group.roles.unshift(layer.role); + } + groups.set(root, group); + } + + const aggregated = new Map(); + for (const edge of edges) { + const from = rootSegment(edge.from); + const to = rootSegment(edge.to); + if (from === to) { + continue; + } + const key = `${from} ${to}`; + aggregated.set(key, (aggregated.get(key) ?? 0) + edge.fileDependencies); + } + + const nodeLines = [...groups.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map( + ([id, group]) => + `${id}: {class: ${classFor(id)}; label: ${quote(group.name)}; tooltip: ${quote( + `${pluralise(group.fileCount, "source file")}${group.roles[0] ? ` · ${group.roles[0]}` : ""}`, + )}}`, + ); + + const edgeLines = [...aggregated.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, count]) => { + const [from = "", to = ""] = key.split(" "); + return `${from} -> ${to}: {tooltip: ${quote( + pluralise(count, "file-level dependency", "file-level dependencies"), + )}}`; + }); + + return [ + header(generatedBy), + "# top-level layers\n", + nodeLines.join("\n"), + "\n# dependencies\n", + edgeLines.join("\n"), + "\n# styling\n", + styleClasses, + "", + ].join("\n"); +}; + +/** + * Whether the `d2` renderer is available. + * + * Checked before pages are emitted so the bundle never references an SVG it + * did not produce. `d2` is a declared repo tool, but environments that install + * tools individually (Vercel) can legitimately lack it. + */ +export const canRenderDiagrams = (repoRoot: string): boolean => { + const result = spawnSync( + "mise", + ["exec", "--env", "dev", "--", "d2", "--version"], + { cwd: repoRoot, encoding: "utf8" }, + ); + return !result.error && result.status === 0; +}; +/** + * Renders a `.d2` file to SVG. + * + * `d2` is provided by mise, matching how the previous script invoked it. When it + * is unavailable the caller is told rather than the build failing outright: the + * `.d2` sources are the diffable artefact, and a missing renderer should not + * block regenerating the model and pages. + */ +export const renderD2 = ( + repoRoot: string, + sourcePath: string, + outputPath: string, +): { ok: true } | { ok: false; error: string } => { + const result = spawnSync( + "mise", + [ + "exec", + "--env", + "dev", + "--", + "d2", + "--layout", + "elk", + sourcePath, + outputPath, + ], + { cwd: repoRoot, encoding: "utf8" }, + ); + + if (result.error) { + return { ok: false, error: result.error.message }; + } + + if (result.status !== 0) { + return { + ok: false, + error: result.stderr || result.stdout || "d2 exited non-zero", + }; + } + + return { ok: true }; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/emit/mdx.test.ts b/libs/@local/petrinaut-arch-docs/src/emit/mdx.test.ts new file mode 100644 index 00000000000..b88397cc082 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/emit/mdx.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest"; + +import { resolveAuthoredLinks, layerSlug } from "./mdx"; + +/** + * An authored page's final slug depends on its `attachTo`, so it cannot write a + * correct relative link by hand. These tests pin the resolution that replaces + * hand-written paths, including the failure case — an unresolved target must be + * reported, never emitted as-is and left to 404. + */ + +const layerSlugs = new Map([ + ["core", layerSlug("core")], + ["core.simulation", layerSlug("core.simulation")], + ["core.simulation.engine", layerSlug("core.simulation.engine")], +]); + +const docSlugs = new Map([ + ["index", "index"], + ["two-execution-paths", "two-execution-paths"], + ["simulation/memory-model", "architecture/core/simulation/memory-model"], +]); + +const resolve = (contents: string, fromSlug: string) => + resolveAuthoredLinks(contents, fromSlug, { layerSlugs, docSlugs }); + +describe("resolveAuthoredLinks", () => { + it("resolves a layer link from a top-level page", () => { + const { contents, unresolved } = resolve( + "See [the engine](layer:core.simulation.engine).", + "two-execution-paths", + ); + + expect(unresolved).toEqual([]); + expect(contents).toBe( + "See [the engine](architecture/core/simulation/engine).", + ); + }); + + it("resolves a layer link from a page attached deep in the tree", () => { + const { contents } = resolve( + "See [the engine](layer:core.simulation.engine).", + "architecture/core/simulation/worker/protocol", + ); + + // Same target, different depth — the path has to differ. + expect(contents).toBe("See [the engine](../engine)."); + }); + + it("resolves a doc link between attached pages", () => { + const { contents, unresolved } = resolve( + "See [memory](doc:simulation/memory-model).", + "architecture/core/simulation/worker/protocol", + ); + + expect(unresolved).toEqual([]); + expect(contents).toBe("See [memory](../memory-model)."); + }); + + it("resolves a doc link that climbs out of the architecture tree", () => { + const { contents } = resolve( + "See [paths](doc:two-execution-paths).", + "architecture/core/simulation/memory-model", + ); + + expect(contents).toBe("See [paths](../../../two-execution-paths)."); + }); + + it("preserves a fragment", () => { + const { contents } = resolve( + "See [engine](layer:core.simulation.engine#invariants).", + "two-execution-paths", + ); + + expect(contents).toBe( + "See [engine](architecture/core/simulation/engine#invariants).", + ); + }); + + it("reports an unknown layer instead of emitting a broken link", () => { + const { contents, unresolved } = resolve( + "See [nope](layer:core.nonexistent).", + "index", + ); + + expect(unresolved).toEqual(["layer:core.nonexistent"]); + // Left untouched so the diagnostic is the only signal, not a silent rewrite. + expect(contents).toBe("See [nope](layer:core.nonexistent)."); + }); + + it("reports an unknown doc", () => { + const { unresolved } = resolve("[gone](doc:missing-page).", "index"); + + expect(unresolved).toEqual(["doc:missing-page"]); + }); + + it("leaves ordinary links alone", () => { + const source = + "[external](https://example.com) [relative](../sibling) [anchor](#section)"; + const { contents, unresolved } = resolve(source, "index"); + + expect(contents).toBe(source); + expect(unresolved).toEqual([]); + }); + + it("does not rewrite a scheme-like string outside a link target", () => { + const source = "Run `yarn doc:architecture` to regenerate."; + const { contents, unresolved } = resolve(source, "index"); + + expect(contents).toBe(source); + expect(unresolved).toEqual([]); + }); + + it("resolves every link in a page, not just the first", () => { + const { contents, unresolved } = resolve( + "[a](layer:core) then [b](layer:core.simulation)", + "index", + ); + + expect(unresolved).toEqual([]); + expect(contents).toBe( + "[a](architecture/core) then [b](architecture/core/simulation)", + ); + }); +}); diff --git a/libs/@local/petrinaut-arch-docs/src/emit/mdx.ts b/libs/@local/petrinaut-arch-docs/src/emit/mdx.ts new file mode 100644 index 00000000000..70091670257 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/emit/mdx.ts @@ -0,0 +1,519 @@ +/** + * MDX page generation. + * + * Output here is deliberately **framework-neutral**: YAML frontmatter plus plain + * CommonMark. No JSX, no imports, no framework-specific components. That + * constraint is what lets the same bundle render in the Starlight site, in + * hash.dev's Next.js MDX pipeline, and as plain text for an AI agent — a single + * `` component here would break two of those three. + * + * Diagrams are referenced as relative image paths, and every layer page links + * back to the annotation that declared it so a reader can go straight from the + * rendered claim to the source of truth. + */ + +import { posix } from "node:path"; + +import type { ArchitectureModel, Edge, Layer } from "../model"; + +export interface GeneratedPage { + /** Path within the bundle, e.g. `pages/core.simulation.mdx`. */ + path: string; + /** Route-ish identifier a host can map onto its own URL space. */ + slug: string; + title: string; + description: string; + contents: string; + order: number; +} + +/** + * Sidebar order at which generated pages begin. + * + * Authored pages are the narrative entry to the docs and generated pages are + * reference, so the two sets are kept in separate bands rather than interleaved + * by number. An authored page can still sort itself after the reference section + * by choosing a `sidebar_order` above this. + */ +export const GENERATED_ORDER_BASE = 1000; + +const escapeTableCell = (text: string): string => + text.replace(/\|/gu, "\\|").replace(/\n/gu, " "); + +/** Serialises frontmatter by hand so the output stays byte-stable. */ +const frontmatter = (fields: Record): string => { + const lines = Object.entries(fields).map( + ([key, value]) => + `${key}: ${typeof value === "number" ? value : JSON.stringify(value)}`, + ); + return ["---", ...lines, "---", ""].join("\n"); +}; + +const sourceLink = ( + sourceUrlPrefix: string, + file: string, + line?: number, +): string => + `${sourceUrlPrefix}${file}${line !== undefined && line > 1 ? `#L${line}` : ""}`; + +/** The slug a layer's generated page occupies. */ +export const layerSlug = (id: string): string => + `architecture/${id.replace(/\./gu, "/")}`; + +/** + * Rewrites relative links in embedded README prose to absolute source URLs. + * + * A README's `[engine](./engine/README.md)` is correct where the README lives + * and broken once the prose is embedded in a docs page served from somewhere + * else entirely. Resolving against the README's own directory keeps those links + * working wherever the bundle is mounted. + */ +export const rewriteRelativeLinks = ( + prose: string, + declaredIn: string, + sourceUrlPrefix: string, +): string => { + const baseDirectory = posix.dirname(declaredIn); + + return prose.replace( + /(!?\[[^\]]*\])\(([^)\s]+)(\s+"[^"]*")?\)/gu, + (match, label: string, target: string, title: string | undefined) => { + if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/iu.test(target)) { + return match; + } + + const [path = "", fragment] = target.split("#"); + + if (path === "") { + return match; + } + + const resolved = posix.normalize(posix.join(baseDirectory, path)); + + // A link that escapes the repository root cannot be made absolute. + if (resolved.startsWith("..")) { + return match; + } + + return `${label}(${sourceUrlPrefix}${resolved}${fragment === undefined ? "" : `#${fragment}`}${title ?? ""})`; + }, + ); +}; + +/** + * Relative link from one bundle page to another, so the bundle works when + * mounted at any base path. + * + * Resolved against the *slug* — page links are followed in URL space by a + * reader, and assume slugs map to URLs without a trailing slash. + */ +const relativeTo = (fromSlug: string, toSlug: string): string => { + const relative = posix.relative(posix.dirname(fromSlug), toSlug); + return relative === "" ? "." : relative; +}; + +/** + * Relative path from a page's *file* to an asset in the bundle. + * + * Deliberately different from `relativeTo`: MDX toolchains resolve image paths + * against the file on disk at build time, not against the page's URL. Using the + * slug-relative form here produced `diagrams/x.svg` from `pages/architecture.mdx`, + * which points at a `pages/diagrams/` directory that does not exist. + */ +const assetPathFrom = (slug: string, assetPath: string): string => + posix.relative(posix.dirname(`pages/${slug}`), assetPath); + +/** + * Resolves `doc:` and `layer:` link targets in authored pages. + * + * An authored page's final slug depends on its `attachTo`, so it cannot know + * its own depth and therefore cannot write a correct relative link by hand. + * These two schemes let a page name its target and have the path computed: + * + * - `[text](layer:core.simulation.engine)` → that layer's generated page + * - `[text](doc:two-execution-paths)` → another authored page, by its file slug + * + * Unresolvable targets are reported rather than silently emitted, because a + * broken link here is invisible until someone clicks it. + */ +export const resolveAuthoredLinks = ( + contents: string, + fromSlug: string, + options: { + layerSlugs: Map; + docSlugs: Map; + }, +): { contents: string; unresolved: string[] } => { + const unresolved: string[] = []; + + const resolved = contents.replace( + /(\]\()(layer|doc):([^)\s#]+)(#[^)\s]*)?(\))/gu, + ( + match, + open: string, + scheme: string, + target: string, + fragment: string | undefined, + close: string, + ) => { + const slug = + scheme === "layer" + ? options.layerSlugs.get(target) + : options.docSlugs.get(target); + + if (slug === undefined) { + unresolved.push(`${scheme}:${target}`); + return match; + } + + return `${open}${relativeTo(fromSlug, slug)}${fragment ?? ""}${close}`; + }, + ); + + return { contents: resolved, unresolved }; +}; + +/** + * Rewrites `@diagrams/x` import specifiers to a path relative to the page. + * + * Same problem as `layer:`/`doc:` links: a page's depth depends on its + * `attachTo`, so it cannot write a correct relative import by hand. Authors use + * a stable alias and the real path is computed at emit time. + */ +export const resolveComponentImports = ( + contents: string, + fromSlug: string, + available: Set, +): { contents: string; unresolved: string[] } => { + const unresolved: string[] = []; + const fromDirectory = posix.dirname(`pages/${fromSlug}`); + + const resolved = contents.replace( + /(["'])@diagrams\/([^"']+)\1/gu, + (match, quote: string, name: string) => { + if (!available.has(name)) { + unresolved.push(`@diagrams/${name}`); + return match; + } + + const target = posix.relative(fromDirectory, `components/${name}`); + return `${quote}${target.startsWith(".") ? target : `./${target}`}${quote}`; + }, + ); + + return { contents: resolved, unresolved }; +}; + +const describeEdges = ( + layer: Layer, + edges: Edge[], + layersById: Map, + slug: string, +): string[] => { + const outgoing = edges.filter((edge) => edge.from === layer.id); + const incoming = edges.filter((edge) => edge.to === layer.id); + + const sections: string[] = []; + + const table = ( + heading: string, + intro: string, + rows: Edge[], + direction: "to" | "from", + ): string[] => { + if (rows.length === 0) { + return []; + } + + return [ + `## ${heading}`, + "", + intro, + "", + "| Layer | Imports | Package boundary |", + "| --- | --- | --- |", + ...rows + .slice() + .sort((left, right) => right.fileDependencies - left.fileDependencies) + .map((edge) => { + const otherId = direction === "to" ? edge.to : edge.from; + const other = layersById.get(otherId); + const label = other ? other.name : otherId; + const link = `[${escapeTableCell(label)}](${relativeTo(slug, layerSlug(otherId))})`; + const crosses = edge.crossesPackage ? "crossed" : "—"; + return `| ${link} | ${edge.fileDependencies} | ${crosses} |`; + }), + "", + ]; + }; + + sections.push( + ...table( + "Depends on", + "Aggregated from real TypeScript imports.", + outgoing, + "to", + ), + ); + sections.push( + ...table( + "Depended on by", + "Who reaches into this layer.", + incoming, + "from", + ), + ); + + return sections; +}; + +const buildLayerPage = ( + layer: Layer, + model: ArchitectureModel, + layersById: Map, + sourceUrlPrefix: string, + order: number, + neighbourhoodDiagram: string | null, + subtreeDiagram: string | null, + attachedGuides: { slug: string; title: string; description: string }[], +): GeneratedPage => { + const slug = layerSlug(layer.id); + const children = model.layers.filter((other) => other.parent === layer.id); + + const body: string[] = []; + + body.push(`> ${layer.role}`, ""); + + body.push( + [ + `**Package** \`${layer.package}\``, + `**Layer id** \`${layer.id}\``, + `**Files** ${layer.fileCount}`, + `**Lines** ${layer.lineCount.toLocaleString("en-US")}`, + ].join(" · "), + "", + ); + + body.push( + `Declared in [\`${layer.declaredIn}\`](${sourceLink(sourceUrlPrefix, layer.declaredIn)}).`, + "", + ); + + if (neighbourhoodDiagram !== null) { + body.push( + `![What ${layer.name} depends on, and what depends on it](${assetPathFrom( + slug, + `diagrams/${neighbourhoodDiagram}.svg`, + )})`, + "", + ); + } + + if (subtreeDiagram !== null) { + body.push( + `![Layers within ${layer.name}](${assetPathFrom( + slug, + `diagrams/${subtreeDiagram}.svg`, + )})`, + "", + ); + } + + if (children.length > 0) { + body.push( + "## Sub-layers", + "", + ...children.map( + (child) => + `- [${child.name}](${relativeTo(slug, layerSlug(child.id))}) — ${child.role}`, + ), + "", + ); + } + + if (attachedGuides.length > 0) { + body.push( + "## Guides", + "", + "Hand-written explanations of this layer. Unlike the rest of this page, they are not generated and not checked against the code.", + "", + ...attachedGuides.map( + (guide) => + `- [${guide.title}](${relativeTo(slug, guide.slug)})${guide.description === "" ? "" : ` — ${guide.description}`}`, + ), + "", + ); + } + + body.push(...describeEdges(layer, model.edges, layersById, slug)); + + if (layer.prose !== null) { + body.push( + "## Notes", + "", + rewriteRelativeLinks(layer.prose, layer.declaredIn, sourceUrlPrefix), + "", + ); + } + + if (layer.references.length > 0) { + body.push( + "## Further reading", + "", + ...layer.references.map( + (reference) => + `- [\`${reference}\`](${sourceLink(sourceUrlPrefix, reference)})`, + ), + "", + ); + } + + if (layer.files.length > 0) { + // A per-file list would dominate the page (the editor layer alone resolves + // over a hundred files) and every consumer that actually wants the list can + // read `files` from architecture.json instead. + const folder = posix.dirname(layer.declaredIn); + body.push( + "## Source", + "", + `${layer.fileCount} file${layer.fileCount === 1 ? "" : "s"} resolve to this layer, rooted at [\`${folder}\`](${sourceUrlPrefix}${folder}) — files under a sub-layer's folder belong to that sub-layer instead. The full list is in \`architecture.json\`.`, + "", + ); + } + + return { + path: `pages/${slug}.mdx`, + slug, + title: layer.name, + description: layer.role, + order, + contents: + frontmatter({ + title: layer.name, + description: layer.role, + sidebar_order: order, + }) + `\n${body.join("\n")}`, + }; +}; + +const buildOverviewPage = ( + model: ArchitectureModel, + overviewDiagram: string | null, +): GeneratedPage => { + const slug = "architecture"; + const roots = model.layers.filter((layer) => layer.parent === null); + + const body: string[] = [ + "> Generated from annotations in the Petrinaut source. Every layer and edge on this page was read out of the code, not drawn by hand.", + "", + ]; + + if (overviewDiagram !== null) { + body.push( + `![Top-level layers and the dependencies between them](${assetPathFrom( + slug, + `diagrams/${overviewDiagram}.svg`, + )})`, + "", + ); + } + + body.push( + "## Top-level layers", + "", + "| Layer | Responsibility | Files |", + "| --- | --- | --- |", + ...roots.map( + (layer) => + `| [${escapeTableCell(layer.name)}](${relativeTo(slug, layerSlug(layer.id))}) | ${escapeTableCell(layer.role)} | ${model.layers + .filter( + (other) => + other.id === layer.id || other.id.startsWith(`${layer.id}.`), + ) + .reduce((total, other) => total + other.fileCount, 0)} |`, + ), + "", + ); + + body.push( + "## Packages", + "", + "| Package | Path | Description |", + "| --- | --- | --- |", + ...model.packages.map( + (pkg) => + `| \`${pkg.name}\` | \`${pkg.path}\` | ${escapeTableCell(pkg.description)} |`, + ), + "", + ); + + if (model.rules.length > 0) { + body.push( + "## Enforced rules", + "", + "These are checked against the real import graph on every CI run.", + "", + "| Rule | Reason |", + "| --- | --- |", + ...model.rules.map( + (rule) => + `| \`${rule.from}\` must not depend on \`${rule.to}\` | ${escapeTableCell(rule.reason)} |`, + ), + "", + ); + } + + return { + path: `pages/${slug}.mdx`, + slug, + title: "Architecture", + description: + "Generated map of the Petrinaut packages: layers and the dependencies between them.", + order: GENERATED_ORDER_BASE, + contents: + frontmatter({ + title: "Architecture", + description: + "Generated map of the Petrinaut packages: layers and the dependencies between them.", + sidebar_order: GENERATED_ORDER_BASE, + }) + `\n${body.join("\n")}`, + }; +}; + +export const buildPages = ( + model: ArchitectureModel, + options: { + sourceUrlPrefix: string; + overviewDiagram: string | null; + /** Layer id → diagram name showing its dependencies and dependents. */ + neighbourhoodDiagrams: Map; + /** Layer id → diagram name showing its direct children, for parents only. */ + subtreeDiagrams: Map; + /** Authored guides attached to each layer id. */ + guidesByLayer?: Map< + string, + { slug: string; title: string; description: string }[] + >; + }, +): GeneratedPage[] => { + const layersById = new Map(model.layers.map((layer) => [layer.id, layer])); + + const pages = [buildOverviewPage(model, options.overviewDiagram)]; + + model.layers.forEach((layer, index) => { + pages.push( + buildLayerPage( + layer, + model, + layersById, + options.sourceUrlPrefix, + GENERATED_ORDER_BASE + index + 1, + options.neighbourhoodDiagrams.get(layer.id) ?? null, + options.subtreeDiagrams.get(layer.id) ?? null, + options.guidesByLayer?.get(layer.id) ?? [], + ), + ); + }); + + return pages; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/extract.test.ts b/libs/@local/petrinaut-arch-docs/src/extract.test.ts new file mode 100644 index 00000000000..15397028cfc --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/extract.test.ts @@ -0,0 +1,216 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { extract } from "./extract"; + +import type { ArchitecturePackage } from "./model"; + +/** + * The extractor walks a real directory tree, so these tests build small trees in + * a temp dir. Inheritance is the part most worth pinning down: it is what makes + * ~40 declarations cover 400 files, and a regression there would silently + * re-bucket files rather than fail loudly. + */ + +let root: string; + +const pkg: ArchitecturePackage = { + name: "@test/pkg", + path: "pkg", + description: "test package", + language: "typescript", + sourceDirectory: "src", +}; + +const write = async (relativePath: string, contents: string): Promise => { + const absolute = join(root, relativePath); + await mkdir(join(absolute, ".."), { recursive: true }); + await writeFile(absolute, contents, "utf8"); +}; + +const run = async () => + extract({ + repoRoot: root, + packages: [pkg], + ignoredDirectories: ["node_modules"], + ignoredFilePattern: /\.test\.ts$/u, + }); + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), "arch-docs-extract-")); +}); + +afterEach(async () => { + await rm(root, { recursive: true, force: true }); +}); + +describe("extract", () => { + it("assigns files to the nearest declaring ancestor", async () => { + await write( + "pkg/src/README.md", + "---\nlayer: core\nrole: Root\n---\n\nRoot prose.\n", + ); + await write( + "pkg/src/engine/README.md", + "---\nlayer: core.engine\nrole: Engine\n---\n", + ); + await write("pkg/src/top.ts", "export const a = 1;\n"); + await write("pkg/src/engine/step.ts", "export const b = 2;\n"); + await write("pkg/src/engine/deep/nested.ts", "export const c = 3;\n"); + + const { layers, fileLayers, diagnostics } = await run(); + + expect(diagnostics).toEqual([]); + expect(fileLayers.get("pkg/src/top.ts")).toBe("core"); + expect(fileLayers.get("pkg/src/engine/step.ts")).toBe("core.engine"); + // Inherits through a folder that declares nothing of its own. + expect(fileLayers.get("pkg/src/engine/deep/nested.ts")).toBe("core.engine"); + + const engine = layers.find((layer) => layer.id === "core.engine"); + expect(engine?.fileCount).toBe(2); + expect(layers.find((layer) => layer.id === "core")?.fileCount).toBe(1); + }); + + it("captures README prose as the layer body", async () => { + await write( + "pkg/src/README.md", + "---\nlayer: core\nrole: Root\n---\n\n# Title\n\nBody text.\n", + ); + await write("pkg/src/a.ts", "export const a = 1;\n"); + + const { layers } = await run(); + + expect(layers[0]?.prose).toBe("# Title\n\nBody text."); + }); + + it("declares a layer from a @layerRoot entry file", async () => { + await write( + "pkg/src/index.ts", + `/**\n * @layerRoot core\n * @role Does the thing\n */\nexport const a = 1;\n`, + ); + + const { layers, diagnostics } = await run(); + + expect(diagnostics).toEqual([]); + expect(layers).toHaveLength(1); + expect(layers[0]?.name).toBe("core"); + expect(layers[0]?.role).toBe("Does the thing"); + expect(layers[0]?.declaredIn).toBe("pkg/src/index.ts"); + }); + + it("requires a role alongside @layerRoot", async () => { + await write( + "pkg/src/index.ts", + `/**\n * @layerRoot core\n */\nexport const a = 1;\n`, + ); + + const { diagnostics } = await run(); + + expect( + diagnostics.some((diagnostic) => diagnostic.message.includes("@role")), + ).toBe(true); + }); + + it("reports a file that no declaration covers", async () => { + await write("pkg/src/orphan.ts", "export const a = 1;\n"); + + const { diagnostics } = await run(); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toContain("no layer resolves"); + }); + + it("rejects two declarations on one folder", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write( + "pkg/src/index.ts", + `/**\n * @layerRoot core.other\n * @role Other\n */\nexport const a = 1;\n`, + ); + + const { diagnostics } = await run(); + + expect( + diagnostics.some((diagnostic) => + diagnostic.message.includes("may declare at most one layer"), + ), + ).toBe(true); + }); + + it("rejects the same layer id declared twice", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write("pkg/src/a/README.md", "---\nlayer: core\nrole: Dup\n---\n"); + + const { diagnostics } = await run(); + + expect( + diagnostics.some((diagnostic) => + diagnostic.message.includes("is already declared in"), + ), + ).toBe(true); + }); + + it("lists non-declaring markdown as references", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write("pkg/src/BUFFER_ABI.md", "# ABI\n\nDetails.\n"); + await write("pkg/src/a.ts", "export const a = 1;\n"); + + const { layers } = await run(); + + expect(layers[0]?.references).toEqual(["pkg/src/BUFFER_ABI.md"]); + }); + + it("excludes files matching the ignore pattern", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write("pkg/src/a.ts", "export const a = 1;\n"); + await write("pkg/src/a.test.ts", "export const b = 2;\n"); + + const { layers, fileLayers } = await run(); + + expect(layers[0]?.fileCount).toBe(1); + expect(fileLayers.has("pkg/src/a.test.ts")).toBe(false); + }); + + it("counts non-blank lines only", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write("pkg/src/a.ts", "const a = 1;\n\n\nconst b = 2;\n"); + + const { layers } = await run(); + + expect(layers[0]?.lineCount).toBe(2); + }); + + it("produces layers sorted by id, so output is stable", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write("pkg/src/z/README.md", "---\nlayer: core.z\nrole: Z\n---\n"); + await write("pkg/src/a/README.md", "---\nlayer: core.a\nrole: A\n---\n"); + await write("pkg/src/a/f.ts", "export const a = 1;\n"); + await write("pkg/src/z/f.ts", "export const a = 1;\n"); + + const { layers } = await run(); + + expect(layers.map((layer) => layer.id)).toEqual([ + "core", + "core.a", + "core.z", + ]); + }); + + it("derives the parent id from the dotted layer id", async () => { + await write("pkg/src/README.md", "---\nlayer: core\nrole: Root\n---\n"); + await write( + "pkg/src/a/b/README.md", + "---\nlayer: core.a.b\nrole: Deep\n---\n", + ); + await write("pkg/src/a/b/f.ts", "export const a = 1;\n"); + + const { layers } = await run(); + + expect(layers.find((layer) => layer.id === "core")?.parent).toBeNull(); + expect(layers.find((layer) => layer.id === "core.a.b")?.parent).toBe( + "core.a", + ); + }); +}); diff --git a/libs/@local/petrinaut-arch-docs/src/extract.ts b/libs/@local/petrinaut-arch-docs/src/extract.ts new file mode 100644 index 00000000000..0d15cb2d4ae --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/extract.ts @@ -0,0 +1,372 @@ +/** + * Walks the configured packages and resolves every source file to a layer. + * + * This is the module that replaces the hand-maintained path→layer switch that + * used to live in `petrinaut-core/scripts/generate-dependency-diagrams.mjs`. + * There, the mapping was ~180 lines of `if (path.startsWith(...))` sitting far + * from the code it described, with a silent fallback that mis-bucketed anything + * moved or renamed. Here the mapping is declared next to the code and resolved + * by inheritance: declare a layer on a folder, and every descendant file + * belongs to it until a deeper declaration says otherwise. + */ + +import { readdir, readFile } from "node:fs/promises"; +import { join, posix, relative } from "node:path"; + +import { parseFrontmatter } from "./frontmatter"; +import { toPosix } from "./paths"; +import { scanTags } from "./tags"; + +import type { Layer, ArchitecturePackage } from "./model"; + +/** A layer declaration plus everything resolved onto it during the walk. */ +interface LayerAccumulator { + id: string; + name: string; + package: string; + role: string; + declaredIn: string; + /** Folder the declaration governs; descendants inherit from it. */ + scope: string; + prose: string | null; + references: string[]; + files: string[]; + lineCount: number; +} + +export interface Diagnostic { + file: string; + line: number | null; + message: string; + severity: "error" | "warning"; +} + +export interface ExtractionResult { + layers: Layer[]; + /** Repo-relative source file → layer id, for the graph stage. */ + fileLayers: Map; + diagnostics: Diagnostic[]; +} + +export interface ExtractOptions { + repoRoot: string; + packages: ArchitecturePackage[]; + /** Directory names skipped entirely during the walk. */ + ignoredDirectories: string[]; + /** Regex matched against repo-relative paths to skip files. */ + ignoredFilePattern: RegExp; +} + +const sourceExtensions = new Set([".ts", ".tsx", ".mts", ".cts"]); + +const extensionOf = (name: string): string => { + const dot = name.lastIndexOf("."); + return dot === -1 ? "" : name.slice(dot); +}; + +const countNonBlankLines = (text: string): number => + text.split("\n").filter((line) => line.trim() !== "").length; + +interface WalkEntry { + /** Repo-relative, posix-separated. */ + path: string; + absolutePath: string; + /** Repo-relative posix directory containing the file. */ + directory: string; + name: string; +} + +const walkFiles = async ( + root: string, + repoRoot: string, + ignoredDirectories: Set, +): Promise => { + const entries: WalkEntry[] = []; + + const visit = async (absoluteDirectory: string): Promise => { + const contents = await readdir(absoluteDirectory, { withFileTypes: true }); + + for (const item of contents) { + const absolutePath = join(absoluteDirectory, item.name); + + if (item.isDirectory()) { + if (!ignoredDirectories.has(item.name)) { + await visit(absolutePath); + } + continue; + } + + if (!item.isFile()) { + continue; + } + + const relativePath = toPosix(relative(repoRoot, absolutePath)); + entries.push({ + path: relativePath, + absolutePath, + directory: posix.dirname(relativePath), + name: item.name, + }); + } + }; + + await visit(root); + return entries.sort((left, right) => left.path.localeCompare(right.path)); +}; + +/** + * Finds the declaration governing a path by walking up the directory chain and + * taking the first (deepest) match. Ties cannot happen: a folder may hold at + * most one declaration, which `collectDeclarations` enforces. + */ +const resolveScope = ( + directory: string, + scopes: Map, +): string | null => { + let current = directory; + + for (;;) { + const layerId = scopes.get(current); + if (layerId !== undefined) { + return layerId; + } + + const parent = posix.dirname(current); + if (parent === current) { + return null; + } + current = parent; + } +}; + +export const extract = async ( + options: ExtractOptions, +): Promise => { + const { repoRoot, packages, ignoredFilePattern } = options; + const ignoredDirectories = new Set(options.ignoredDirectories); + const diagnostics: Diagnostic[] = []; + + const accumulators = new Map(); + /** Folder → layer id it declares. */ + const scopes = new Map(); + /** Layer id → the file that declared it, for duplicate reporting. */ + const declaredBy = new Map(); + + const packageEntries = new Map(); + + for (const pkg of packages) { + // A package whose language has no extractor would otherwise contribute no + // files, no layers and no diagnostics — appearing in the model as covered + // while being entirely undescribed. That is the silent mis-bucketing this + // system exists to remove, so it is an error rather than a skip. + if (pkg.language !== "typescript") { + diagnostics.push({ + file: `${pkg.path}/package.json`, + line: null, + severity: "error", + message: `package \`${pkg.name}\` is configured as \`${pkg.language}\`, which has no extractor — it would be listed in the model with no layers. Remove it from architecture.config.ts until one exists.`, + }); + continue; + } + + const entries = await walkFiles( + join(repoRoot, pkg.path, pkg.sourceDirectory), + repoRoot, + ignoredDirectories, + ); + packageEntries.set( + pkg.name, + entries.filter((entry) => !ignoredFilePattern.test(entry.path)), + ); + } + + // Pass 1 — collect declarations, so inheritance can be resolved in pass 2 + // regardless of the order files are visited in. + for (const pkg of packages) { + for (const entry of packageEntries.get(pkg.name) ?? []) { + const isMarkdown = entry.name.toLowerCase().endsWith(".md"); + const isSource = sourceExtensions.has(extensionOf(entry.name)); + + if (!isMarkdown && !isSource) { + continue; + } + + const contents = await readFile(entry.absolutePath, "utf8"); + + const register = ( + accumulator: Omit< + LayerAccumulator, + "files" | "lineCount" | "references" + >, + ): void => { + const existingFile = declaredBy.get(accumulator.id); + if (existingFile !== undefined) { + diagnostics.push({ + file: accumulator.declaredIn, + line: null, + severity: "error", + message: `layer \`${accumulator.id}\` is already declared in ${existingFile}`, + }); + return; + } + + const existingScope = scopes.get(accumulator.scope); + if (existingScope !== undefined) { + diagnostics.push({ + file: accumulator.declaredIn, + line: null, + severity: "error", + message: `${accumulator.scope} already declares layer \`${existingScope}\`; a folder may declare at most one layer`, + }); + return; + } + + declaredBy.set(accumulator.id, accumulator.declaredIn); + scopes.set(accumulator.scope, accumulator.id); + accumulators.set(accumulator.id, { + ...accumulator, + references: [], + files: [], + lineCount: 0, + }); + }; + + if (isMarkdown) { + const { declaration, body, errors } = parseFrontmatter(contents); + + for (const message of errors) { + diagnostics.push({ + file: entry.path, + line: null, + severity: "error", + message, + }); + } + + if (declaration) { + const segments = declaration.layer.split("."); + register({ + id: declaration.layer, + name: segments[segments.length - 1] ?? "", + package: pkg.name, + role: declaration.role, + declaredIn: entry.path, + scope: entry.directory, + prose: body === "" ? null : body, + }); + } + + continue; + } + + const { tags, diagnostics: tagDiagnostics } = scanTags(contents); + + for (const diagnostic of tagDiagnostics) { + diagnostics.push({ + file: entry.path, + line: diagnostic.line, + severity: "error", + message: diagnostic.message, + }); + } + + if (tags.layerRoot) { + const id = tags.layerRoot.value; + const segments = id.split("."); + register({ + id, + name: segments[segments.length - 1] ?? "", + package: pkg.name, + role: tags.role?.value ?? "", + declaredIn: entry.path, + scope: entry.directory, + prose: null, + }); + + if (!tags.role) { + diagnostics.push({ + file: entry.path, + line: tags.layerRoot.line, + severity: "error", + message: `@layerRoot ${id} also needs an @role describing what the layer is responsible for`, + }); + } + } + } + } + + // Pass 2 — resolve every source file to a layer and fold in file-level tags. + const fileLayers = new Map(); + + for (const pkg of packages) { + for (const entry of packageEntries.get(pkg.name) ?? []) { + const isMarkdown = entry.name.toLowerCase().endsWith(".md"); + const isSource = sourceExtensions.has(extensionOf(entry.name)); + + if (!isMarkdown && !isSource) { + continue; + } + + const inheritedLayerId = resolveScope(entry.directory, scopes); + + if (isMarkdown) { + if (inheritedLayerId === null) { + continue; + } + const accumulator = accumulators.get(inheritedLayerId); + if (accumulator && accumulator.declaredIn !== entry.path) { + accumulator.references.push(entry.path); + } + continue; + } + + if (inheritedLayerId === null) { + diagnostics.push({ + file: entry.path, + line: null, + severity: "error", + message: `no layer resolves for this file — declare one on ${entry.directory} or an ancestor (README frontmatter or @layerRoot)`, + }); + continue; + } + + const accumulator = accumulators.get(inheritedLayerId); + + if (!accumulator) { + continue; + } + + // Only line counts are read here: with no per-file tags left in the + // vocabulary, a file's contents say nothing about the architecture beyond + // which layer's folder it sits in. + const contents = await readFile(entry.absolutePath, "utf8"); + + accumulator.files.push(entry.path); + accumulator.lineCount += countNonBlankLines(contents); + fileLayers.set(entry.path, inheritedLayerId); + } + } + + const layers: Layer[] = [...accumulators.values()] + .map((accumulator) => ({ + id: accumulator.id, + name: accumulator.name, + parent: + accumulator.id.lastIndexOf(".") === -1 + ? null + : accumulator.id.slice(0, accumulator.id.lastIndexOf(".")), + package: accumulator.package, + role: accumulator.role, + declaredIn: accumulator.declaredIn, + prose: accumulator.prose, + references: accumulator.references.sort((left, right) => + left.localeCompare(right), + ), + files: accumulator.files, + fileCount: accumulator.files.length, + lineCount: accumulator.lineCount, + })) + .sort((left, right) => left.id.localeCompare(right.id)); + + return { layers, fileLayers, diagnostics }; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/frontmatter.test.ts b/libs/@local/petrinaut-arch-docs/src/frontmatter.test.ts new file mode 100644 index 00000000000..0e296cdea86 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/frontmatter.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from "vitest"; + +import { parseFrontmatter } from "./frontmatter"; + +describe("parseFrontmatter", () => { + it("reads a full layer declaration and keeps the prose body", () => { + const { declaration, body, errors } = parseFrontmatter(`--- +layer: core.simulation.monte-carlo +name: Monte Carlo runtime +role: Runs many bounded-memory simulations +entryPoints: + - "@hashintel/petrinaut-core/workers/monte-carlo" +boundaries: + - kind: worker + note: Frame buffers never cross to the main thread +invariants: + - Two reusable frame buffers per run +--- + +# Monte Carlo + +Runs batches. +`); + + expect(errors).toEqual([]); + expect(declaration).toEqual({ + layer: "core.simulation.monte-carlo", + role: "Runs many bounded-memory simulations", + }); + expect(body).toBe("# Monte Carlo\n\nRuns batches."); + }); + + it("ignores keys this version does not read, rather than rejecting them", () => { + const { declaration, errors } = parseFrontmatter(`--- +layer: core.simulation.monte-carlo +name: Monte Carlo runtime +role: Runs many simulations with bounded frame memory +entryPoints: ["@hashintel/petrinaut-core/workers/monte-carlo"] +boundaries: + - kind: worker + note: Frame buffers stay inside the worker +invariants: ["Two reusable frame buffers per run"] +--- +`); + + expect(errors).toEqual([]); + expect(declaration).toEqual({ + layer: "core.simulation.monte-carlo", + role: "Runs many simulations with bounded frame memory", + }); + }); + + it("treats a README with no frontmatter as prose only", () => { + const { declaration, body, errors } = parseFrontmatter( + "# HIR\n\nThe compiler pipeline.\n", + ); + + expect(declaration).toBeNull(); + expect(errors).toEqual([]); + expect(body).toBe("# HIR\n\nThe compiler pipeline."); + }); + + it("ignores frontmatter that is unrelated to architecture", () => { + const { declaration, errors } = parseFrontmatter(`--- +title: Some page +sidebar_position: 3 +--- +`); + + expect(declaration).toBeNull(); + expect(errors).toEqual([]); + }); + + it("flags a half-written declaration missing its layer key", () => { + const { declaration, errors } = parseFrontmatter(`--- +role: Does a thing +boundaries: + - kind: worker + note: something +--- +`); + + expect(declaration).toBeNull(); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("no `layer` key"); + }); + + it("requires a role alongside a layer", () => { + const { declaration, errors } = parseFrontmatter(`--- +layer: core.thing +--- +`); + + expect(declaration).toBeNull(); + expect(errors.join(" ")).toContain("role"); + }); + + it("reports malformed YAML instead of throwing", () => { + const { declaration, errors } = parseFrontmatter(`--- +layer: [unclosed +--- +`); + + expect(declaration).toBeNull(); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("invalid YAML"); + }); + + it("handles CRLF line endings", () => { + const { declaration } = parseFrontmatter( + "---\r\nlayer: core.thing\r\nrole: Does a thing\r\n---\r\nBody\r\n", + ); + + expect(declaration?.layer).toBe("core.thing"); + }); +}); diff --git a/libs/@local/petrinaut-arch-docs/src/frontmatter.ts b/libs/@local/petrinaut-arch-docs/src/frontmatter.ts new file mode 100644 index 00000000000..ea67c7624ca --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/frontmatter.ts @@ -0,0 +1,112 @@ +/** + * Layer declarations read from folder `README.md` frontmatter. + * + * A README is the natural home for folder-level architecture metadata: the + * frontmatter declares the layer, and the prose below it becomes the layer's + * page body. Several Petrinaut folders already have READMEs describing exactly + * this, so declaring a layer there costs a few lines of frontmatter rather than + * a new document. + * + * A README *without* a `layer` key is not a declaration — it stays an ordinary + * document and is surfaced as a reference on whichever layer it falls under. + */ + +import { load } from "js-yaml"; +import { z } from "zod"; + +const frontmatterPattern = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u; + +/** + * Unknown keys are ignored rather than rejected. + * + * Several Petrinaut READMEs still carry `name`, `entryPoints`, `boundaries` and + * `invariants` from a richer vocabulary this version does not read. Rejecting + * them would force those facts to be deleted from the source to satisfy a + * generator that has simply stopped looking at them — so they stay written + * where they are, and re-reading one is a schema change here, not a rewrite of + * the packages. + */ +export const layerDeclarationSchema = z.object({ + /** Dotted layer id this folder and its descendants belong to. */ + layer: z.string().min(1), + /** One-line responsibility statement. */ + role: z.string().min(1), +}); + +export type LayerDeclaration = z.infer; + +export interface FrontmatterResult { + /** Present only when the frontmatter carried a `layer` key. */ + declaration: LayerDeclaration | null; + /** Markdown body with frontmatter removed. */ + body: string; + errors: string[]; +} + +/** + * Keys that only make sense as part of a layer declaration. Seeing one without + * a `layer` key almost always means the declaration is half-written, so it is + * reported rather than ignored. + */ +const declarationOnlyKeys = new Set(["role"]); + +export const parseFrontmatter = (markdown: string): FrontmatterResult => { + const match = frontmatterPattern.exec(markdown); + + if (!match) { + return { declaration: null, body: markdown.trim(), errors: [] }; + } + + const body = markdown.slice(match[0].length).trim(); + let parsed: unknown; + + try { + parsed = load(match[1] ?? ""); + } catch (error) { + return { + declaration: null, + body, + errors: [ + `invalid YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`, + ], + }; + } + + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return { declaration: null, body, errors: [] }; + } + + const record = parsed as Record; + + if (!("layer" in record)) { + const strayKeys = Object.keys(record).filter((key) => + declarationOnlyKeys.has(key), + ); + + return { + declaration: null, + body, + errors: + strayKeys.length > 0 + ? [ + `frontmatter has ${strayKeys.map((key) => `\`${key}\``).join(", ")} but no \`layer\` key, so it does not declare a layer`, + ] + : [], + }; + } + + const result = layerDeclarationSchema.safeParse(record); + + if (!result.success) { + return { + declaration: null, + body, + errors: result.error.issues.map( + (issue) => + `${issue.path.length > 0 ? `${issue.path.join(".")}: ` : ""}${issue.message}`, + ), + }; + } + + return { declaration: result.data, body, errors: [] }; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/graph.ts b/libs/@local/petrinaut-arch-docs/src/graph.ts new file mode 100644 index 00000000000..d880ee9d683 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/graph.ts @@ -0,0 +1,239 @@ +/** + * Turns the real TypeScript import graph into layer-level edges. + * + * dependency-cruiser supplies the file-level truth; the layer assignment comes + * from `extract.ts`. Aggregating one against the other is what makes the + * diagrams trustworthy — an edge appears because imports exist, never because + * someone drew it. + * + * Package subpath aliases are derived from each package's `exports` map rather + * than hand-listed, so a new entry point cannot silently drop out of the graph. + * The previous script hard-coded seven of `petrinaut-core`'s ten entry points, + * which meant imports through `./ai`, `./optimization` and `./compiled-model` + * resolved to nothing and vanished from the diagram. + */ + +import { existsSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +import { cruise, type ICruiseResult, type IModule } from "dependency-cruiser"; +import extractTSConfig from "dependency-cruiser/config-utl/extract-ts-config"; + +import { toPosix } from "./paths"; + +import type { ArchitecturePackage, Edge, Layer } from "./model"; + +/** How many representative file pairs to record per edge. */ +const examplesPerEdge = 3; + +interface Alias { + alias: string; + name: string; + onlyModule: true; +} + +/** + * Maps a package's `exports` targets back to their source entry files. + * + * `./dist/hir.js` → `src/hir.ts`, and `./dist/react.js` → `src/react/index.ts` + * when the flat file does not exist. Both shapes are in use across the + * Petrinaut packages. + */ +const deriveAliases = ( + repoRoot: string, + pkg: ArchitecturePackage, +): { aliases: Alias[]; warnings: string[] } => { + const packageRoot = join(repoRoot, pkg.path); + const manifestPath = join(packageRoot, "package.json"); + const warnings: string[] = []; + + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { + exports?: Record; + }; + + const aliases: Alias[] = []; + + for (const [subpath, target] of Object.entries(manifest.exports ?? {})) { + if (subpath === "./package.json") { + continue; + } + + const distPath = + typeof target === "string" + ? target + : ((target as Record | null)?.import ?? + (target as Record | null)?.default ?? + null); + + if (distPath === null || !distPath.endsWith(".js")) { + // Asset exports such as `./styles.css` have no module counterpart. + continue; + } + + const withoutDist = distPath.replace(/^\.\/dist\//u, ""); + const stem = withoutDist.replace(/\.js$/u, ""); + + const candidates = [ + join(packageRoot, "src", `${stem}.ts`), + join(packageRoot, "src", `${stem}.tsx`), + join(packageRoot, "src", stem, "index.ts"), + join(packageRoot, "src", stem, "index.tsx"), + ]; + + const resolved = candidates.find((candidate) => existsSync(candidate)); + + if (resolved === undefined) { + warnings.push( + `${pkg.name} exports \`${subpath}\` but no source entry file was found for it (looked for src/${stem}.ts and src/${stem}/index.ts)`, + ); + continue; + } + + const specifier = + subpath === "." ? pkg.name : `${pkg.name}${subpath.slice(1)}`; + + aliases.push({ alias: resolved, name: specifier, onlyModule: true }); + } + + // Longest specifier first so `@pkg/workers/lsp` is not shadowed by `@pkg`. + return { + aliases: aliases.sort( + (left, right) => right.name.length - left.name.length, + ), + warnings, + }; +}; + +export interface GraphOptions { + repoRoot: string; + packages: ArchitecturePackage[]; + tsconfigPath: string; + excludePattern: string; + /** Repo-relative source file → layer id, from `extract`. */ + fileLayers: Map; + layers: Layer[]; +} + +export interface GraphResult { + edges: Edge[]; + warnings: string[]; +} + +export const buildGraph = async ( + options: GraphOptions, +): Promise => { + const { repoRoot, packages, fileLayers, layers } = options; + const warnings: string[] = []; + + const aliases: Alias[] = []; + for (const pkg of packages) { + if (pkg.language !== "typescript") { + continue; + } + const derived = deriveAliases(repoRoot, pkg); + aliases.push(...derived.aliases); + warnings.push(...derived.warnings); + } + + const sourceRoots = packages + .filter((pkg) => pkg.language === "typescript") + .map((pkg) => `${pkg.path}/src`); + + const includeOnly = `^(?:${sourceRoots + .map((root) => root.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")) + .join("|")})/`; + + const cruiseResult = await cruise( + sourceRoots, + { + baseDir: repoRoot, + exclude: options.excludePattern, + includeOnly, + moduleSystems: ["es6"], + tsPreCompilationDeps: true, + }, + { + alias: aliases, + conditionNames: ["types", "import", "default"], + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], + }, + { tsConfig: extractTSConfig(options.tsconfigPath) }, + ); + + if (typeof cruiseResult.output === "string") { + throw new TypeError("dependency-cruiser returned formatted output"); + } + + const modules: IModule[] = (cruiseResult.output as ICruiseResult).modules; + + const layerPackages = new Map( + layers.map((layer) => [layer.id, layer.package]), + ); + + interface EdgeAccumulator { + fileDependencies: number; + examples: { from: string; to: string }[]; + } + + const edges = new Map(); + + for (const module of modules) { + const fromFile = toPosix(relative(repoRoot, join(repoRoot, module.source))); + const fromLayer = fileLayers.get(fromFile); + + if (fromLayer === undefined) { + continue; + } + + for (const dependency of module.dependencies) { + const toFile = toPosix( + relative(repoRoot, join(repoRoot, dependency.resolved)), + ); + const toLayer = fileLayers.get(toFile); + + // Imports landing outside any layer — node_modules, uncovered packages. + if (toLayer === undefined) { + continue; + } + + if (toLayer === fromLayer) { + continue; + } + + const key = `${fromLayer}\u0000${toLayer}`; + const accumulator = edges.get(key) ?? { + fileDependencies: 0, + examples: [], + }; + accumulator.fileDependencies += 1; + if (accumulator.examples.length < examplesPerEdge) { + accumulator.examples.push({ from: fromFile, to: toFile }); + } + edges.set(key, accumulator); + } + } + + const result: Edge[] = [...edges.entries()] + .map(([key, accumulator]) => { + const [from = "", to = ""] = key.split("\u0000"); + const fromPackage = layerPackages.get(from); + const toPackage = layerPackages.get(to); + + return { + from, + to, + fileDependencies: accumulator.fileDependencies, + examples: accumulator.examples, + crossesPackage: + fromPackage !== undefined && + toPackage !== undefined && + fromPackage !== toPackage, + }; + }) + .sort( + (left, right) => + left.from.localeCompare(right.from) || left.to.localeCompare(right.to), + ); + + return { edges: result, warnings }; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/index.ts b/libs/@local/petrinaut-arch-docs/src/index.ts new file mode 100644 index 00000000000..0ea2dbaeec2 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/index.ts @@ -0,0 +1,5 @@ +/** + * The one type a host of the bundle needs: the shape of `manifest.json`. + */ + +export type { BundleManifest } from "./emit/bundle-outputs"; diff --git a/libs/@local/petrinaut-arch-docs/src/model.ts b/libs/@local/petrinaut-arch-docs/src/model.ts new file mode 100644 index 00000000000..d2193e86874 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/model.ts @@ -0,0 +1,120 @@ +/** + * The architecture model — the schema every consumer reads. + * + * Consumers read `architecture.json` rather than re-parsing the codebase, so + * this is the public contract; bump `ARCHITECTURE_MODEL_VERSION` on a breaking + * shape change so they fail loudly rather than mis-reading fields. + */ + +import { z } from "zod"; + +export const ARCHITECTURE_MODEL_VERSION = 1; + +/** + * A layer: one node in the architecture, and one page in the docs. + * + * `id` is dotted and hierarchical (`core.simulation.monte-carlo`). Every + * ancestor segment must itself be a declared layer — the checks enforce this so + * the taxonomy cannot grow implicit holes. + */ +export const layerSchema = z.object({ + id: z + .string() + .regex( + /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*$/u, + "layer ids are dot-separated kebab-case segments", + ), + name: z.string().min(1), + parent: z.string().nullable(), + package: z.string().min(1), + /** One-line statement of what this layer is responsible for. */ + role: z.string().min(1), + /** Repo-relative path of the declaring `README.md` or entry file. */ + declaredIn: z.string().min(1), + /** Prose body of the declaring README, if any — becomes the page body. */ + prose: z.string().nullable(), + /** Other markdown under this layer, e.g. `hir/BUFFER_ABI.md`, linked from its page. */ + references: z.array(z.string().min(1)), + files: z.array(z.string().min(1)), + fileCount: z.number().int().nonnegative(), + /** Total non-blank lines across `files`. */ + lineCount: z.number().int().nonnegative(), +}); + +export type Layer = z.infer; + +/** An aggregated import relationship between two layers. */ +export const edgeSchema = z.object({ + from: z.string().min(1), + to: z.string().min(1), + /** How many file-level imports collapse into this edge. */ + fileDependencies: z.number().int().positive(), + /** A few representative imports, so a reader can jump to real code. */ + examples: z.array(z.object({ from: z.string(), to: z.string() })), + /** + * Whether the two layers live in different workspace packages. + * + * Deliberately the *only* boundary fact derived for edges. 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. Package membership, by contrast, is + * a property of the two layers and is always true when reported. + */ + crossesPackage: z.boolean(), +}); + +export type Edge = z.infer; + +export const packageSchema = z.object({ + name: z.string().min(1), + /** Repo-relative package root. */ + path: z.string().min(1), + description: z.string(), + language: z.enum(["typescript", "python"]), + /** + * Subdirectory holding the code the architecture describes, relative to + * `path`. Build configuration (`vite.config.ts`, `panda.config.ts`, + * `.storybook/`) sits outside it and is deliberately not part of any layer — + * it configures the build, it is not a piece of the system's design. + */ + sourceDirectory: z.string().min(1).default("src"), +}); + +export type ArchitecturePackage = z.infer; + +/** Config-facing shape, where defaulted fields may be omitted. */ +export type ArchitecturePackageInput = z.input; + +export const architectureModelSchema = z.object({ + version: z.literal(ARCHITECTURE_MODEL_VERSION), + packages: z.array(packageSchema), + layers: z.array(layerSchema), + edges: z.array(edgeSchema), + /** Echoed from config so consumers can render the rules without reading it. */ + rules: z.array( + z.object({ + from: z.string().min(1), + to: z.string().min(1), + reason: z.string().min(1), + }), + ), +}); + +export type ArchitectureModel = z.infer; + +/** Derive the parent layer id from a dotted id (`a.b.c` → `a.b`). */ +export const parentLayerId = (id: string): string | null => { + const lastDot = id.lastIndexOf("."); + return lastDot === -1 ? null : id.slice(0, lastDot); +}; + +/** All ancestor ids of a dotted id, nearest first. */ +export const ancestorLayerIds = (id: string): string[] => { + const ancestors: string[] = []; + let current = parentLayerId(id); + while (current !== null) { + ancestors.push(current); + current = parentLayerId(current); + } + return ancestors; +}; diff --git a/libs/@local/petrinaut-arch-docs/src/paths.ts b/libs/@local/petrinaut-arch-docs/src/paths.ts new file mode 100644 index 00000000000..8af5a139881 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/paths.ts @@ -0,0 +1,14 @@ +/** + * Path helpers shared by every stage that reports a file. + * + * Repo-relative, posix-separated paths are the model's currency: they appear in + * `declaredIn`, in diagnostics, in edge examples and in source links. The + * conversion lived in three modules separately, which is three chances for the + * model to disagree with itself about what a path looks like on Windows. + */ + +import { posix, sep } from "node:path"; + +/** Converts a platform path to the posix form the model always uses. */ +export const toPosix = (path: string): string => + path.split(sep).join(posix.sep); diff --git a/libs/@local/petrinaut-arch-docs/src/tags.test.ts b/libs/@local/petrinaut-arch-docs/src/tags.test.ts new file mode 100644 index 00000000000..046cb5b76c6 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/tags.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from "vitest"; + +import { scanTags } from "./tags"; + +describe("scanTags", () => { + it("reads a layer declaration from a file header", () => { + const { tags, diagnostics } = scanTags(`/** + * @layerRoot core.simulation.monte-carlo + * @role Runs many bounded-memory simulations + */ +export const run = () => {}; +`); + + expect(diagnostics).toEqual([]); + expect(tags.layerRoot?.value).toBe("core.simulation.monte-carlo"); + expect(tags.layerRoot?.line).toBe(2); + expect(tags.role?.value).toBe("Runs many bounded-memory simulations"); + }); + + it("continues a tag's text across wrapped lines", () => { + const { tags } = scanTags(`/** + * @role Runs many bounded-memory simulations, so a long experiment + * never grows the heap regardless of how many frames it computes + */`); + + expect(tags.role?.value).toBe( + "Runs many bounded-memory simulations, so a long experiment never grows the heap regardless of how many frames it computes", + ); + }); + + it("ends a tag's text at a blank line", () => { + const { tags } = scanTags(`/** + * @role Compiles user code + * + * Extra prose that is not part of the role. + */`); + + expect(tags.role?.value).toBe("Compiles user code"); + }); + + it("reports a duplicated singular tag rather than silently overwriting", () => { + const { tags, diagnostics } = scanTags(`/** + * @role First + * @role Second + */`); + + expect(tags.role?.value).toBe("First"); + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toContain("duplicate @role"); + }); + + it("suggests a correction for a miscased tag", () => { + const { diagnostics } = scanTags(`/** + * @LayerRoot core.thing + */`); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toBe( + "unknown tag @LayerRoot; did you mean @layerRoot?", + ); + }); + + it("ignores standard JSDoc tags", () => { + const { tags, diagnostics } = scanTags(`/** + * Does a thing. + * + * @param input the thing + * @returns the other thing + * @see somewhere + */`); + + expect(diagnostics).toEqual([]); + expect(tags.layerRoot).toBeNull(); + expect(tags.role).toBeNull(); + }); + + it("ignores an annotation this version does not read", () => { + const { tags, diagnostics } = scanTags(`/** + * @layerRoot core.lsp + * @role Language-server client + * @boundary thread — requests reach the server over a worker transport + * @invariant No DOM access + * @entryPoint @hashintel/petrinaut-core/workers/lsp + */`); + + expect(diagnostics).toEqual([]); + expect(tags.layerRoot?.value).toBe("core.lsp"); + expect(tags.role?.value).toBe("Language-server client"); + }); + + it("does not treat a tag mentioned mid-sentence as a tag", () => { + const { tags, diagnostics } = scanTags(`/** + * Layers are declared with @layerRoot on a folder entry file. + */`); + + expect(tags.layerRoot).toBeNull(); + expect(diagnostics).toEqual([]); + }); + + it("reports line numbers relative to the whole file", () => { + const { tags } = scanTags(`line one +line two +line three + +/** + * @layerRoot core.thing + */`); + + expect(tags.layerRoot?.line).toBe(6); + }); +}); diff --git a/libs/@local/petrinaut-arch-docs/src/tags.ts b/libs/@local/petrinaut-arch-docs/src/tags.ts new file mode 100644 index 00000000000..e6db88a3206 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/src/tags.ts @@ -0,0 +1,202 @@ +/** + * The in-code tag vocabulary, and the scanner that reads it. + * + * A layer is declared either here, with `@layerRoot` on a folder's primary + * file, or in a folder `README.md`'s frontmatter (see `frontmatter.ts`), whose + * prose then becomes the layer's page. Files with no tags inherit from the + * nearest declaring ancestor. + * + * The vocabulary is deliberately two tags: an id and a one-line role. Both are + * needed to place a layer in the graph and to label it — anything more is a + * claim the generator cannot check, and this version does not make claims it + * cannot keep. Other tags (`@boundary`, `@invariant`, `@entryPoint`) remain + * written in the Petrinaut source and are simply not read; re-registering one + * here is all it would take to surface them again. + * + * Tags are recognised only at the start of a line inside a block comment, so a + * tag named in prose or in a string literal is not picked up. + */ + +/** A tag occurrence, with the line it was found on for error reporting. */ +export interface TagValue { + value: string; + line: number; +} + +export interface ParsedTags { + /** + * `@layerRoot` — declares that this file's folder *and its descendants* form + * the named layer. The alternative to a README declaration, for folders that + * have a barrel entry file but no README. + */ + layerRoot: TagValue | null; + /** `@role` — one-line statement of what the layer is responsible for. */ + role: TagValue | null; +} + +export interface TagDiagnostic { + line: number; + message: string; +} + +export interface TagScanResult { + tags: ParsedTags; + diagnostics: TagDiagnostic[]; +} + +const emptyTags = (): ParsedTags => ({ + layerRoot: null, + role: null, +}); + +/** Tags that may appear at most once per file. */ +const singularTags = ["layerRoot", "role"] as const; + +type SingularTag = (typeof singularTags)[number]; + +const knownTagNames = new Set(singularTags); + +const blockCommentPattern = /\/\*\*[\s\S]*?\*\//gu; + +/** + * Byte offsets of the start of each line, so an offset can be turned into a + * 1-based line number by binary search rather than a rescan per comment. + */ +const lineStartOffsets = (text: string): number[] => { + const starts = [0]; + for (let position = 0; position < text.length; position += 1) { + if (text[position] === "\n") { + starts.push(position + 1); + } + } + return starts; +}; + +const lineNumberAt = (lineStarts: number[], offset: number): number => { + let low = 0; + let high = lineStarts.length - 1; + while (low < high) { + const middle = Math.ceil((low + high) / 2); + if ((lineStarts[middle] ?? 0) <= offset) { + low = middle; + } else { + high = middle - 1; + } + } + return low + 1; +}; + +/** + * Strips the comment delimiters and the leading `*` gutter, returning body + * lines paired with their line number in the original file. + */ +const commentBodyLines = ( + comment: string, + startLine: number, +): { text: string; line: number }[] => + comment + .replace(/^\/\*\*/u, "") + .replace(/\*\/$/u, "") + .split("\n") + .map((rawLine, offset) => ({ + text: rawLine.replace(/^\s*\*\s?/u, ""), + line: startLine + offset, + })); + +interface RawTag { + name: string; + text: string; + line: number; +} + +/** + * Collects tags from one comment body. A tag's text continues onto following + * lines until the next tag or the end of the comment, so multi-line notes stay + * readable in source. + */ +const collectRawTags = (lines: { text: string; line: number }[]): RawTag[] => { + const tags: RawTag[] = []; + let current: RawTag | null = null; + + for (const { text, line } of lines) { + const match = /^@([A-Za-z][A-Za-z0-9]*)\s*(.*)$/u.exec(text.trim()); + + if (match) { + if (current) { + tags.push(current); + } + current = { name: match[1] ?? "", text: match[2] ?? "", line }; + continue; + } + + if (current) { + const continuation = text.trim(); + if (continuation === "") { + // A blank line ends the tag's text but not the comment. + tags.push(current); + current = null; + } else { + current.text = `${current.text} ${continuation}`.trim(); + } + } + } + + if (current) { + tags.push(current); + } + + return tags; +}; + +/** Reads every architecture tag out of a source file's block comments. */ +export const scanTags = (sourceText: string): TagScanResult => { + const tags = emptyTags(); + const diagnostics: TagDiagnostic[] = []; + + const assignSingular = (name: SingularTag, { text, line }: RawTag): void => { + if (text === "") { + diagnostics.push({ line, message: `@${name} requires a value` }); + return; + } + if (tags[name] !== null) { + diagnostics.push({ + line, + message: `duplicate @${name} (already set on line ${tags[name]?.line})`, + }); + return; + } + tags[name] = { value: text, line }; + }; + + const lineStarts = lineStartOffsets(sourceText); + + for (const match of sourceText.matchAll(blockCommentPattern)) { + const startLine = lineNumberAt(lineStarts, match.index); + const rawTags = collectRawTags(commentBodyLines(match[0], startLine)); + + for (const rawTag of rawTags) { + const { name, line } = rawTag; + + if (singularTags.includes(name as SingularTag)) { + assignSingular(name as SingularTag, rawTag); + continue; + } + + // Only a miscased version of one of our own tags is reported. Every other + // unknown tag is someone else's — `@param`, `@deprecated`, an eslint + // directive, or one of the annotations this version deliberately does not + // read — and is none of our business. + const suggestion = [...knownTagNames].find( + (known) => known !== name && known.toLowerCase() === name.toLowerCase(), + ); + if (suggestion !== undefined) { + diagnostics.push({ + line, + message: `unknown tag @${name}; did you mean @${suggestion}?`, + }); + } + } + } + + return { tags, diagnostics }; +}; diff --git a/libs/@local/petrinaut-arch-docs/tsconfig.json b/libs/@local/petrinaut-arch-docs/tsconfig.json new file mode 100644 index 00000000000..d25e7719d59 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2024", + "lib": ["ESNext"], + "types": ["node"], + "module": "preserve", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true + }, + "include": ["src", "architecture.config.ts"] +} diff --git a/libs/@local/petrinaut-arch-docs/turbo.json b/libs/@local/petrinaut-arch-docs/turbo.json new file mode 100644 index 00000000000..17273dee65f --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/turbo.json @@ -0,0 +1,20 @@ +{ + "extends": ["//"], + "tasks": { + "doc:architecture": { + // Not cached. Turborepo hashes a task's own package plus its dependencies' + // task *outputs*, and the annotations this reads are plain source comments + // in `@hashintel/petrinaut` and `petrinaut-core` — not any task's output. + // A cached result would therefore survive an annotation change and go + // quietly stale, which is the exact failure this package exists to prevent. + // The run takes a few seconds; correctness is worth more here. + "cache": false, + // Declared so consumers can depend on this task rather than on the + // directory existing, and so `turbo run` prunes it on a clean. + "outputs": ["bundle/**"] + }, + "lint:arch-docs": { + "cache": false + } + } +} diff --git a/oxfmt.config.ts b/oxfmt.config.ts index 99290d85b26..0b7274dcd29 100644 --- a/oxfmt.config.ts +++ b/oxfmt.config.ts @@ -67,5 +67,11 @@ export default defineConfig({ "**/*.snap.*", "**/openapi.json", "**/*.aux.mir", + // The architecture bundle is written by @local/petrinaut-arch-docs, which + // owns its byte-for-byte output — CI diffs a fresh build against these + // files to detect drift. Reformatting them here would make every + // regeneration fail the format check and every format run fail the drift + // check. + "libs/@local/petrinaut-arch-docs/bundle/**", ], }); diff --git a/yarn.config.cjs b/yarn.config.cjs index 0ecfa160fb5..c860ad062d0 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -18,6 +18,9 @@ const enforcedDevDependencies = { const allowedUnscriptedDevDependencies = { // Bundled into the LSP worker, not required by published package consumers. "@hashintel/petrinaut-core": new Set(["typescript"]), + // `astro check` type-checks the diagram components, and needs TypeScript + // even though no script invokes `tsc` directly. + "@apps/petrinaut-docs": new Set(["typescript"]), }; const ignoredDependencies = [ diff --git a/yarn.lock b/yarn.lock index 20f7da8ae38..79bb858f8fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -888,6 +888,25 @@ __metadata: languageName: unknown linkType: soft +"@apps/petrinaut-docs@workspace:apps/petrinaut-docs": + version: 0.0.0-use.local + resolution: "@apps/petrinaut-docs@workspace:apps/petrinaut-docs" + dependencies: + "@astrojs/check": "npm:0.9.10" + "@astrojs/react": "npm:6.0.2" + "@astrojs/starlight": "npm:0.41.5" + "@local/petrinaut-arch-docs": "workspace:*" + "@types/node": "npm:22.18.13" + "@types/react": "npm:19.2.14" + "@types/react-dom": "npm:19.2.3" + astro: "npm:7.1.6" + react: "npm:19.2.6" + react-dom: "npm:19.2.6" + sharp: "npm:0.35.3" + typescript: "npm:5.9.3" + languageName: unknown + linkType: soft + "@apps/petrinaut-opt@workspace:*, @apps/petrinaut-opt@workspace:apps/petrinaut-opt": version: 0.0.0-use.local resolution: "@apps/petrinaut-opt@workspace:apps/petrinaut-opt" @@ -1100,10 +1119,356 @@ __metadata: languageName: node linkType: hard -"@astrojs/compiler@npm:^2.0.0": - version: 2.10.3 - resolution: "@astrojs/compiler@npm:2.10.3" - checksum: 10c0/35e7a6e9d197924a3203afd3bd7bff39c8d4271516816c30173cca872302312c3748eefc5a5832523f49b98743115628a1b96922d7d96588a8d96e110a106b88 +"@astrojs/check@npm:0.9.10": + version: 0.9.10 + resolution: "@astrojs/check@npm:0.9.10" + dependencies: + "@astrojs/language-server": "npm:^2.16.7" + chokidar: "npm:^4.0.3" + kleur: "npm:^4.1.5" + yargs: "npm:^18.0.0" + peerDependencies: + typescript: ^5.0.0 || ^6.0.0 + bin: + astro-check: ./bin/astro-check.js + checksum: 10c0/a5de46d71e2c295bbee3c575af73b015e070c2a418f1bc1196b115fddacf60681b5313d9f6ea0427cec5f138bdf6d3c894d8e1938fd54025624b7bab2f52e7eb + languageName: node + linkType: hard + +"@astrojs/compiler-binding-darwin-arm64@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-darwin-arm64@npm:0.3.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@astrojs/compiler-binding-darwin-x64@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-darwin-x64@npm:0.3.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@astrojs/compiler-binding-linux-arm64-gnu@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-linux-arm64-gnu@npm:0.3.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@astrojs/compiler-binding-linux-arm64-musl@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-linux-arm64-musl@npm:0.3.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@astrojs/compiler-binding-linux-x64-gnu@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-linux-x64-gnu@npm:0.3.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@astrojs/compiler-binding-linux-x64-musl@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-linux-x64-musl@npm:0.3.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@astrojs/compiler-binding-wasm32-wasi@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-wasm32-wasi@npm:0.3.2" + dependencies: + "@napi-rs/wasm-runtime": "npm:^1.2.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@astrojs/compiler-binding-win32-arm64-msvc@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-win32-arm64-msvc@npm:0.3.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@astrojs/compiler-binding-win32-x64-msvc@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding-win32-x64-msvc@npm:0.3.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@astrojs/compiler-binding@npm:0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-binding@npm:0.3.2" + dependencies: + "@astrojs/compiler-binding-darwin-arm64": "npm:0.3.2" + "@astrojs/compiler-binding-darwin-x64": "npm:0.3.2" + "@astrojs/compiler-binding-linux-arm64-gnu": "npm:0.3.2" + "@astrojs/compiler-binding-linux-arm64-musl": "npm:0.3.2" + "@astrojs/compiler-binding-linux-x64-gnu": "npm:0.3.2" + "@astrojs/compiler-binding-linux-x64-musl": "npm:0.3.2" + "@astrojs/compiler-binding-wasm32-wasi": "npm:0.3.2" + "@astrojs/compiler-binding-win32-arm64-msvc": "npm:0.3.2" + "@astrojs/compiler-binding-win32-x64-msvc": "npm:0.3.2" + dependenciesMeta: + "@astrojs/compiler-binding-darwin-arm64": + optional: true + "@astrojs/compiler-binding-darwin-x64": + optional: true + "@astrojs/compiler-binding-linux-arm64-gnu": + optional: true + "@astrojs/compiler-binding-linux-arm64-musl": + optional: true + "@astrojs/compiler-binding-linux-x64-gnu": + optional: true + "@astrojs/compiler-binding-linux-x64-musl": + optional: true + "@astrojs/compiler-binding-wasm32-wasi": + optional: true + "@astrojs/compiler-binding-win32-arm64-msvc": + optional: true + "@astrojs/compiler-binding-win32-x64-msvc": + optional: true + checksum: 10c0/8cdf2c6a9b64cee904f81b88f8b15aad7b2225b569f7c973c713dd960774f625c5f1857db3d8a2bd1cdc0a6aa865a178c750d7407d8c0c8447f0f272d6825ac2 + languageName: node + linkType: hard + +"@astrojs/compiler-rs@npm:^0.3.2": + version: 0.3.2 + resolution: "@astrojs/compiler-rs@npm:0.3.2" + dependencies: + "@astrojs/compiler-binding": "npm:0.3.2" + checksum: 10c0/b92237f7b93eb6439ed0fb3676abe6c2e0df74c50a5e1b4b2ace5884f9169fb9d49113d4ae8bea360b6f6d7c1c4545e4f600739cb46c9a6b1e691fa0e3ec5d5c + languageName: node + linkType: hard + +"@astrojs/compiler@npm:^2.0.0, @astrojs/compiler@npm:^2.13.1": + version: 2.13.1 + resolution: "@astrojs/compiler@npm:2.13.1" + checksum: 10c0/83d85c30c8b1bd6d2382f1bc3d99126732838dc747e3a9defa55f726ccc2276c710e6af81bfb68d8fb17fde452c69cf0187cb1d2f8eb022764f1ccc4cf3a3db1 + languageName: node + linkType: hard + +"@astrojs/internal-helpers@npm:0.10.2": + version: 0.10.2 + resolution: "@astrojs/internal-helpers@npm:0.10.2" + dependencies: + "@types/hast": "npm:^3.0.4" + "@types/mdast": "npm:^4.0.4" + js-yaml: "npm:^4.3.0" + picomatch: "npm:^4.0.4" + retext-smartypants: "npm:^6.2.0" + shiki: "npm:^4.0.2" + smol-toml: "npm:^1.6.0" + unified: "npm:^11.0.5" + checksum: 10c0/003fd40fc588b70ee50daf49d1d76ff9acf3f946d7d79fcb06c89187d078c5b4044f7a24c6c3b614363dd9bfd7be6cdced98d1422bbbc090f902ea7b3b27d0bb + languageName: node + linkType: hard + +"@astrojs/language-server@npm:^2.16.7": + version: 2.16.13 + resolution: "@astrojs/language-server@npm:2.16.13" + dependencies: + "@astrojs/compiler": "npm:^2.13.1" + "@astrojs/yaml2ts": "npm:^0.2.4" + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + "@volar/kit": "npm:~2.4.28" + "@volar/language-core": "npm:~2.4.28" + "@volar/language-server": "npm:~2.4.28" + "@volar/language-service": "npm:~2.4.28" + muggle-string: "npm:^0.4.1" + tinyglobby: "npm:^0.2.16" + volar-service-css: "npm:0.0.71" + volar-service-emmet: "npm:0.0.71" + volar-service-html: "npm:0.0.71" + volar-service-prettier: "npm:0.0.71" + volar-service-typescript: "npm:0.0.71" + volar-service-typescript-twoslash-queries: "npm:0.0.71" + volar-service-yaml: "npm:0.0.71" + vscode-html-languageservice: "npm:^5.6.2" + vscode-uri: "npm:^3.1.0" + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: ">=0.11.0" + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + bin: + astro-ls: ./bin/nodeServer.js + checksum: 10c0/ea46894ba994d45deb8c86696587dbd2a99013c352eef066f505a1583cd24e050d3fb4af0db9828be89d597a42e895fcbbd0fd2cb33ea9127f9752405a093068 + languageName: node + linkType: hard + +"@astrojs/markdown-remark@npm:7.2.2": + version: 7.2.2 + resolution: "@astrojs/markdown-remark@npm:7.2.2" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/prism": "npm:4.0.2" + github-slugger: "npm:^2.0.0" + hast-util-from-html: "npm:^2.0.3" + hast-util-to-text: "npm:^4.0.2" + mdast-util-definitions: "npm:^6.0.0" + rehype-raw: "npm:^7.0.0" + rehype-stringify: "npm:^10.0.1" + remark-gfm: "npm:^4.0.1" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.1.2" + remark-smartypants: "npm:^3.0.2" + unified: "npm:^11.0.5" + unist-util-remove-position: "npm:^5.0.0" + unist-util-visit: "npm:^5.1.0" + unist-util-visit-parents: "npm:^6.0.2" + vfile: "npm:^6.0.3" + checksum: 10c0/acec0876cdb4d9900bffc72ab41ff5dfd9595308581b330896e0c6b942dc095c8ea150273471a0a70ba99091667a0a0d762137bab1a5154addfd32cd725027e2 + languageName: node + linkType: hard + +"@astrojs/markdown-satteri@npm:0.3.5, @astrojs/markdown-satteri@npm:^0.3.2": + version: 0.3.5 + resolution: "@astrojs/markdown-satteri@npm:0.3.5" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/prism": "npm:4.0.2" + github-slugger: "npm:^2.0.0" + hast-util-from-html: "npm:^2.0.3" + satteri: "npm:^0.9.1" + checksum: 10c0/4d88de2c7b058c8ab0a1343e35cfec14f50f36c2c7a7f8024f679ef4edb5e42c8ca8c8828446aa9124aa1c2f5fb1e0ec837e7bf789cd775e7e61a2405a09bc24 + languageName: node + linkType: hard + +"@astrojs/mdx@npm:^7.0.0": + version: 7.0.5 + resolution: "@astrojs/mdx@npm:7.0.5" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/markdown-remark": "npm:7.2.2" + "@mdx-js/mdx": "npm:^3.1.1" + acorn: "npm:^8.16.0" + es-module-lexer: "npm:^2.0.0" + estree-util-visit: "npm:^2.0.0" + hast-util-to-html: "npm:^9.0.5" + piccolore: "npm:^0.1.3" + rehype-raw: "npm:^7.0.0" + remark-gfm: "npm:^4.0.1" + remark-smartypants: "npm:^3.0.2" + source-map: "npm:^0.7.6" + unist-util-visit: "npm:^5.1.0" + vfile: "npm:^6.0.3" + peerDependencies: + "@astrojs/markdown-satteri": ^0.3.1 + astro: ^7.0.0 + peerDependenciesMeta: + "@astrojs/markdown-satteri": + optional: true + checksum: 10c0/e917ee98475b107488fa2bc08d3b60fa5acc771ab925725c0b72e8374fc6ed0287f4684013cf40b000771f3f3aa45bd42733c5c9ff00024fab5b245bf1ca958f + languageName: node + linkType: hard + +"@astrojs/prism@npm:4.0.2": + version: 4.0.2 + resolution: "@astrojs/prism@npm:4.0.2" + dependencies: + prismjs: "npm:^1.30.0" + checksum: 10c0/23a5bc096f966c08be6f8e89adfe9287fd6d08eeb9c4ec7de8172280aa638f2684dcefc5f848f55e211ff63369abc4f0661698daa5e2de6078c8f269215a7101 + languageName: node + linkType: hard + +"@astrojs/react@npm:6.0.2": + version: 6.0.2 + resolution: "@astrojs/react@npm:6.0.2" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@vitejs/plugin-react": "npm:^5.2.0" + devalue: "npm:^5.8.1" + ultrahtml: "npm:^1.6.0" + vite: "npm:^8.0.13" + peerDependencies: + "@types/react": ^17.0.50 || ^18.0.21 || ^19.0.0 + "@types/react-dom": ^17.0.17 || ^18.0.6 || ^19.0.0 + react: ^17.0.2 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 + checksum: 10c0/47e546a4a75e42738abe5cf3df0971297cd700fbd91649d3e9149a335f29e9be9dbc08ab04490d81bc57f56aa0c6f70d4dfa3241f1b00ece52d80f74b7858dd7 + languageName: node + linkType: hard + +"@astrojs/sitemap@npm:^3.7.2": + version: 3.7.3 + resolution: "@astrojs/sitemap@npm:3.7.3" + dependencies: + sitemap: "npm:^9.0.0" + stream-replace-string: "npm:^2.0.0" + zod: "npm:^4.3.6" + checksum: 10c0/99c0240586ea2ab1868903341a302ff4bd909b53c49c1c8be476921593fbee838623207c4fa88522264d79291f074ea536aef614d6b1fb286f29a1a5d244ec21 + languageName: node + linkType: hard + +"@astrojs/starlight@npm:0.41.5": + version: 0.41.5 + resolution: "@astrojs/starlight@npm:0.41.5" + dependencies: + "@astrojs/markdown-satteri": "npm:^0.3.2" + "@astrojs/mdx": "npm:^7.0.0" + "@astrojs/sitemap": "npm:^3.7.2" + "@pagefind/default-ui": "npm:^1.3.0" + "@types/hast": "npm:^3.0.4" + "@types/js-yaml": "npm:^4.0.9" + "@types/mdast": "npm:^4.0.4" + astro-expressive-code: "npm:^0.44.0" + bcp-47: "npm:^2.1.0" + hast-util-from-html: "npm:^2.0.3" + hast-util-select: "npm:^6.0.4" + hast-util-to-string: "npm:^3.0.1" + hastscript: "npm:^9.0.1" + i18next: "npm:^26.0.7" + js-yaml: "npm:^4.1.1" + klona: "npm:^2.0.6" + magic-string: "npm:^0.30.21" + mdast-util-directive: "npm:^3.1.0" + mdast-util-to-markdown: "npm:^2.1.2" + mdast-util-to-string: "npm:^4.0.0" + pagefind: "npm:^1.5.2" + rehype: "npm:^13.0.2" + rehype-format: "npm:^5.0.1" + remark-directive: "npm:^4.0.0" + satteri: "npm:^0.9.1" + ultrahtml: "npm:^1.6.0" + unified: "npm:^11.0.5" + unist-util-visit: "npm:^5.1.0" + vfile: "npm:^6.0.3" + peerDependencies: + "@astrojs/markdown-remark": ^7.2.0 + astro: ^7.0.2 + peerDependenciesMeta: + "@astrojs/markdown-remark": + optional: true + checksum: 10c0/cfb3d03c9ad05a2aad3b6180efc2bf093a73e36ae5ce4a4bdc0ae051a941d40a3e559f4958c31d1f47a8bcf2bb476ab087a3cc27e204a778c861680da63fd67d + languageName: node + linkType: hard + +"@astrojs/telemetry@npm:3.3.3": + version: 3.3.3 + resolution: "@astrojs/telemetry@npm:3.3.3" + dependencies: + ci-info: "npm:^4.4.0" + dset: "npm:^3.1.4" + is-docker: "npm:^4.0.0" + package-manager-detector: "npm:^1.6.0" + checksum: 10c0/d5373e0aa8cbfd684143027dce4e31f7fd45e90df799ce4d255fb750109173a3dd2fc3ef26532b298c715fe063d6e955984d238e323d96e1d8f6c0a03fbbbc73 + languageName: node + linkType: hard + +"@astrojs/yaml2ts@npm:^0.2.4": + version: 0.2.4 + resolution: "@astrojs/yaml2ts@npm:0.2.4" + dependencies: + yaml: "npm:^2.8.3" + checksum: 10c0/bb0a25ae730ea2b8ba20da4626c9d023ba27898396c2ad71384e0aae10c4479bb5d6e9f4f27aad1f486f1d287200c4753ff949ab16af367788f085b078d4b4e6 languageName: node linkType: hard @@ -4149,6 +4514,73 @@ __metadata: languageName: node linkType: hard +"@bruits/satteri-darwin-arm64@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-darwin-arm64@npm:0.9.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@bruits/satteri-darwin-x64@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-darwin-x64@npm:0.9.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@bruits/satteri-linux-arm64-gnu@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-arm64-gnu@npm:0.9.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@bruits/satteri-linux-arm64-musl@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-arm64-musl@npm:0.9.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@bruits/satteri-linux-x64-gnu@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-x64-gnu@npm:0.9.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@bruits/satteri-linux-x64-musl@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-x64-musl@npm:0.9.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@bruits/satteri-wasm32-wasi@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-wasm32-wasi@npm:0.9.5" + dependencies: + "@emnapi/core": "npm:1.11.1" + "@emnapi/runtime": "npm:1.11.1" + "@napi-rs/wasm-runtime": "npm:^1.1.6" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@bruits/satteri-win32-arm64-msvc@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-win32-arm64-msvc@npm:0.9.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@bruits/satteri-win32-x64-msvc@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-win32-x64-msvc@npm:0.9.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@bufbuild/protobuf@npm:^2.6.2": version: 2.12.1 resolution: "@bufbuild/protobuf@npm:2.12.1" @@ -4156,6 +4588,15 @@ __metadata: languageName: node linkType: hard +"@capsizecss/unpack@npm:^4.0.0": + version: 4.0.1 + resolution: "@capsizecss/unpack@npm:4.0.1" + dependencies: + fontkitten: "npm:^1.0.3" + checksum: 10c0/72ce03871e137bae1c4925f9dc916a6250cf9b9630a8bb28d2ef7575d20fd7ef0e8966b361aaa3a54c0e8d5ff40b133490bd97a9beb6dc209529102947d392b1 + languageName: node + linkType: hard + "@chainsafe/as-chacha20poly1305@npm:^0.1.0": version: 0.1.0 resolution: "@chainsafe/as-chacha20poly1305@npm:0.1.0" @@ -4533,6 +4974,16 @@ __metadata: languageName: node linkType: hard +"@clack/core@npm:1.4.3": + version: 1.4.3 + resolution: "@clack/core@npm:1.4.3" + dependencies: + fast-wrap-ansi: "npm:^0.2.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/16cda430974bc02844cfa6e3f0e6e19695d97e915a580ab74603d34a1f00dcbdd8dc856adc3f89405e3555b5cf42568ae8687e748cb92c3434fe4d9fc7ebe664 + languageName: node + linkType: hard + "@clack/prompts@npm:0.11.0": version: 0.11.0 resolution: "@clack/prompts@npm:0.11.0" @@ -4544,6 +4995,18 @@ __metadata: languageName: node linkType: hard +"@clack/prompts@npm:^1.1.0": + version: 1.7.0 + resolution: "@clack/prompts@npm:1.7.0" + dependencies: + "@clack/core": "npm:1.4.3" + fast-string-width: "npm:^3.0.2" + fast-wrap-ansi: "npm:^0.2.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/a11e4f8d4a03cfe2d9eb21c3263f0252648573977b96e495a7d4f159f7efa929fe775a6ac3fe7ae30d8acb72514d94418a2c4335855d05b575a41aa885c9ff26 + languageName: node + linkType: hard + "@cloudamqp/amqp-client@npm:^2.1.1": version: 2.1.1 resolution: "@cloudamqp/amqp-client@npm:2.1.1" @@ -4725,6 +5188,13 @@ __metadata: languageName: node linkType: hard +"@ctrl/tinycolor@npm:^4.0.4": + version: 4.2.0 + resolution: "@ctrl/tinycolor@npm:4.2.0" + checksum: 10c0/374034581953f6debd950e4b07da833d1d8f7af5aead5117c91545fbe1583c91b2407bc285e1625529839135d6465c2e7549e7a8ad70979cab4c207486798064 + languageName: node + linkType: hard + "@dabh/diagnostics@npm:^2.0.8": version: 2.0.8 resolution: "@dabh/diagnostics@npm:2.0.8" @@ -4940,6 +5410,64 @@ __metadata: languageName: node linkType: hard +"@emmetio/abbreviation@npm:^2.3.3": + version: 2.3.3 + resolution: "@emmetio/abbreviation@npm:2.3.3" + dependencies: + "@emmetio/scanner": "npm:^1.0.4" + checksum: 10c0/835b460706d5920a6f9a569a44b7d98e88d5530e3983af3678b44fa38b4cbdf68b5df933476d72e340779b16e7e7962ffa63142db8d2f59b1175a11c30c14635 + languageName: node + linkType: hard + +"@emmetio/css-abbreviation@npm:^2.1.8": + version: 2.1.8 + resolution: "@emmetio/css-abbreviation@npm:2.1.8" + dependencies: + "@emmetio/scanner": "npm:^1.0.4" + checksum: 10c0/b5b3b39e773185d848b634e48e1b520e6ebffd28bfd0ba34fbcf877ca77e0edb8c7bbf58230cb0621f80f579bd7fd0265f00ab5e09ac482a835897cbdb6182a6 + languageName: node + linkType: hard + +"@emmetio/css-parser@npm:^0.4.1": + version: 0.4.1 + resolution: "@emmetio/css-parser@npm:0.4.1" + dependencies: + "@emmetio/stream-reader": "npm:^2.2.0" + "@emmetio/stream-reader-utils": "npm:^0.1.0" + checksum: 10c0/d34bf6c8d431eaa2b8b9cdff14360755ca7e235392a23666fe89f2e2b14ac54657712c196bbb6cb016b5bbf3e19faf7f413f7de815d35a26fce82a507191950e + languageName: node + linkType: hard + +"@emmetio/html-matcher@npm:^1.3.0": + version: 1.3.0 + resolution: "@emmetio/html-matcher@npm:1.3.0" + dependencies: + "@emmetio/scanner": "npm:^1.0.0" + checksum: 10c0/354f6d4d52ffb124883cc350d6a09942f2ec352b03dfb928ba8c3078b0f570041b6095367367094a307388c158e6b053565c09a9c1db699517801435a0eadb5c + languageName: node + linkType: hard + +"@emmetio/scanner@npm:^1.0.0, @emmetio/scanner@npm:^1.0.4": + version: 1.0.4 + resolution: "@emmetio/scanner@npm:1.0.4" + checksum: 10c0/ae6244e563caaff0f88d7afefc33fd6cfb7cc767ce914b54d35b46002637948cfc65951dba6d6941328afa54c721c225836fafce2de40fb7643660ba09fe7372 + languageName: node + linkType: hard + +"@emmetio/stream-reader-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "@emmetio/stream-reader-utils@npm:0.1.0" + checksum: 10c0/8630a066200976aa07ce25708484e5c2458182961280dc4637a7a6e98e1f3bb59fb7bf3bf26380f74c6ff1fd40d3dbf1143f21c721833199e45f669614a33b87 + languageName: node + linkType: hard + +"@emmetio/stream-reader@npm:^2.2.0": + version: 2.2.0 + resolution: "@emmetio/stream-reader@npm:2.2.0" + checksum: 10c0/d1a5c83b1ef062ce89b48e1fc170cd5f21b0dfa6132dc41421ef7eda455563227c047977cd4c7b70f072fa73b5af553b04cafe381c600df9bd17c58ca8238e8b + languageName: node + linkType: hard + "@emnapi/core@npm:1.11.1, @emnapi/core@npm:^1.4.3": version: 1.11.1 resolution: "@emnapi/core@npm:1.11.1" @@ -4950,6 +5478,16 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:2.0.0-alpha.3": + version: 2.0.0-alpha.3 + resolution: "@emnapi/core@npm:2.0.0-alpha.3" + dependencies: + "@emnapi/wasi-threads": "npm:2.0.1" + tslib: "npm:^2.4.0" + checksum: 10c0/dfc26f53eb40dab0e316206cabe01e6e98d21ef8a0b820f81ce7e0fa7b4307c94f2a9599c11e6c601ad7bd77e8efb9b7cb9f86dbc3b6237f799405e0cb60c587 + languageName: node + linkType: hard + "@emnapi/runtime@npm:1.11.1": version: 1.11.1 resolution: "@emnapi/runtime@npm:1.11.1" @@ -4959,6 +5497,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:2.0.0-alpha.3": + version: 2.0.0-alpha.3 + resolution: "@emnapi/runtime@npm:2.0.0-alpha.3" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/c8f6e0ad2f9c0ced8bc156a7ec3ab5fbbd2d3b7147b6a37537f6892131844b969d3aa450749807fc238160b5db61d6160b8066abfce4b2c40acfb1941bb31dd1 + languageName: node + linkType: hard + "@emnapi/runtime@npm:^1.11.1, @emnapi/runtime@npm:^1.4.3": version: 1.11.2 resolution: "@emnapi/runtime@npm:1.11.2" @@ -4977,6 +5524,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/wasi-threads@npm:2.0.1": + version: 2.0.1 + resolution: "@emnapi/wasi-threads@npm:2.0.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/5f7bf4cc4f6a1c31ec2084c9321d054f3b3b6c7e89430bb23fc3487d55e7be40f1ff31b2cb9a51721b444d4e8ac1f065b8749606771f6e4e7ccb32bbb709a303 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.13.5": version: 11.13.5 resolution: "@emotion/babel-plugin@npm:11.13.5" @@ -5197,6 +5753,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/android-arm64@npm:0.25.12" @@ -5211,6 +5774,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/android-arm@npm:0.25.12" @@ -5225,6 +5795,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/android-x64@npm:0.25.12" @@ -5239,6 +5816,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/darwin-arm64@npm:0.25.12" @@ -5253,6 +5837,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/darwin-x64@npm:0.25.12" @@ -5267,6 +5858,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/freebsd-arm64@npm:0.25.12" @@ -5281,6 +5879,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/freebsd-x64@npm:0.25.12" @@ -5295,6 +5900,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-arm64@npm:0.25.12" @@ -5309,6 +5921,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-arm@npm:0.25.12" @@ -5323,6 +5942,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-ia32@npm:0.25.12" @@ -5337,6 +5963,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-loong64@npm:0.25.12" @@ -5351,6 +5984,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-mips64el@npm:0.25.12" @@ -5365,6 +6005,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-ppc64@npm:0.25.12" @@ -5379,6 +6026,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-riscv64@npm:0.25.12" @@ -5393,6 +6047,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-s390x@npm:0.25.12" @@ -5407,6 +6068,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-x64@npm:0.25.12" @@ -5421,6 +6089,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/netbsd-arm64@npm:0.25.12" @@ -5435,6 +6110,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/netbsd-x64@npm:0.25.12" @@ -5449,6 +6131,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openbsd-arm64@npm:0.25.12" @@ -5463,6 +6152,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openbsd-x64@npm:0.25.12" @@ -5477,6 +6173,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openharmony-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openharmony-arm64@npm:0.25.12" @@ -5491,6 +6194,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/sunos-x64@npm:0.25.12" @@ -5505,6 +6215,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/win32-arm64@npm:0.25.12" @@ -5519,6 +6236,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/win32-ia32@npm:0.25.12" @@ -5533,6 +6257,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/win32-x64@npm:0.25.12" @@ -5547,6 +6278,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" @@ -5818,6 +6556,51 @@ __metadata: languageName: node linkType: hard +"@expressive-code/core@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/core@npm:0.44.1" + dependencies: + "@ctrl/tinycolor": "npm:^4.0.4" + hast-util-select: "npm:^6.0.2" + hast-util-to-html: "npm:^9.0.1" + hast-util-to-text: "npm:^4.0.1" + hastscript: "npm:^9.0.0" + postcss: "npm:^8.4.38" + postcss-nested: "npm:^6.0.1" + unist-util-visit: "npm:^5.0.0" + unist-util-visit-parents: "npm:^6.0.1" + checksum: 10c0/0e2bc031ce475b290e107ae61b4f915daebf2c7672d293d5f3c12dcc0c88f8c9b82d8649a0a9f5da7fdb8bc2a620c8f08d84f536a59c8ea0fc1777758db735a1 + languageName: node + linkType: hard + +"@expressive-code/plugin-frames@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/plugin-frames@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + checksum: 10c0/d32c6c4ae72439b4a5ac25688180d037df27ce3bc1a361e89732f487109afe8960b1b2a573c3d704bda2feb2e2a1e72828385abc6971d4c823ba3b06153bcd28 + languageName: node + linkType: hard + +"@expressive-code/plugin-shiki@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/plugin-shiki@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + shiki: "npm:^4.0.2" + checksum: 10c0/3e9fac7ddc3ebf8cf24579bae9a6767b92f5c87d8de3eb47c0dac1ed138070ef2a4148732b3cd63d178c8fe9bc25f46933228a5b2b44615e5ab453eb7e064afb + languageName: node + linkType: hard + +"@expressive-code/plugin-text-markers@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/plugin-text-markers@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + checksum: 10c0/78b8d2a98b931d78156830d3deeadbfd16fa1e221fe08a7ed8aca00c20663623f20496d1079529d70cea2660bf27c77783c4626797d77b8ce6932d02729096bd + languageName: node + linkType: hard + "@faker-js/faker@npm:10.4.0": version: 10.4.0 resolution: "@faker-js/faker@npm:10.4.0" @@ -6966,7 +7749,6 @@ __metadata: dependencies: "@types/node": "npm:22.18.13" "@typescript/native-preview": "npm:7.0.0-dev.20260511.1" - dependency-cruiser: "npm:18.0.0" elkjs: "npm:0.11.0" immer: "npm:10.1.3" oxlint: "npm:1.63.0" @@ -8731,6 +9513,25 @@ __metadata: languageName: unknown linkType: soft +"@local/petrinaut-arch-docs@workspace:*, @local/petrinaut-arch-docs@workspace:libs/@local/petrinaut-arch-docs": + version: 0.0.0-use.local + resolution: "@local/petrinaut-arch-docs@workspace:libs/@local/petrinaut-arch-docs" + dependencies: + "@hashintel/petrinaut": "workspace:*" + "@hashintel/petrinaut-core": "workspace:*" + "@local/tsconfig": "workspace:*" + "@types/js-yaml": "npm:^4" + "@types/node": "npm:22.18.13" + dependency-cruiser: "npm:18.0.0" + js-yaml: "npm:4.3.0" + oxlint: "npm:1.63.0" + tsx: "npm:4.20.6" + typescript: "npm:5.9.3" + vitest: "npm:4.1.10" + zod: "npm:4.4.3" + languageName: unknown + linkType: soft + "@local/petrinaut-optimizer-client@workspace:*, @local/petrinaut-optimizer-client@workspace:libs/@local/petrinaut-optimizer-client": version: 0.0.0-use.local resolution: "@local/petrinaut-optimizer-client@workspace:libs/@local/petrinaut-optimizer-client" @@ -8878,7 +9679,7 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.1.0": +"@mdx-js/mdx@npm:^3.1.0, @mdx-js/mdx@npm:^3.1.1": version: 3.1.1 resolution: "@mdx-js/mdx@npm:3.1.1" dependencies: @@ -9450,15 +10251,15 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.5, @napi-rs/wasm-runtime@npm:^1.1.6": - version: 1.1.6 - resolution: "@napi-rs/wasm-runtime@npm:1.1.6" +"@napi-rs/wasm-runtime@npm:^1.1.5, @napi-rs/wasm-runtime@npm:^1.1.6, @napi-rs/wasm-runtime@npm:^1.2.0": + version: 1.2.2 + resolution: "@napi-rs/wasm-runtime@npm:1.2.2" dependencies: "@tybys/wasm-util": "npm:^0.10.3" peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - checksum: 10c0/344518bf3ef65051dda4c00969f293aa4a21ab7dc7822b3f48519b17cd5eaa3f0bc34898d115d50ba59b1817a0cb905d46f7a7223c8249239cd14c28db388e10 + "@emnapi/core": ^1.7.1 || ^2.0.0-alpha.3 + "@emnapi/runtime": ^1.7.1 || ^2.0.0-alpha.3 + checksum: 10c0/670ff8359761660f58d95a29fe22ac959d2c295675144fe9bc35118b0e723d1e0ac192df9896ba428eb13930cf0893b632606b70ccaea259fd6eca1836c2eb09 languageName: node linkType: hard @@ -10153,6 +10954,13 @@ __metadata: languageName: node linkType: hard +"@oslojs/encoding@npm:^1.1.0": + version: 1.1.0 + resolution: "@oslojs/encoding@npm:1.1.0" + checksum: 10c0/5553a0974dca60e1a8b247b7b97abcb141cc7ee4e22444f424a07921d6a5f76a43c316f3ee669222787fdef6549f8749cc6d68ff5a631e2542521c56fe36417f + languageName: node + linkType: hard + "@oxc-project/types@npm:=0.137.0": version: 0.137.0 resolution: "@oxc-project/types@npm:0.137.0" @@ -10167,6 +10975,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.142.0": + version: 0.142.0 + resolution: "@oxc-project/types@npm:0.142.0" + checksum: 10c0/e4fa60b8fe1a77b0db6b9a2dcbc78d9a5a18ef64dffde01d909108f3ddbc562b876c568cb01e297ba71a256fc8aaafc928d4562475a9b2a25b276fee5bf635c3 + languageName: node + linkType: hard + "@oxfmt/binding-android-arm-eabi@npm:0.50.0": version: 0.50.0 resolution: "@oxfmt/binding-android-arm-eabi@npm:0.50.0" @@ -10475,6 +11290,62 @@ __metadata: languageName: node linkType: hard +"@pagefind/darwin-arm64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/darwin-arm64@npm:1.5.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@pagefind/darwin-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/darwin-x64@npm:1.5.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@pagefind/default-ui@npm:^1.3.0": + version: 1.5.2 + resolution: "@pagefind/default-ui@npm:1.5.2" + checksum: 10c0/2e4add8f315b6ceb2355b635bf1bbc1fc84e4d3d1dadafc38696184aff1d459720a8efcb843d58ae798a30512859620960a671311ff8e197d0a67b43c47fdeef + languageName: node + linkType: hard + +"@pagefind/freebsd-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/freebsd-x64@npm:1.5.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@pagefind/linux-arm64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/linux-arm64@npm:1.5.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@pagefind/linux-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/linux-x64@npm:1.5.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@pagefind/windows-arm64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/windows-arm64@npm:1.5.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@pagefind/windows-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/windows-x64@npm:1.5.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@pandacss/config@npm:1.11.1, @pandacss/config@npm:^1.11.1": version: 1.11.1 resolution: "@pandacss/config@npm:1.11.1" @@ -12573,6 +13444,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-android-arm64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-android-arm64@npm:1.2.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-darwin-arm64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-darwin-arm64@npm:1.1.2" @@ -12587,6 +13465,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-darwin-arm64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-darwin-arm64@npm:1.2.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-darwin-x64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-darwin-x64@npm:1.1.2" @@ -12601,6 +13486,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-darwin-x64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-darwin-x64@npm:1.2.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rolldown/binding-freebsd-x64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-freebsd-x64@npm:1.1.2" @@ -12615,6 +13507,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-freebsd-x64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-freebsd-x64@npm:1.2.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rolldown/binding-linux-arm-gnueabihf@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.1.2" @@ -12629,6 +13528,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm-gnueabihf@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.2.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@rolldown/binding-linux-arm64-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.1.2" @@ -12643,6 +13549,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm64-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.2.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-arm64-musl@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-arm64-musl@npm:1.1.2" @@ -12657,6 +13570,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm64-musl@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.2.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rolldown/binding-linux-ppc64-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.1.2" @@ -12671,6 +13591,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-ppc64-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.2.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-s390x-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.1.2" @@ -12685,6 +13612,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-s390x-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.2.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-x64-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-x64-gnu@npm:1.1.2" @@ -12699,6 +13633,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-x64-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.2.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-x64-musl@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-x64-musl@npm:1.1.2" @@ -12713,6 +13654,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-x64-musl@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.2.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rolldown/binding-openharmony-arm64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-openharmony-arm64@npm:1.1.2" @@ -12727,6 +13675,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-openharmony-arm64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.2.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-wasm32-wasi@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-wasm32-wasi@npm:1.1.2" @@ -12749,6 +13704,17 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-wasm32-wasi@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.2.1" + dependencies: + "@emnapi/core": "npm:2.0.0-alpha.3" + "@emnapi/runtime": "npm:2.0.0-alpha.3" + "@napi-rs/wasm-runtime": "npm:^1.2.0" + checksum: 10c0/e5bbfad1862187c60cd9cab802af22c71b2496f89fe73a161931f04594f9db1e09ad7a503a2f980f55a2f6d5a4d2013ad1d17260970ac170a1aa6b9286a41738 + languageName: node + linkType: hard + "@rolldown/binding-win32-arm64-msvc@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.1.2" @@ -12763,6 +13729,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-win32-arm64-msvc@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.2.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-win32-x64-msvc@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-win32-x64-msvc@npm:1.1.2" @@ -12777,6 +13750,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-win32-x64-msvc@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.2.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rolldown/plugin-babel@npm:0.2.1": version: 0.2.1 resolution: "@rolldown/plugin-babel@npm:0.2.1" @@ -12813,6 +13793,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-rc.3": + version: 1.0.0-rc.3 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.3" + checksum: 10c0/3928b6282a30f307d1b075d2f217180ae173ea9e00638ce46ab65f089bd5f7a0b2c488ae1ce530f509387793c656a2910337c4cd68fa9d37d7e439365989e699 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-rc.7": version: 1.0.0-rc.7 resolution: "@rolldown/pluginutils@npm:1.0.0-rc.7" @@ -14242,6 +15229,86 @@ __metadata: languageName: node linkType: hard +"@shikijs/core@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/core@npm:4.4.1" + dependencies: + "@shikijs/primitive": "npm:4.4.1" + "@shikijs/types": "npm:4.4.1" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + hast-util-to-html: "npm:^9.0.5" + checksum: 10c0/42c2027721db5e34dc660a77aa0e4223a830e597e5cf682a049b6a121c8640a3f7c4c4cfc423d957c6935af0904b14c0fb01ab9684e87e65a4dbfd35e0b6d485 + languageName: node + linkType: hard + +"@shikijs/engine-javascript@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/engine-javascript@npm:4.4.1" + dependencies: + "@shikijs/types": "npm:4.4.1" + "@shikijs/vscode-textmate": "npm:^10.0.2" + oniguruma-to-es: "npm:^4.3.6" + checksum: 10c0/5e4a9defb5ab07c1e5fa4a6e747778e97d66a76e9d78fd4e0fcf340016f6f08174627c98973b84eb553ff22803950674349f3821af58aa0dd029cd78f0590c2b + languageName: node + linkType: hard + +"@shikijs/engine-oniguruma@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/engine-oniguruma@npm:4.4.1" + dependencies: + "@shikijs/types": "npm:4.4.1" + "@shikijs/vscode-textmate": "npm:^10.0.2" + checksum: 10c0/eac78a9697a4310d4064c30f2699e784917ba23ef3a75d53d3483e62bfc52dbf9852ccaabd298c6d881ce1836c0fa7060e61e78922551e92953ff895fa490e70 + languageName: node + linkType: hard + +"@shikijs/langs@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/langs@npm:4.4.1" + dependencies: + "@shikijs/types": "npm:4.4.1" + checksum: 10c0/50edfaae8fd00d3f319a8de87380e7b12f29785403d655098b9182f86740e33d87080d0e39ce79e3a6dbab3c698249202ed09af52cb04999b795e3f158b08d3d + languageName: node + linkType: hard + +"@shikijs/primitive@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/primitive@npm:4.4.1" + dependencies: + "@shikijs/types": "npm:4.4.1" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + checksum: 10c0/7a0afce4b20fff41d18f614322604fe9495d214afa46b8621e838ff39d87ad037167f2deaa6a2cf7fb33fc9d735c8c0beed645f40bb79bcdf6668caec0c7f433 + languageName: node + linkType: hard + +"@shikijs/themes@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/themes@npm:4.4.1" + dependencies: + "@shikijs/types": "npm:4.4.1" + checksum: 10c0/a1a67c0e2815a24845e092c55a61112845d6d41a9eef376ca69b92640733bba7f1e5cc1acf5e43fa131d0558bb320863b970987197cc4b1b41c379dd34dbfe27 + languageName: node + linkType: hard + +"@shikijs/types@npm:4.4.1": + version: 4.4.1 + resolution: "@shikijs/types@npm:4.4.1" + dependencies: + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + checksum: 10c0/f3f742c3f34338795f6e4732964ea3a7e251bff5b9d586e0c4546531e3d7c4e9f2693f1ce3f4bed014fb8f38a15d9e7f1c1c2be678c601c91d885566b1b1f5f2 + languageName: node + linkType: hard + +"@shikijs/vscode-textmate@npm:^10.0.2": + version: 10.0.2 + resolution: "@shikijs/vscode-textmate@npm:10.0.2" + checksum: 10c0/36b682d691088ec244de292dc8f91b808f95c89466af421cf84cbab92230f03c8348649c14b3251991b10ce632b0c715e416e992dd5f28ff3221dc2693fd9462 + languageName: node + linkType: hard + "@sigma/edge-curve@npm:3.0.0-beta.16": version: 3.0.0-beta.16 resolution: "@sigma/edge-curve@npm:3.0.0-beta.16" @@ -16848,7 +17915,7 @@ __metadata: languageName: node linkType: hard -"@types/estree-jsx@npm:^1.0.0": +"@types/estree-jsx@npm:^1.0.0, @types/estree-jsx@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree-jsx@npm:1.0.5" dependencies: @@ -16991,12 +18058,12 @@ __metadata: languageName: node linkType: hard -"@types/hast@npm:^3.0.0": - version: 3.0.4 - resolution: "@types/hast@npm:3.0.4" +"@types/hast@npm:^3.0.0, @types/hast@npm:^3.0.4, @types/hast@npm:^3.0.5": + version: 3.0.5 + resolution: "@types/hast@npm:3.0.5" dependencies: "@types/unist": "npm:*" - checksum: 10c0/3249781a511b38f1d330fd1e3344eed3c4e7ea8eff82e835d35da78e637480d36fad37a78be5a7aed8465d237ad0446abc1150859d0fde395354ea634decf9f7 + checksum: 10c0/f3af8594a6903a507ed191eda944af18099198d6708c29102ae17118c3e20779f6929e91ed37e033541fd28d58055d8a5910a4366dbdf976cf81b13a464741fb languageName: node linkType: hard @@ -17054,7 +18121,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4": +"@types/js-yaml@npm:^4, @types/js-yaml@npm:^4.0.9": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 @@ -17198,7 +18265,7 @@ __metadata: languageName: node linkType: hard -"@types/mdast@npm:^4.0.0": +"@types/mdast@npm:^4.0.0, @types/mdast@npm:^4.0.4": version: 4.0.4 resolution: "@types/mdast@npm:4.0.4" dependencies: @@ -17251,6 +18318,15 @@ __metadata: languageName: node linkType: hard +"@types/nlcst@npm:^2.0.0": + version: 2.0.3 + resolution: "@types/nlcst@npm:2.0.3" + dependencies: + "@types/unist": "npm:*" + checksum: 10c0/d83549aaee59681ae8fa2a78d8a1b968a41eb7c0422773dff12acbf3661e4b2b2859740c3effdad9d0cd12ea14a0ec33ca302da12106476b627e09d2a029d3c1 + languageName: node + linkType: hard + "@types/node-fetch@npm:2.6.13": version: 2.6.13 resolution: "@types/node-fetch@npm:2.6.13" @@ -17261,12 +18337,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^24.0.13": - version: 24.10.13 - resolution: "@types/node@npm:24.10.13" +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^24.0.13, @types/node@npm:^24.9.2": + version: 24.13.3 + resolution: "@types/node@npm:24.13.3" dependencies: - undici-types: "npm:~7.16.0" - checksum: 10c0/4ff0b9b060b5477c0fec5b11a176f294be588104ab546295db65b17a92ba0a6077b52ad92dd3c0d2154198c7f9d0021e6c1d42b00c9ac7ebfd85632afbcc48a4 + undici-types: "npm:~7.18.0" + checksum: 10c0/a5bc08f49b9581dcdca90e02cd77197a3c799840807664942e5cd5161a553d4d2ae8064f7e3294410d748d7d098b5c1848be7fa50c06f29c4193ab16be4e59eb languageName: node linkType: hard @@ -17542,6 +18618,15 @@ __metadata: languageName: node linkType: hard +"@types/sax@npm:^1.2.1": + version: 1.2.7 + resolution: "@types/sax@npm:1.2.7" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d077a761a0753b079bf8279b3993948030ca86ed9125437b9b29c1de40db9b2deb7fddc369f014b58861d450e8b8cc75f163aa29dc8cea81952efbfd859168cf + languageName: node + linkType: hard + "@types/semver@npm:7.7.1": version: 7.7.1 resolution: "@types/semver@npm:7.7.1" @@ -17639,7 +18724,7 @@ __metadata: languageName: node linkType: hard -"@types/unist@npm:*, @types/unist@npm:^3.0.0": +"@types/unist@npm:*, @types/unist@npm:^3.0.0, @types/unist@npm:^3.0.3": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60 @@ -18488,6 +19573,22 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^5.2.0": + version: 5.2.0 + resolution: "@vitejs/plugin-react@npm:5.2.0" + dependencies: + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" + "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" + "@rolldown/pluginutils": "npm:1.0.0-rc.3" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.18.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/bac0a409e71eee954a05bc41580411c369bd5f9ef0586a1f9743fba76ad6603c437d93d407d230780015361f93d1592c55e53314813cded6369c36d3c1e8edbf + languageName: node + linkType: hard + "@vitest/browser-playwright@npm:4.1.10": version: 4.1.10 resolution: "@vitest/browser-playwright@npm:4.1.10" @@ -18703,30 +19804,94 @@ __metadata: languageName: node linkType: hard -"@volar/language-core@npm:2.4.23, @volar/language-core@npm:~2.4.11": - version: 2.4.23 - resolution: "@volar/language-core@npm:2.4.23" +"@volar/kit@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/kit@npm:2.4.28" dependencies: - "@volar/source-map": "npm:2.4.23" - checksum: 10c0/1b8d60c7c0faa29ef5ec46dd2b673227592d0697753767e4df088f7c2d93843828116fe59472bb9d604ba653400be32a538e985730844b1af4f42a7075e62049 + "@volar/language-service": "npm:2.4.28" + "@volar/typescript": "npm:2.4.28" + typesafe-path: "npm:^0.2.2" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + peerDependencies: + typescript: "*" + checksum: 10c0/c097b930eeb94d432ba7d21e0286ec787931d9fbf1c07e47e0df2d01dfa5a8d9413bac5c2fb4e342edfe57bec29b916427e62d40792ba39b614a2ff1fe3907a1 + languageName: node + linkType: hard + +"@volar/language-core@npm:2.4.28, @volar/language-core@npm:~2.4.11, @volar/language-core@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/language-core@npm:2.4.28" + dependencies: + "@volar/source-map": "npm:2.4.28" + checksum: 10c0/d41f7327fed7fa5301fbf2d8f96753d645a976b21dbbeb869794a780aa6523d1e6bf258242bc3d8ccd37f8e8b98a04fea9574e6f63badc585a8a3c2e068c4a86 + languageName: node + linkType: hard + +"@volar/language-server@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/language-server@npm:2.4.28" + dependencies: + "@volar/language-core": "npm:2.4.28" + "@volar/language-service": "npm:2.4.28" + "@volar/typescript": "npm:2.4.28" + path-browserify: "npm:^1.0.1" + request-light: "npm:^0.7.0" + vscode-languageserver: "npm:^9.0.1" + vscode-languageserver-protocol: "npm:^3.17.5" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/8266fdab264dbe449501e7d05e584069e6eb6cc5a06bf02968107c3fa32f8eb49b363d57d7abd76e5f637265469be8e13ebce1dd4b3e6ebdfae030691879d283 + languageName: node + linkType: hard + +"@volar/language-service@npm:2.4.28, @volar/language-service@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/language-service@npm:2.4.28" + dependencies: + "@volar/language-core": "npm:2.4.28" + vscode-languageserver-protocol: "npm:^3.17.5" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/a150d34f0c1c92641ff978eb6045ac6f0717cf16a51cfd3d0798267ccdc91aca21e6746403bf0254c747fa510ea92e3eb064acfa42320c0ebae959fd4a88add8 languageName: node linkType: hard -"@volar/source-map@npm:2.4.23": - version: 2.4.23 - resolution: "@volar/source-map@npm:2.4.23" - checksum: 10c0/08af690093b811d0a37bdd8d306755b4e7f1535b67625c26f6fa6eb9ae081e24c55dabc8231ce8856aa1b731a5ac137b3f0449b34c093923c3545afdbe462c7a +"@volar/source-map@npm:2.4.28": + version: 2.4.28 + resolution: "@volar/source-map@npm:2.4.28" + checksum: 10c0/24b0b02c7f66febe47f0bfda4a5ed4beaf949041eddc6325c7478b900faeb071795b696d97a4f326dde47217d06e40b67129300bc544f054772c5cb84c2f254e languageName: node linkType: hard -"@volar/typescript@npm:^2.4.11": - version: 2.4.23 - resolution: "@volar/typescript@npm:2.4.23" +"@volar/typescript@npm:2.4.28, @volar/typescript@npm:^2.4.11": + version: 2.4.28 + resolution: "@volar/typescript@npm:2.4.28" dependencies: - "@volar/language-core": "npm:2.4.23" + "@volar/language-core": "npm:2.4.28" path-browserify: "npm:^1.0.1" vscode-uri: "npm:^3.0.8" - checksum: 10c0/dbb449b66e627a75f8f6df98b3210c32edff62747a12d1e6237a6dc2a75f26432833d4d3646d6fbd60ed21fa52d7e342437377973b80cf4bbeacee1980ffd0cb + checksum: 10c0/075c890b9ec1cb17f17e38aaed035f8ee7d507439e87270d8e3c394356fc9387fd0bda9ec1069b36ea4c378d9375a08f5bc64c063a83427010ddd86d472124fc + languageName: node + linkType: hard + +"@vscode/emmet-helper@npm:^2.9.3": + version: 2.11.0 + resolution: "@vscode/emmet-helper@npm:2.11.0" + dependencies: + emmet: "npm:^2.4.3" + jsonc-parser: "npm:^2.3.0" + vscode-languageserver-textdocument: "npm:^1.0.1" + vscode-languageserver-types: "npm:^3.15.1" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/41c743258f958ff143f5152a7c8e8e1c6f96336f4147d537ace7c81337d5f9d7f93efd6df16c80e6d9506c51df9c8deb6440765a1deae219e31cdbead3cfd841 + languageName: node + linkType: hard + +"@vscode/l10n@npm:^0.0.18": + version: 0.0.18 + resolution: "@vscode/l10n@npm:0.0.18" + checksum: 10c0/d1fc797001f7d508ab3fa91175f7a50ea98516c4e47830ff2be79163cde9279279514a167a3bad15b7ab7fc243e7808d8f32d3eb41f4a7d6721d9dfdbb38d89e languageName: node linkType: hard @@ -20301,7 +21466,7 @@ __metadata: languageName: node linkType: hard -"ajv-draft-04@npm:~1.0.0": +"ajv-draft-04@npm:^1.0.0, ajv-draft-04@npm:~1.0.0": version: 1.0.0 resolution: "ajv-draft-04@npm:1.0.0" peerDependencies: @@ -20341,6 +21506,15 @@ __metadata: languageName: node linkType: hard +"ajv-i18n@npm:^4.2.0": + version: 4.2.0 + resolution: "ajv-i18n@npm:4.2.0" + peerDependencies: + ajv: ^8.0.0-beta.0 + checksum: 10c0/8c27882f2d46147600538aa8f48a633c72cd65baa37e0fae7159e832122b4ee504f4076f81ea3f78c15d7236a2d499d42a34d1a27b202066edc620f23963f6db + languageName: node + linkType: hard + "ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -20392,6 +21566,17 @@ __metadata: languageName: node linkType: hard +"am-i-vibing@npm:^0.4.0": + version: 0.4.0 + resolution: "am-i-vibing@npm:0.4.0" + dependencies: + process-ancestry: "npm:^0.1.0" + bin: + am-i-vibing: dist/cli.mjs + checksum: 10c0/af511ee621c78957e7e9e86b15498d64b9d73c54689e571a03531fe123002d3a5f7bd2d7fadda9cc07511c8f7cfdcc888d897bebeb12d2bc8b725161e64fd3f5 + languageName: node + linkType: hard + "anser@npm:^2.1.1": version: 2.3.2 resolution: "anser@npm:2.3.2" @@ -20523,7 +21708,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:~3.1.2": +"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -20556,6 +21741,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^5.0.0": + version: 5.0.2 + resolution: "arg@npm:5.0.2" + checksum: 10c0/ccaf86f4e05d342af6666c569f844bec426595c567d32a8289715087825c2ca7edd8a3d204e4d2fb2aa4602e09a57d0c13ea8c9eea75aac3dbb4af5514e6800e + languageName: node + linkType: hard + "argparse@npm:^1.0.7, argparse@npm:~1.0.3, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -20648,6 +21840,13 @@ __metadata: languageName: node linkType: hard +"array-iterate@npm:^2.0.0": + version: 2.0.1 + resolution: "array-iterate@npm:2.0.1" + checksum: 10c0/756c08334f95e290f03ab2141b034514af1311ef7b62f15b0f5ea6f8f3033ee9cc6a8f1c3e9ff4803d4d723cf992aa61460acf5fce884936972db966b1da287d + languageName: node + linkType: hard + "array-union@npm:^1.0.1": version: 1.0.2 resolution: "array-union@npm:1.0.2" @@ -20850,6 +22049,90 @@ __metadata: languageName: node linkType: hard +"astro-expressive-code@npm:^0.44.0": + version: 0.44.1 + resolution: "astro-expressive-code@npm:0.44.1" + dependencies: + rehype-expressive-code: "npm:^0.44.1" + url-extras: "npm:^0.1.0" + peerDependencies: + astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 + checksum: 10c0/52d57b7d0b8b0001a51e55f37eb82ade6dea93c53ca1455930c21b89f6e245a3e2660da75fbe924ea019f403cc2648675772ac0352f3a92f5486564e79a54a41 + languageName: node + linkType: hard + +"astro@npm:7.1.6": + version: 7.1.6 + resolution: "astro@npm:7.1.6" + dependencies: + "@astrojs/compiler-rs": "npm:^0.3.2" + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/markdown-satteri": "npm:0.3.5" + "@astrojs/telemetry": "npm:3.3.3" + "@capsizecss/unpack": "npm:^4.0.0" + "@clack/prompts": "npm:^1.1.0" + "@oslojs/encoding": "npm:^1.1.0" + "@rollup/pluginutils": "npm:^5.3.0" + am-i-vibing: "npm:^0.4.0" + aria-query: "npm:^5.3.2" + axobject-query: "npm:^4.1.0" + ci-info: "npm:^4.4.0" + clsx: "npm:^2.1.1" + common-ancestor-path: "npm:^2.0.0" + cookie: "npm:^2.0.1" + devalue: "npm:^5.8.1" + diff: "npm:^8.0.3" + dset: "npm:^3.1.4" + es-module-lexer: "npm:^2.0.0" + esbuild: "npm:^0.28.0" + flattie: "npm:^1.1.1" + fontace: "npm:~0.4.1" + get-tsconfig: "npm:5.0.0-beta.4" + github-slugger: "npm:^2.0.0" + html-escaper: "npm:3.0.3" + http-cache-semantics: "npm:^4.2.0" + js-yaml: "npm:^4.3.0" + jsonc-parser: "npm:^3.3.1" + magic-string: "npm:^1.0.0" + magicast: "npm:^0.5.2" + mrmime: "npm:^2.0.1" + neotraverse: "npm:^1.0.1" + obug: "npm:^2.1.1" + p-limit: "npm:^7.3.0" + p-queue: "npm:^9.1.0" + package-manager-detector: "npm:^1.6.0" + piccolore: "npm:^0.1.3" + picomatch: "npm:^4.0.4" + semver: "npm:^7.7.4" + sharp: "npm:^0.34.0 || ^0.35.0" + shiki: "npm:^4.0.2" + smol-toml: "npm:^1.6.0" + svgo: "npm:^4.0.1" + tinyclip: "npm:^0.1.12" + tinyexec: "npm:^1.0.4" + tinyglobby: "npm:^0.2.15" + ultrahtml: "npm:^1.6.0" + unifont: "npm:~0.7.4" + unstorage: "npm:^1.17.5" + vite: "npm:^8.0.13" + vitefu: "npm:^1.1.2" + xxhash-wasm: "npm:^1.1.0" + yargs-parser: "npm:^22.0.0" + zod: "npm:^4.3.6" + peerDependencies: + "@astrojs/markdown-remark": 7.2.2 + dependenciesMeta: + sharp: + optional: true + peerDependenciesMeta: + "@astrojs/markdown-remark": + optional: true + bin: + astro: ./bin/astro.mjs + checksum: 10c0/cdf83d5b19fa5fbb533bbcb7d8a9cef10f9e350ffe1585a5e0e79ead10915e73527270c3eb140ee8641754907e79df7e915a4547663833ab79cbe7d8bef4f590 + languageName: node + linkType: hard + "astrojs-compiler-sync@npm:^1.0.0": version: 1.0.1 resolution: "astrojs-compiler-sync@npm:1.0.1" @@ -21240,6 +22523,17 @@ __metadata: languageName: node linkType: hard +"bcp-47@npm:^2.1.0": + version: 2.1.1 + resolution: "bcp-47@npm:2.1.1" + dependencies: + is-alphabetical: "npm:^2.0.0" + is-alphanumerical: "npm:^2.0.0" + is-decimal: "npm:^2.0.0" + checksum: 10c0/086cb208c8bb0a2dad6ffcfc626ed9f81116037b92cecb63525410ccc943c1dfb78cb1b9ad41a8bcd239d98500933d1c4c44ae6903d5af428f738f2d743f8dec + languageName: node + linkType: hard + "better-opn@npm:^3.0.2": version: 3.0.2 resolution: "better-opn@npm:3.0.2" @@ -22023,6 +23317,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^5.0.0": + version: 5.0.0 + resolution: "chokidar@npm:5.0.0" + dependencies: + readdirp: "npm:^5.0.0" + checksum: 10c0/42fc907cb2a7ff5c9e220f84dae75380a77997f851c2a5e7865a2cf9ae45dd407a23557208cdcdbf3ac8c93341135a1748e4c48c31855f3bfa095e5159b6bdec + languageName: node + linkType: hard + "chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -22057,7 +23360,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^4.0.0, ci-info@npm:^4.3.1": +"ci-info@npm:^4.0.0, ci-info@npm:^4.3.1, ci-info@npm:^4.4.0": version: 4.4.0 resolution: "ci-info@npm:4.4.0" checksum: 10c0/44156201545b8dde01aa8a09ee2fe9fc7a73b1bef9adbd4606c9f61c8caeeb73fb7a575c88b0443f7b4edb5ee45debaa59ed54ba5f99698339393ca01349eb3a @@ -22509,6 +23812,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^11.1.0": + version: 11.1.0 + resolution: "commander@npm:11.1.0" + checksum: 10c0/13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179 + languageName: node + linkType: hard + "commander@npm:^12.0.0, commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" @@ -22558,6 +23868,13 @@ __metadata: languageName: node linkType: hard +"common-ancestor-path@npm:^2.0.0": + version: 2.0.0 + resolution: "common-ancestor-path@npm:2.0.0" + checksum: 10c0/fa0872dc8d5ffb2c0bb006d1f9e7ba4586773df4f0cf3dfa4b4c95710cedb8a78246fbbcc1392c71c882bd5428a2d003851bdd9033f549a445ac2c5deacb45ca + languageName: node + linkType: hard + "common-tags@npm:1.8.2": version: 1.8.2 resolution: "common-tags@npm:1.8.2" @@ -22773,6 +24090,13 @@ __metadata: languageName: node linkType: hard +"cookie-es@npm:^1.2.3": + version: 1.2.3 + resolution: "cookie-es@npm:1.2.3" + checksum: 10c0/429eae6f5130a7380ea024d787d7e1ecc644ca84f9c43dfb70f18761a831d2ba591d28f837ce350892cfff0857d711f3a4ad93a082637bceb478823c339c1a97 + languageName: node + linkType: hard + "cookie-signature@npm:^1.2.1": version: 1.2.2 resolution: "cookie-signature@npm:1.2.2" @@ -22801,6 +24125,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^2.0.1": + version: 2.0.1 + resolution: "cookie@npm:2.0.1" + checksum: 10c0/dcf3bd6ec9d0f4f32be7575e23242866cb53be55fc3d4cf36215638596661cec1327a3b4ca286ba15eba8f2abd443e422b8c5f708af4ea9f75b4d3dedc67ee4c + languageName: node + linkType: hard + "cookie@npm:~1.0.1": version: 1.0.2 resolution: "cookie@npm:1.0.2" @@ -22985,6 +24316,15 @@ __metadata: languageName: node linkType: hard +"crossws@npm:^0.3.5": + version: 0.3.5 + resolution: "crossws@npm:0.3.5" + dependencies: + uncrypto: "npm:^0.1.3" + checksum: 10c0/9e873546f0806606c4f775219f6811768fc3b3b0765ca8230722e849058ad098318af006e1faa39a8008c03009c37c519f6bccad41b0d78586237585c75fb38b + languageName: node + linkType: hard + "crypt@npm:0.0.2": version: 0.0.2 resolution: "crypt@npm:0.0.2" @@ -23068,6 +24408,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.0.1, css-tree@npm:^3.1.0": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + "css-tree@npm:~2.2.0": version: 2.2.1 resolution: "css-tree@npm:2.2.1" @@ -23632,6 +24982,13 @@ __metadata: languageName: node linkType: hard +"defu@npm:^6.1.6": + version: 6.1.7 + resolution: "defu@npm:6.1.7" + checksum: 10c0/e6635388103c8be3c574ac31302f6930e5e6eeedba32cb1b30cf993c7d9fb571aec2485446dfa23bfa63e55e66156fe109027a9695db82a50f931e91e8d4bedb + languageName: node + linkType: hard + "degenerator@npm:7.0.1": version: 7.0.1 resolution: "degenerator@npm:7.0.1" @@ -23767,6 +25124,13 @@ __metadata: languageName: node linkType: hard +"destr@npm:^2.0.5": + version: 2.0.5 + resolution: "destr@npm:2.0.5" + checksum: 10c0/efabffe7312a45ad90d79975376be958c50069f1156b94c181199763a7f971e113bd92227c26b94a169c71ca7dbc13583b7e96e5164743969fc79e1ff153e646 + languageName: node + linkType: hard + "destroy@npm:1.2.0, destroy@npm:^1.0.4, destroy@npm:~1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -23811,6 +25175,13 @@ __metadata: languageName: node linkType: hard +"devalue@npm:^5.8.1": + version: 5.9.0 + resolution: "devalue@npm:5.9.0" + checksum: 10c0/0ad4cc8e041747fdda6900bccf8648b68ec035fdaf72dd8d30b69295108b704e3078e515921758745139ce64dbd5bdc80e3c9491e4a3205926adcc95a2c22907 + languageName: node + linkType: hard + "devlop@npm:^1.0.0, devlop@npm:^1.1.0": version: 1.1.0 resolution: "devlop@npm:1.1.0" @@ -23827,10 +25198,10 @@ __metadata: languageName: node linkType: hard -"diff@npm:~8.0.2": - version: 8.0.3 - resolution: "diff@npm:8.0.3" - checksum: 10c0/d29321c70d3545fdcb56c5fdd76028c3f04c012462779e062303d4c3c531af80d2c360c26b871e6e2b9a971d2422d47e1779a859106c4cac4b5d2d143df70e20 +"diff@npm:^8.0.3, diff@npm:~8.0.2": + version: 8.0.4 + resolution: "diff@npm:8.0.4" + checksum: 10c0/7ee5d03926db4039be7252ac3b0abaae1bd122a2ca971e5ca7270e444e36ff83dd906fad1a719740ca347e97ed5dc8f458a76a8391dbcd7aff363bdafb348a00 languageName: node linkType: hard @@ -24057,6 +25428,13 @@ __metadata: languageName: node linkType: hard +"dset@npm:^3.1.4": + version: 3.1.4 + resolution: "dset@npm:3.1.4" + checksum: 10c0/b67bbd28dd8a539e90c15ffb61100eb64ef995c5270a124d4f99bbb53f4d82f55a051b731ba81f3215dd9dce2b4c8d69927dc20b3be1c5fc88bab159467aa438 + languageName: node + linkType: hard + "dts-resolver@npm:^3.0.0": version: 3.0.0 resolution: "dts-resolver@npm:3.0.0" @@ -24207,6 +25585,16 @@ __metadata: languageName: node linkType: hard +"emmet@npm:^2.4.3": + version: 2.4.11 + resolution: "emmet@npm:2.4.11" + dependencies: + "@emmetio/abbreviation": "npm:^2.3.3" + "@emmetio/css-abbreviation": "npm:^2.1.8" + checksum: 10c0/4099d9d0d5dee766603c4ea03e1b87296bd397a0e8c6d8d5d6dcfdaad3e4581df5d48939a00eb4437dc08c83e857e231222ee037cb34ad63b1f2cce4041c6fc2 + languageName: node + linkType: hard + "emoji-mart@npm:5.6.0": version: 5.6.0 resolution: "emoji-mart@npm:5.6.0" @@ -24845,6 +26233,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.28.0": + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb + languageName: node + linkType: hard + "escalade@npm:3.2.0, escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -25959,7 +27436,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": +"eventemitter3@npm:^5.0.1, eventemitter3@npm:^5.0.4": version: 5.0.4 resolution: "eventemitter3@npm:5.0.4" checksum: 10c0/575b8cac8d709e1473da46f8f15ef311b57ff7609445a7c71af5cd42598583eee6f098fa7a593e30f27e94b8865642baa0689e8fa97c016f742abdb3b1bf6d9a @@ -26211,6 +27688,18 @@ __metadata: languageName: node linkType: hard +"expressive-code@npm:^0.44.1": + version: 0.44.1 + resolution: "expressive-code@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + "@expressive-code/plugin-frames": "npm:^0.44.1" + "@expressive-code/plugin-shiki": "npm:^0.44.1" + "@expressive-code/plugin-text-markers": "npm:^0.44.1" + checksum: 10c0/ca09029718c687bfa494f095a0cc78b5981adc1e19d89a2740b2b7e07b326b205371018d894bfe8abe477cf842b015ef78367ecd575cbb8f2defd6d4910a7967 + languageName: node + linkType: hard + "exsolve@npm:^1.0.7": version: 1.0.8 resolution: "exsolve@npm:1.0.8" @@ -26344,6 +27833,22 @@ __metadata: languageName: node linkType: hard +"fast-string-truncated-width@npm:^3.0.2": + version: 3.0.3 + resolution: "fast-string-truncated-width@npm:3.0.3" + checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981 + languageName: node + linkType: hard + +"fast-string-width@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-string-width@npm:3.0.2" + dependencies: + fast-string-truncated-width: "npm:^3.0.2" + checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7 + languageName: node + linkType: hard + "fast-unique-numbers@npm:^8.0.13": version: 8.0.13 resolution: "fast-unique-numbers@npm:8.0.13" @@ -26361,6 +27866,15 @@ __metadata: languageName: node linkType: hard +"fast-wrap-ansi@npm:^0.2.0": + version: 0.2.2 + resolution: "fast-wrap-ansi@npm:0.2.2" + dependencies: + fast-string-width: "npm:^3.0.2" + checksum: 10c0/1aa7be4f7cb86f4bdb14691cb6bcc0b8df8b3b89df142ade3ae1602332dcf6f990cd750a923cd581ca0847808cb4ec1aa5afaafa7a72f849e87a2a62c98fa370 + languageName: node + linkType: hard + "fast-xml-builder@npm:^1.1.5": version: 1.2.0 resolution: "fast-xml-builder@npm:1.2.0" @@ -26693,6 +28207,13 @@ __metadata: languageName: node linkType: hard +"flattie@npm:^1.1.1": + version: 1.1.1 + resolution: "flattie@npm:1.1.1" + checksum: 10c0/a8f8242c7af126cb2f1aa4a067af338fce609fc4c4df183c626fcc70a46c1878ce4aa88cd0dc8ef8f583ad4e7088a3b11ebeb6a62c9c97d75c0b1b0f08182ee3 + languageName: node + linkType: hard + "fn.name@npm:1.x.x": version: 1.1.0 resolution: "fn.name@npm:1.1.0" @@ -26710,6 +28231,15 @@ __metadata: languageName: node linkType: hard +"fontace@npm:~0.4.1": + version: 0.4.1 + resolution: "fontace@npm:0.4.1" + dependencies: + fontkitten: "npm:^1.0.2" + checksum: 10c0/2b3b8bc60192b0e9d87c369f1d72bf5da6c5b0f6055d5eeb0c06693455c55395f9592844273035399289f410202306c1d7f7329312e6e42a2694912d39c70709 + languageName: node + linkType: hard + "fontfaceobserver@npm:2.1.0": version: 2.1.0 resolution: "fontfaceobserver@npm:2.1.0" @@ -26717,6 +28247,15 @@ __metadata: languageName: node linkType: hard +"fontkitten@npm:^1.0.2, fontkitten@npm:^1.0.3": + version: 1.0.3 + resolution: "fontkitten@npm:1.0.3" + dependencies: + tiny-inflate: "npm:^1.0.3" + checksum: 10c0/edee7bde2f824a778cf401a2019ada63376fc8bcc5303d09a11e8a9245f35bd945934abea04aef1ca52e8dbb6ce3a17fe9ecdfa52cc0c92f56b25709fb865a76 + languageName: node + linkType: hard + "for-each@npm:^0.3.3, for-each@npm:^0.3.5": version: 0.3.5 resolution: "for-each@npm:0.3.5" @@ -27200,6 +28739,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:5.0.0-beta.4": + version: 5.0.0-beta.4 + resolution: "get-tsconfig@npm:5.0.0-beta.4" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/6b908fc5d2abc937fca6938f5e9218238f49b11cd07362831383aa5815b25ce6f5fff36d4bcd70caa17778a54922b654745ce6c0b1bb19be8eac87da4e3fc087 + languageName: node + linkType: hard + "get-tsconfig@npm:5.0.0-beta.5": version: 5.0.0-beta.5 resolution: "get-tsconfig@npm:5.0.0-beta.5" @@ -27248,6 +28796,13 @@ __metadata: languageName: node linkType: hard +"github-slugger@npm:^2.0.0": + version: 2.0.0 + resolution: "github-slugger@npm:2.0.0" + checksum: 10c0/21b912b6b1e48f1e5a50b2292b48df0ff6abeeb0691b161b3d93d84f4ae6b1acd6ae23702e914af7ea5d441c096453cf0f621b72d57893946618d21dd1a1c486 + languageName: node + linkType: hard + "glob-parent@npm:6.0.2, glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -27835,6 +29390,23 @@ __metadata: languageName: node linkType: hard +"h3@npm:^1.15.10": + version: 1.15.11 + resolution: "h3@npm:1.15.11" + dependencies: + cookie-es: "npm:^1.2.3" + crossws: "npm:^0.3.5" + defu: "npm:^6.1.6" + destr: "npm:^2.0.5" + iron-webcrypto: "npm:^1.2.1" + node-mock-http: "npm:^1.0.4" + radix3: "npm:^1.1.2" + ufo: "npm:^1.6.3" + uncrypto: "npm:^0.1.3" + checksum: 10c0/6ccb421b9f92e02e6330c2b6697b18ef18e9550e4a1708f224ca517c40ecd201cd00967d0feb26e718595e86e985edec1755933cf8792d34fb8504f1c7cc261d + languageName: node + linkType: hard + "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -27955,6 +29527,45 @@ __metadata: languageName: node linkType: hard +"hast-util-embedded@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-embedded@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + checksum: 10c0/054c3d3b96fcd5c1d1c6f8d38ce1f7f33022ba6362129a022673d0b539f876acdcababbb9df29812fb927294f98ef7a2f44519a80d637fe3eea1819c9e69eeac + languageName: node + linkType: hard + +"hast-util-format@npm:^1.0.0": + version: 1.1.0 + resolution: "hast-util-format@npm:1.1.0" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-embedded: "npm:^3.0.0" + hast-util-minify-whitespace: "npm:^1.0.0" + hast-util-phrasing: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + html-whitespace-sensitive-tag-names: "npm:^3.0.0" + unist-util-visit-parents: "npm:^6.0.0" + checksum: 10c0/6ab223cffe8a524ef4f2564d0385cab174a52551513b318496b3776e6882594eab4810b0f8d90f20e8291fa4e87fa068e03cba316d83c0836dab12dd140e98df + languageName: node + linkType: hard + +"hast-util-from-html@npm:^2.0.0, hast-util-from-html@npm:^2.0.3": + version: 2.0.3 + resolution: "hast-util-from-html@npm:2.0.3" + dependencies: + "@types/hast": "npm:^3.0.0" + devlop: "npm:^1.1.0" + hast-util-from-parse5: "npm:^8.0.0" + parse5: "npm:^7.0.0" + vfile: "npm:^6.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10c0/993ef707c1a12474c8d4094fc9706a72826c660a7e308ea54c50ad893353d32e139b7cbc67510c2e82feac572b320e3b05aeb13d0f9c6302d61261f337b46764 + languageName: node + linkType: hard + "hast-util-from-parse5@npm:^8.0.0": version: 8.0.3 resolution: "hast-util-from-parse5@npm:8.0.3" @@ -27980,6 +29591,37 @@ __metadata: languageName: node linkType: hard +"hast-util-is-body-ok-link@npm:^3.0.0": + version: 3.0.1 + resolution: "hast-util-is-body-ok-link@npm:3.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/c320cbd9a9a834b007a6f2f8c271e98b8331c0193adf06e0a7c5ea0acae664e97ce28eb4436e0658bc5cdb8f47390ec1c6cba7c4fe1ded10951fcdd1432f60bf + languageName: node + linkType: hard + +"hast-util-is-element@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-is-element@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/f5361e4c9859c587ca8eb0d8343492f3077ccaa0f58a44cd09f35d5038f94d65152288dcd0c19336ef2c9491ec4d4e45fde2176b05293437021570aa0bc3613b + languageName: node + linkType: hard + +"hast-util-minify-whitespace@npm:^1.0.0": + version: 1.0.1 + resolution: "hast-util-minify-whitespace@npm:1.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-embedded: "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/20a7d64947e080463084f444ad09c7f28c40e7648ca2d9c6c036e42a67f8e945d352560ff599304c988257c1e477abcf6a1f508c0900211fa58ec1ba21b36533 + languageName: node + linkType: hard + "hast-util-parse-selector@npm:^4.0.0": version: 4.0.0 resolution: "hast-util-parse-selector@npm:4.0.0" @@ -27989,6 +29631,19 @@ __metadata: languageName: node linkType: hard +"hast-util-phrasing@npm:^3.0.0": + version: 3.0.1 + resolution: "hast-util-phrasing@npm:3.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-embedded: "npm:^3.0.0" + hast-util-has-property: "npm:^3.0.0" + hast-util-is-body-ok-link: "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + checksum: 10c0/d77e186ea3d7d62f6db9c4a55c3e6d9f1f6affd5f40250e8de9d73f167ae19fcc02fafe1601dfbe36e90f76ed5013ac004f0b6b398aee3a04a7a81de12788600 + languageName: node + linkType: hard + "hast-util-raw@npm:^9.0.0": version: 9.1.0 resolution: "hast-util-raw@npm:9.1.0" @@ -28010,7 +29665,7 @@ __metadata: languageName: node linkType: hard -"hast-util-select@npm:^6.0.0": +"hast-util-select@npm:^6.0.0, hast-util-select@npm:^6.0.2, hast-util-select@npm:^6.0.4": version: 6.0.4 resolution: "hast-util-select@npm:6.0.4" dependencies: @@ -28057,6 +29712,25 @@ __metadata: languageName: node linkType: hard +"hast-util-to-html@npm:^9.0.0, hast-util-to-html@npm:^9.0.1, hast-util-to-html@npm:^9.0.5": + version: 9.0.5 + resolution: "hast-util-to-html@npm:9.0.5" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + ccount: "npm:^2.0.0" + comma-separated-tokens: "npm:^2.0.0" + hast-util-whitespace: "npm:^3.0.0" + html-void-elements: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + property-information: "npm:^7.0.0" + space-separated-tokens: "npm:^2.0.0" + stringify-entities: "npm:^4.0.0" + zwitch: "npm:^2.0.4" + checksum: 10c0/b7a08c30bab4371fc9b4a620965c40b270e5ae7a8e94cf885f43b21705179e28c8e43b39c72885d1647965fb3738654e6962eb8b58b0c2a84271655b4d748836 + languageName: node + linkType: hard + "hast-util-to-jsx-runtime@npm:^2.0.0": version: 2.3.6 resolution: "hast-util-to-jsx-runtime@npm:2.3.6" @@ -28095,7 +29769,7 @@ __metadata: languageName: node linkType: hard -"hast-util-to-string@npm:^3.0.0": +"hast-util-to-string@npm:^3.0.0, hast-util-to-string@npm:^3.0.1": version: 3.0.1 resolution: "hast-util-to-string@npm:3.0.1" dependencies: @@ -28104,6 +29778,18 @@ __metadata: languageName: node linkType: hard +"hast-util-to-text@npm:^4.0.1, hast-util-to-text@npm:^4.0.2": + version: 4.0.2 + resolution: "hast-util-to-text@npm:4.0.2" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + unist-util-find-after: "npm:^5.0.0" + checksum: 10c0/93ecc10e68fe5391c6e634140eb330942e71dea2724c8e0c647c73ed74a8ec930a4b77043b5081284808c96f73f2bee64ee416038ece75a63a467e8d14f09946 + languageName: node + linkType: hard + "hast-util-whitespace@npm:^3.0.0": version: 3.0.0 resolution: "hast-util-whitespace@npm:3.0.0" @@ -28113,7 +29799,7 @@ __metadata: languageName: node linkType: hard -"hastscript@npm:^9.0.0": +"hastscript@npm:^9.0.0, hastscript@npm:^9.0.1": version: 9.0.1 resolution: "hastscript@npm:9.0.1" dependencies: @@ -28310,6 +29996,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:3.0.3": + version: 3.0.3 + resolution: "html-escaper@npm:3.0.3" + checksum: 10c0/a042fa4139127ff7546513e90ea39cc9161a1938ce90122dbc4260d4b7252c9aa8452f4509c0c2889901b8ae9a8699179150f1f99d3f80bcf7317573c5f08f4e + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -28442,6 +30135,13 @@ __metadata: languageName: node linkType: hard +"html-whitespace-sensitive-tag-names@npm:^3.0.0": + version: 3.0.1 + resolution: "html-whitespace-sensitive-tag-names@npm:3.0.1" + checksum: 10c0/da06cad111f6a432edd85c6cd09f6b5abbb385872fba79f23f939bdd4626920ac2e62507f604ef94eb8449902033bda292774624e3283b4dea4ed1620a2be3b2 + languageName: node + linkType: hard + "html@npm:^1.0.0": version: 1.0.0 resolution: "html@npm:1.0.0" @@ -28499,10 +30199,10 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": - version: 4.1.1 - resolution: "http-cache-semantics@npm:4.1.1" - checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc +"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1, http-cache-semantics@npm:^4.2.0": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 languageName: node linkType: hard @@ -28763,6 +30463,18 @@ __metadata: languageName: node linkType: hard +"i18next@npm:^26.0.7": + version: 26.3.6 + resolution: "i18next@npm:26.3.6" + peerDependencies: + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/5920ac8fb6b647a2bdd439d121de04e5297246e52c4d95d13f5faae0824fd45b2faeb66038bcd61fc62362daa815f9c32fb992d6faa3787607d1dfe768e9a8b1 + languageName: node + linkType: hard + "iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -29131,6 +30843,13 @@ __metadata: languageName: node linkType: hard +"iron-webcrypto@npm:^1.2.1": + version: 1.2.1 + resolution: "iron-webcrypto@npm:1.2.1" + checksum: 10c0/5cf27c6e2bd3ef3b4970e486235fd82491ab8229e2ed0ac23307c28d6c80d721772a86ed4e9fe2a5cabadd710c2f024b706843b40561fb83f15afee58f809f66 + languageName: node + linkType: hard + "is-absolute@npm:^1.0.0": version: 1.0.0 resolution: "is-absolute@npm:1.0.0" @@ -29325,6 +31044,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^4.0.0": + version: 4.0.0 + resolution: "is-docker@npm:4.0.0" + bin: + is-docker: cli.js + checksum: 10c0/4ee05c305b545422b172cab17f42900b940a5fa77820380d3244784fde69279d6881305f249b3b7fa2e261c2294804100548f5638880842e5b43df72cec29087 + languageName: node + linkType: hard + "is-electron@npm:^2.2.0": version: 2.2.2 resolution: "is-electron@npm:2.2.2" @@ -30306,7 +32034,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.3.0, js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": +"js-yaml@npm:4.3.0, js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1, js-yaml@npm:^4.3.0": version: 4.3.0 resolution: "js-yaml@npm:4.3.0" dependencies: @@ -30571,6 +32299,20 @@ __metadata: languageName: node linkType: hard +"jsonc-parser@npm:^2.3.0": + version: 2.3.1 + resolution: "jsonc-parser@npm:2.3.1" + checksum: 10c0/b5e823612f6518a4d35e65d3c642e87b994c52a71b6d83d306d59f9b57003a1f6c64659808f0f1c3448991c28916d56faca45222f31ddb1a32effecdef0f0485 + languageName: node + linkType: hard + +"jsonc-parser@npm:^3.0.0, jsonc-parser@npm:^3.3.1": + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 10c0/269c3ae0a0e4f907a914bf334306c384aabb9929bd8c99f909275ebd5c2d3bc70b9bcd119ad794f339dec9f24b6a4ee9cd5a8ab2e6435e730ad4075388fc2ab6 + languageName: node + linkType: hard + "jsondiffpatch@npm:0.7.2": version: 0.7.2 resolution: "jsondiffpatch@npm:0.7.2" @@ -30725,7 +32467,7 @@ __metadata: languageName: node linkType: hard -"kleur@npm:4.1.5": +"kleur@npm:4.1.5, kleur@npm:^4.1.5": version: 4.1.5 resolution: "kleur@npm:4.1.5" checksum: 10c0/e9de6cb49657b6fa70ba2d1448fd3d691a5c4370d8f7bbf1c2f64c24d461270f2117e1b0afe8cb3114f13bbd8e51de158c2a224953960331904e636a5e4c0f2a @@ -30739,6 +32481,13 @@ __metadata: languageName: node linkType: hard +"klona@npm:^2.0.6": + version: 2.0.6 + resolution: "klona@npm:2.0.6" + checksum: 10c0/94eed2c6c2ce99f409df9186a96340558897b3e62a85afdc1ee39103954d2ebe1c1c4e9fe2b0952771771fa96d70055ede8b27962a7021406374fdb695fd4d01 + languageName: node + linkType: hard + "koa-compose@npm:^4.1.0": version: 4.1.0 resolution: "koa-compose@npm:4.1.0" @@ -31024,9 +32773,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-android-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-android-arm64@npm:1.32.0" +"lightningcss-android-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-android-arm64@npm:1.33.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -31038,9 +32787,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-arm64@npm:1.32.0" +"lightningcss-darwin-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-arm64@npm:1.33.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -31052,9 +32801,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-x64@npm:1.32.0" +"lightningcss-darwin-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-x64@npm:1.33.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -31066,9 +32815,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-freebsd-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-freebsd-x64@npm:1.32.0" +"lightningcss-freebsd-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-freebsd-x64@npm:1.33.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -31080,9 +32829,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-arm-gnueabihf@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" +"lightningcss-linux-arm-gnueabihf@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.33.0" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -31094,9 +32843,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-arm64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" +"lightningcss-linux-arm64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.33.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -31108,9 +32857,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-arm64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" +"lightningcss-linux-arm64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.33.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -31122,9 +32871,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-x64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" +"lightningcss-linux-x64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.33.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -31136,9 +32885,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-x64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-musl@npm:1.32.0" +"lightningcss-linux-x64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-musl@npm:1.33.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -31150,9 +32899,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-win32-arm64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" +"lightningcss-win32-arm64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.33.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -31164,9 +32913,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-win32-x64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" +"lightningcss-win32-x64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.33.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -31214,22 +32963,22 @@ __metadata: languageName: node linkType: hard -"lightningcss@npm:^1.32.0": - version: 1.32.0 - resolution: "lightningcss@npm:1.32.0" +"lightningcss@npm:^1.32.0, lightningcss@npm:^1.33.0": + version: 1.33.0 + resolution: "lightningcss@npm:1.33.0" dependencies: detect-libc: "npm:^2.0.3" - lightningcss-android-arm64: "npm:1.32.0" - lightningcss-darwin-arm64: "npm:1.32.0" - lightningcss-darwin-x64: "npm:1.32.0" - lightningcss-freebsd-x64: "npm:1.32.0" - lightningcss-linux-arm-gnueabihf: "npm:1.32.0" - lightningcss-linux-arm64-gnu: "npm:1.32.0" - lightningcss-linux-arm64-musl: "npm:1.32.0" - lightningcss-linux-x64-gnu: "npm:1.32.0" - lightningcss-linux-x64-musl: "npm:1.32.0" - lightningcss-win32-arm64-msvc: "npm:1.32.0" - lightningcss-win32-x64-msvc: "npm:1.32.0" + lightningcss-android-arm64: "npm:1.33.0" + lightningcss-darwin-arm64: "npm:1.33.0" + lightningcss-darwin-x64: "npm:1.33.0" + lightningcss-freebsd-x64: "npm:1.33.0" + lightningcss-linux-arm-gnueabihf: "npm:1.33.0" + lightningcss-linux-arm64-gnu: "npm:1.33.0" + lightningcss-linux-arm64-musl: "npm:1.33.0" + lightningcss-linux-x64-gnu: "npm:1.33.0" + lightningcss-linux-x64-musl: "npm:1.33.0" + lightningcss-win32-arm64-msvc: "npm:1.33.0" + lightningcss-win32-x64-msvc: "npm:1.33.0" dependenciesMeta: lightningcss-android-arm64: optional: true @@ -31253,7 +33002,7 @@ __metadata: optional: true lightningcss-win32-x64-msvc: optional: true - checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 + checksum: 10c0/ce1f8279fbae636dbf37fa6e7385d5f98ed881d72af3362f24afbd4685e19c1fcdfecf17e5dd77f2ebee3d0c23ade276230d85842d07292229a2cffba8ff20a3 languageName: node linkType: hard @@ -31723,10 +33472,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": - version: 11.2.6 - resolution: "lru-cache@npm:11.2.6" - checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.7": + version: 11.5.2 + resolution: "lru-cache@npm:11.5.2" + checksum: 10c0/ece1ad731f5b655e85d67047d04bfc13823dc77aa61c5454924a9869ba600a0104e39cc33d726021feef812bc347ca34a5608a5eb1972a5dd0870b7ecd42c3f2 languageName: node linkType: hard @@ -31805,6 +33554,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^1.0.0": + version: 1.1.0 + resolution: "magic-string@npm:1.1.0" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/f904dee2fd190ee5450c754234d8a831228677e9092ed2e545e42746beecf18d617156eb911af55a14496ebe1e60179678c4080c919f74eca7738eba3f38f13a + languageName: node + linkType: hard + "magicast@npm:^0.5.2": version: 0.5.3 resolution: "magicast@npm:0.5.3" @@ -31974,6 +33732,34 @@ __metadata: languageName: node linkType: hard +"mdast-util-definitions@npm:^6.0.0": + version: 6.0.0 + resolution: "mdast-util-definitions@npm:6.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/a2e0e51122a3eff4f35379de2c50ee3d8a89bea58488a390b1b40ada95727eb769f87d4bc885e5935d61820d19e0567bc047876db302a2139f3a29668b612b80 + languageName: node + linkType: hard + +"mdast-util-directive@npm:^3.0.0, mdast-util-directive@npm:^3.1.0": + version: 3.1.0 + resolution: "mdast-util-directive@npm:3.1.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + ccount: "npm:^2.0.0" + devlop: "npm:^1.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + parse-entities: "npm:^4.0.0" + stringify-entities: "npm:^4.0.0" + unist-util-visit-parents: "npm:^6.0.0" + checksum: 10c0/596b093b940197cf43af4d0de12e82a1d2b1eb5add73dd16077aa80e0d0e1f208ea642c420726e59ccd352c193d6ecd5c106d6fab769f252617c75333f91a314 + languageName: node + linkType: hard + "mdast-util-find-and-replace@npm:^3.0.0": version: 3.0.1 resolution: "mdast-util-find-and-replace@npm:3.0.1" @@ -32171,7 +33957,7 @@ __metadata: languageName: node linkType: hard -"mdast-util-to-markdown@npm:^2.0.0": +"mdast-util-to-markdown@npm:^2.0.0, mdast-util-to-markdown@npm:^2.1.2": version: 2.1.2 resolution: "mdast-util-to-markdown@npm:2.1.2" dependencies: @@ -32211,6 +33997,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -32389,6 +34182,21 @@ __metadata: languageName: node linkType: hard +"micromark-extension-directive@npm:^4.0.0": + version: 4.0.0 + resolution: "micromark-extension-directive@npm:4.0.0" + dependencies: + devlop: "npm:^1.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-factory-whitespace: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + parse-entities: "npm:^4.0.0" + checksum: 10c0/b4aef0f44339543466ae186130a4514985837b6b12d0c155bd1162e740f631e58f0883a39d0c723206fa0ff53a9b579965c79116f902236f6f123c3340b5fefb + languageName: node + linkType: hard + "micromark-extension-gfm-autolink-literal@npm:^2.0.0": version: 2.1.0 resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0" @@ -33271,10 +35079,10 @@ __metadata: languageName: node linkType: hard -"mrmime@npm:^2.0.0": - version: 2.0.0 - resolution: "mrmime@npm:2.0.0" - checksum: 10c0/312b35ed288986aec90955410b21ed7427fd1e4ee318cb5fc18765c8d029eeded9444faa46589e5b1ed6b35fb2054a802ac8dcb917ddf6b3e189cb3bf11a965c +"mrmime@npm:^2.0.0, mrmime@npm:^2.0.1": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 languageName: node linkType: hard @@ -33583,6 +35391,13 @@ __metadata: languageName: node linkType: hard +"neotraverse@npm:^1.0.1": + version: 1.0.1 + resolution: "neotraverse@npm:1.0.1" + checksum: 10c0/98e5d2d1974f91d527851a9dcfceb62701a1d31970b324b9f35fbdd244d173edc7a379ea1773f64771a9dc282486e3d3b693b24e14b1e969291612b2ff6b5054 + languageName: node + linkType: hard + "netmask@npm:^2.0.2": version: 2.0.2 resolution: "netmask@npm:2.0.2" @@ -33695,6 +35510,15 @@ __metadata: languageName: node linkType: hard +"nlcst-to-string@npm:^4.0.0": + version: 4.0.0 + resolution: "nlcst-to-string@npm:4.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + checksum: 10c0/a192c8b3365a7c076812004e72ae5b4a1734e582be2a6f3c062f3beecf18868a9fe2d1bad870bfead320fb39830f2c4f3752e5ae6574c4e59157126fd1ddba70 + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -33739,6 +35563,13 @@ __metadata: languageName: node linkType: hard +"node-fetch-native@npm:^1.6.7": + version: 1.6.7 + resolution: "node-fetch-native@npm:1.6.7" + checksum: 10c0/8b748300fb053d21ca4d3db9c3ff52593d5e8f8a2d9fe90cbfad159676e324b954fdaefab46aeca007b5b9edab3d150021c4846444e4e8ab1f4e44cd3807be87 + languageName: node + linkType: hard + "node-fetch@npm:2, node-fetch@npm:2.7.0, node-fetch@npm:^2.5.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -33797,6 +35628,13 @@ __metadata: languageName: node linkType: hard +"node-mock-http@npm:^1.0.4": + version: 1.0.5 + resolution: "node-mock-http@npm:1.0.5" + checksum: 10c0/14a36b28423953af95e4659aa1e07208b5a38daaa89c1aac28a193f5420744a2511eb288503019b2fc8de582c70612d2e475d0df1d1a0e44fe3dfd7fc8f5399c + languageName: node + linkType: hard + "node-readable-to-web-readable-stream@npm:^0.4.2": version: 0.4.2 resolution: "node-readable-to-web-readable-stream@npm:0.4.2" @@ -34151,6 +35989,17 @@ __metadata: languageName: node linkType: hard +"ofetch@npm:^1.5.1": + version: 1.5.1 + resolution: "ofetch@npm:1.5.1" + dependencies: + destr: "npm:^2.0.5" + node-fetch-native: "npm:^1.6.7" + ufo: "npm:^1.6.1" + checksum: 10c0/97ebc600512ea0ab401e97c73313218cc53c9b530b32ec8c995c347b0c68887129993168d1753f527761a64c6f93a5d823ce1378ccec95fc65a606f323a79a6c + languageName: node + linkType: hard + "officeparser@npm:6.0.7": version: 6.0.7 resolution: "officeparser@npm:6.0.7" @@ -34167,6 +36016,13 @@ __metadata: languageName: node linkType: hard +"ohash@npm:^2.0.11": + version: 2.0.11 + resolution: "ohash@npm:2.0.11" + checksum: 10c0/d07c8d79cc26da082c1a7c8d5b56c399dd4ed3b2bd069fcae6bae78c99a9bcc3ad813b1e1f49ca2f335292846d689c6141a762cf078727d2302a33d414e69c79 + languageName: node + linkType: hard + "on-finished@npm:^2.3.0, on-finished@npm:^2.4.1, on-finished@npm:~2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -34219,6 +36075,24 @@ __metadata: languageName: node linkType: hard +"oniguruma-parser@npm:^0.12.2": + version: 0.12.2 + resolution: "oniguruma-parser@npm:0.12.2" + checksum: 10c0/fe5255d2cd5a6b845d5a0abe1725898ef40cea5522290dba6ccc08cc388891e9e8007af4baa5059942b786ad44b2b677ef25948039c9ba3f057bd35d2c52076a + languageName: node + linkType: hard + +"oniguruma-to-es@npm:^4.3.6": + version: 4.3.6 + resolution: "oniguruma-to-es@npm:4.3.6" + dependencies: + oniguruma-parser: "npm:^0.12.2" + regex: "npm:^6.1.0" + regex-recursion: "npm:^6.0.2" + checksum: 10c0/044e08b98e706987c2882ccf228de2a671de4aff33e3bd370da137ba599c0d520549625ad26ecc8898502445c991e19f106f584e94382262ba2af02c908ca3cf + languageName: node + linkType: hard + "only@npm:~0.0.2": version: 0.0.2 resolution: "only@npm:0.0.2" @@ -34682,6 +36556,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^7.3.0": + version: 7.3.1 + resolution: "p-limit@npm:7.3.1" + dependencies: + yocto-queue: "npm:^1.2.1" + checksum: 10c0/49b7d7fcc244d4659fa3b260c7b4e632bc72333c60e76e4cd24c7befdeb9b8e22da644e5064e83df9e2ded75245e3e3b9df0e9cf8500b08db8ebf58ac18c6764 + languageName: node + linkType: hard + "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -34723,13 +36606,13 @@ __metadata: languageName: node linkType: hard -"p-queue@npm:^9.0.0": - version: 9.0.0 - resolution: "p-queue@npm:9.0.0" +"p-queue@npm:^9.0.0, p-queue@npm:^9.1.0": + version: 9.3.3 + resolution: "p-queue@npm:9.3.3" dependencies: - eventemitter3: "npm:^5.0.1" + eventemitter3: "npm:^5.0.4" p-timeout: "npm:^7.0.0" - checksum: 10c0/0f27fcbec9e4e02f34cc3660f14b5746798c51a2425dc3d3c5238924c34726b1580606d5d1432fc05304e2350c00b112ded03eb43c1b49de7791a7150fbfcb4e + checksum: 10c0/0d88a35cb3301bee2aac654c0a7018a2e40775fa597f3a06da66f33b0311d106bebbe9732c8e5968ae1e0e0b49f79c874333d2a7b0a57fad469189f236c066d7 languageName: node linkType: hard @@ -34879,6 +36762,45 @@ __metadata: languageName: node linkType: hard +"package-manager-detector@npm:^1.6.0": + version: 1.8.0 + resolution: "package-manager-detector@npm:1.8.0" + checksum: 10c0/4c8e2c47fdc874465d3b3b11934401db9d73470ccbdabeb092e46cb94abbc491d5befc8c271b5ea1ae9c6a881f44c2997e011873365bde9fd6c3dc001221ff7a + languageName: node + linkType: hard + +"pagefind@npm:^1.5.2": + version: 1.5.2 + resolution: "pagefind@npm:1.5.2" + dependencies: + "@pagefind/darwin-arm64": "npm:1.5.2" + "@pagefind/darwin-x64": "npm:1.5.2" + "@pagefind/freebsd-x64": "npm:1.5.2" + "@pagefind/linux-arm64": "npm:1.5.2" + "@pagefind/linux-x64": "npm:1.5.2" + "@pagefind/windows-arm64": "npm:1.5.2" + "@pagefind/windows-x64": "npm:1.5.2" + dependenciesMeta: + "@pagefind/darwin-arm64": + optional: true + "@pagefind/darwin-x64": + optional: true + "@pagefind/freebsd-x64": + optional: true + "@pagefind/linux-arm64": + optional: true + "@pagefind/linux-x64": + optional: true + "@pagefind/windows-arm64": + optional: true + "@pagefind/windows-x64": + optional: true + bin: + pagefind: lib/runner/bin.cjs + checksum: 10c0/bcc4f34f473bfeb09dc4cb464a3547bf958e11ee4dee4144ec7a364bd74b9f7c21f85be71e388b66d30923a9a453dcaed2f6b425f3e67631cadc9650ba741aab + languageName: node + linkType: hard + "pako@npm:^0.2.5": version: 0.2.9 resolution: "pako@npm:0.2.9" @@ -34998,6 +36920,20 @@ __metadata: languageName: node linkType: hard +"parse-latin@npm:^7.0.0": + version: 7.0.0 + resolution: "parse-latin@npm:7.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + "@types/unist": "npm:^3.0.0" + nlcst-to-string: "npm:^4.0.0" + unist-util-modify-children: "npm:^4.0.0" + unist-util-visit-children: "npm:^3.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/4232a464f98c41c6680575c54bc2c9b21ac4b82a1f796a871bfef5efa6eddaab9bccf734b08cde6b0a5504ef46a0a14041ddd0bc5d9cc70f73a507f93f610596 + languageName: node + linkType: hard + "parse-ms@npm:^4.0.0": version: 4.0.0 resolution: "parse-ms@npm:4.0.0" @@ -35398,6 +37334,13 @@ __metadata: languageName: node linkType: hard +"piccolore@npm:^0.1.3": + version: 0.1.3 + resolution: "piccolore@npm:0.1.3" + checksum: 10c0/999666bb32eccc96a26b0cf3b8afe72f9d4cd4ca0eab5802b404cc84c601d81db4485b5feee5c50dbc8436ba6e1bc6a2310f99c18cee6687e430cc0eb4a36470 + languageName: node + linkType: hard + "picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -35707,6 +37650,17 @@ __metadata: languageName: node linkType: hard +"postcss-nested@npm:^6.0.1": + version: 6.2.0 + resolution: "postcss-nested@npm:6.2.0" + dependencies: + postcss-selector-parser: "npm:^6.1.1" + peerDependencies: + postcss: ^8.2.14 + checksum: 10c0/7f9c3f2d764191a39364cbdcec350f26a312431a569c9ef17408021424726b0d67995ff5288405e3724bb7152a4c92f73c027e580ec91e798800ed3c52e2bc6e + languageName: node + linkType: hard + "postcss-normalize-whitespace@npm:7.0.1": version: 7.0.1 resolution: "postcss-normalize-whitespace@npm:7.0.1" @@ -35728,6 +37682,16 @@ __metadata: languageName: node linkType: hard +"postcss-selector-parser@npm:^6.1.1": + version: 6.1.4 + resolution: "postcss-selector-parser@npm:6.1.4" + dependencies: + cssesc: "npm:^3.0.0" + util-deprecate: "npm:^1.0.2" + checksum: 10c0/996f3290dee08ecb073d5f396d1134e619494cfd83140aec07a29618ee1e76d370769a8959d4c0cfefb24aa96dcffa4e7c4937dd881e03b735d50cba959ceb19 + languageName: node + linkType: hard + "postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" @@ -35746,14 +37710,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.33, postcss@npm:^8.5.15, postcss@npm:^8.5.16, postcss@npm:^8.5.3, postcss@npm:^8.5.6": - version: 8.5.22 - resolution: "postcss@npm:8.5.22" +"postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.15, postcss@npm:^8.5.23, postcss@npm:^8.5.3, postcss@npm:^8.5.6": + version: 8.5.25 + resolution: "postcss@npm:8.5.25" dependencies: nanoid: "npm:^3.3.16" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/9e143ee457988049d5f187116fd37f2750ae9d8d71cc99eae690b776e549e134b34086cbaa2f0360ecd1729b15d918227bd0fc3a2ffbe341f7212d0827080793 + checksum: 10c0/0a12c1e74b456c57122e81f684e02fd98ff4d57526f794d10c996df1147158808f5ae373ac82b988c8de6cbbaa82dbd7b13803b14f5dd3cf7cc6a42ccad5c9f2 languageName: node linkType: hard @@ -35834,12 +37798,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.2.5": - version: 3.7.4 - resolution: "prettier@npm:3.7.4" +"prettier@npm:^3.2.5, prettier@npm:^3.8.1": + version: 3.9.6 + resolution: "prettier@npm:3.9.6" bin: prettier: bin/prettier.cjs - checksum: 10c0/9675d2cd08eacb1faf1d1a2dbfe24bfab6a912b059fc9defdb380a408893d88213e794a40a2700bd29b140eb3172e0b07c852853f6e22f16f3374659a1a13389 + checksum: 10c0/9f7ddae234035868f3daab3dc34e6de7e54622185afb017378029f0c09a9c023248ccf6f34def0b17a0538e18c47aa1b3188bab72965d1bf735919baa0747e99 languageName: node linkType: hard @@ -35899,6 +37863,13 @@ __metadata: languageName: node linkType: hard +"process-ancestry@npm:^0.1.0": + version: 0.1.0 + resolution: "process-ancestry@npm:0.1.0" + checksum: 10c0/3b20263db7c2eeb567efcef21bf4b3f3363c4bf7041796943fd8a467f888d94eb093a022b8fcc49c8d843f3fc81d15047255d4c2b4990f2d5a4f851f3c5c9cd4 + languageName: node + linkType: hard + "process-nextick-args@npm:^2.0.1, process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -36422,6 +38393,13 @@ __metadata: languageName: node linkType: hard +"radix3@npm:^1.1.2": + version: 1.1.2 + resolution: "radix3@npm:1.1.2" + checksum: 10c0/d4a295547f71af079868d2c2ed3814a9296ee026c5488212d58c106e6b4797c6eaec1259b46c9728913622f2240c9a944bfc8e2b3b5f6e4a5045338b1609f1e4 + languageName: node + linkType: hard + "raf-schd@npm:^4.0.2": version: 4.0.3 resolution: "raf-schd@npm:4.0.3" @@ -36838,7 +38816,7 @@ __metadata: languageName: node linkType: hard -"react-refresh@npm:0.18.0": +"react-refresh@npm:0.18.0, react-refresh@npm:^0.18.0": version: 0.18.0 resolution: "react-refresh@npm:0.18.0" checksum: 10c0/34a262f7fd803433a534f50deb27a148112a81adcae440c7d1cbae7ef14d21ea8f2b3d783e858cb7698968183b77755a38b4d4b5b1d79b4f4689c2f6d358fff2 @@ -37175,6 +39153,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^5.0.0": + version: 5.0.0 + resolution: "readdirp@npm:5.0.0" + checksum: 10c0/faf1ec57cff2020f473128da3f8d2a57813cc3a08a36c38cae1c9af32c1579906cc50ba75578043b35bade77e945c098233665797cf9730ba3613a62d6e79219 + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -37433,6 +39418,31 @@ __metadata: languageName: node linkType: hard +"regex-recursion@npm:^6.0.2": + version: 6.0.2 + resolution: "regex-recursion@npm:6.0.2" + dependencies: + regex-utilities: "npm:^2.3.0" + checksum: 10c0/68e8b6889680e904b75d7f26edaf70a1a4dc1087406bff53face4c2929d918fd77c72223843fe816ac8ed9964f96b4160650e8d5909e26a998c6e9de324dadb1 + languageName: node + linkType: hard + +"regex-utilities@npm:^2.3.0": + version: 2.3.0 + resolution: "regex-utilities@npm:2.3.0" + checksum: 10c0/78c550a80a0af75223244fff006743922591bd8f61d91fef7c86b9b56cf9bbf8ee5d7adb6d8991b5e304c57c90103fc4818cf1e357b11c6c669b782839bd7893 + languageName: node + linkType: hard + +"regex@npm:^6.1.0": + version: 6.1.0 + resolution: "regex@npm:6.1.0" + dependencies: + regex-utilities: "npm:^2.3.0" + checksum: 10c0/6e0ee2a1c17d5a66dc1120dfc51899dedf6677857e83a0df4d5a822ebb8645a54a079772efc1ade382b67aad35e4e22b5bd2d33c05ed28b0e000f8f57eb0aec1 + languageName: node + linkType: hard + "regexp-ast-analysis@npm:^0.7.0, regexp-ast-analysis@npm:^0.7.1": version: 0.7.1 resolution: "regexp-ast-analysis@npm:0.7.1" @@ -37547,6 +39557,36 @@ __metadata: languageName: node linkType: hard +"rehype-expressive-code@npm:^0.44.1": + version: 0.44.1 + resolution: "rehype-expressive-code@npm:0.44.1" + dependencies: + expressive-code: "npm:^0.44.1" + checksum: 10c0/7139dcb93dfc479225612ac51d1d7ad9b16e295eadbae4fecd697ab5f6f497dfb2d03317dd7b52a4a71691a8a88b7f23a7d0e216195ca6065b761a4958ae33c9 + languageName: node + linkType: hard + +"rehype-format@npm:^5.0.1": + version: 5.0.1 + resolution: "rehype-format@npm:5.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-format: "npm:^1.0.0" + checksum: 10c0/e87aac3e318ef96688785e108315b23762681f1a834ae52ac449b9787ff63e8435aceb70354fb629d2d733daca4f65889ddcdf0cd44b684ea4f481e8fac750e3 + languageName: node + linkType: hard + +"rehype-parse@npm:^9.0.0": + version: 9.0.1 + resolution: "rehype-parse@npm:9.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-from-html: "npm:^2.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/efa9ca17673fe70e2d322a1d262796bbed5f6a89382f8f8393352bbd6f6bbf1d4d1d050984b86ff9cb6c0fa2535175ab0829e53c94b1e38fc3c158e6c0ad90bc + languageName: node + linkType: hard + "rehype-raw@npm:^7.0.0": version: 7.0.0 resolution: "rehype-raw@npm:7.0.0" @@ -37569,6 +39609,29 @@ __metadata: languageName: node linkType: hard +"rehype-stringify@npm:^10.0.0, rehype-stringify@npm:^10.0.1": + version: 10.0.1 + resolution: "rehype-stringify@npm:10.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-to-html: "npm:^9.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/c643ae3a4862465033e0f1e9f664433767279b4ee9296570746970a79940417ec1fb1997a513659aab97063cf971c5d97e0af8129f590719f01628c8aa480765 + languageName: node + linkType: hard + +"rehype@npm:^13.0.2": + version: 13.0.2 + resolution: "rehype@npm:13.0.2" + dependencies: + "@types/hast": "npm:^3.0.0" + rehype-parse: "npm:^9.0.0" + rehype-stringify: "npm:^10.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/13d82086b673b3ce1fddb54cc8d30be16bde83fb62f1507f0af06070c94b85d07c3780fa994357bad2c9d51b84e4108ff661677b71d187e4f2167cab22d84363 + languageName: node + linkType: hard + "reinterval@npm:^1.1.0": version: 1.1.0 resolution: "reinterval@npm:1.1.0" @@ -37583,7 +39646,19 @@ __metadata: languageName: node linkType: hard -"remark-gfm@npm:4.0.1, remark-gfm@npm:^4.0.0": +"remark-directive@npm:^4.0.0": + version: 4.0.0 + resolution: "remark-directive@npm:4.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-directive: "npm:^3.0.0" + micromark-extension-directive: "npm:^4.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/ab1f71ae220913f62833464d483f29de7b4af0dcf1ff626d33cdb5dd81eda1c553a11f779de9fd9b641802586c5f070175824d70f14d53e451a484850705d53a + languageName: node + linkType: hard + +"remark-gfm@npm:4.0.1, remark-gfm@npm:^4.0.0, remark-gfm@npm:^4.0.1": version: 4.0.1 resolution: "remark-gfm@npm:4.0.1" dependencies: @@ -37619,16 +39694,28 @@ __metadata: languageName: node linkType: hard -"remark-rehype@npm:^11.0.0": - version: 11.1.1 - resolution: "remark-rehype@npm:11.1.1" +"remark-rehype@npm:^11.0.0, remark-rehype@npm:^11.1.2": + version: 11.1.2 + resolution: "remark-rehype@npm:11.1.2" dependencies: "@types/hast": "npm:^3.0.0" "@types/mdast": "npm:^4.0.0" mdast-util-to-hast: "npm:^13.0.0" unified: "npm:^11.0.0" vfile: "npm:^6.0.0" - checksum: 10c0/68f986e8ee758d415e93babda2a0d89477c15b7c200edc23b8b1d914dd6e963c5fc151a11cbbbcfa7dd237367ff3ef86e302be90f31f37a17b0748668bd8c65b + checksum: 10c0/f9eccacfb596d9605581dc05bfad28635d6ded5dd0a18e88af5fd4df0d3fcf9612e1501d4513bc2164d833cfe9636dab20400080b09e53f155c6e1442a1231fb + languageName: node + linkType: hard + +"remark-smartypants@npm:^3.0.2": + version: 3.0.3 + resolution: "remark-smartypants@npm:3.0.3" + dependencies: + retext: "npm:^9.0.0" + retext-smartypants: "npm:^6.0.0" + unified: "npm:^11.0.4" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/24f210e64d44b13559aa6c7423b29a0a3b7fb1b603c3b997c70ad923e1bf1cebff9f234db801c689c02ff5bebdfd5256481c6690928a2465c09cd54a03076514 languageName: node linkType: hard @@ -37691,6 +39778,20 @@ __metadata: languageName: node linkType: hard +"request-light@npm:^0.5.7": + version: 0.5.8 + resolution: "request-light@npm:0.5.8" + checksum: 10c0/65d47f79cbe13c46c10eecb9264ad06ea5f76ea64855766448937c0fe888a70b9c854bf9dc4c2dc1949ea5400030b55bea7d4ba0807fae0202004cee796ea819 + languageName: node + linkType: hard + +"request-light@npm:^0.7.0": + version: 0.7.0 + resolution: "request-light@npm:0.7.0" + checksum: 10c0/1c98f0d74b8a28a6fecb96d4b30c255a27b402f5e10b2696b67ede7257389a7533fba9e8ea122ccec9a7c6f06db36eab5d0b563bd5ff27b15aaa9d3910e31894 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -37879,6 +39980,51 @@ __metadata: languageName: node linkType: hard +"retext-latin@npm:^4.0.0": + version: 4.0.0 + resolution: "retext-latin@npm:4.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + parse-latin: "npm:^7.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/51530be66db9ef6ab8e9cda5dd0598377ff4321481d6a941bf70dac16fa6e9123ff7d8ff093a05c30a3e00e282e37094b845b6130a8005a3cb7186a961ab99cb + languageName: node + linkType: hard + +"retext-smartypants@npm:^6.0.0, retext-smartypants@npm:^6.2.0": + version: 6.2.0 + resolution: "retext-smartypants@npm:6.2.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + nlcst-to-string: "npm:^4.0.0" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/36f925353dd7f31df642bca2493524a8daee15f9b0e0dfe7fb8982462d23ccb12a99864989db22f0bacb6d7fea1f696ba96e031d3fbac4f013e1c95ef3fed881 + languageName: node + linkType: hard + +"retext-stringify@npm:^4.0.0": + version: 4.0.0 + resolution: "retext-stringify@npm:4.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + nlcst-to-string: "npm:^4.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/eb2930356c85999a8978092a5d6ba3695fea859c71f221dcdc485704552922641bc17e50fea2ae0599d665192eaad002e98bb4236ecac94a570b73581b99004d + languageName: node + linkType: hard + +"retext@npm:^9.0.0": + version: 9.0.0 + resolution: "retext@npm:9.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + retext-latin: "npm:^4.0.0" + retext-stringify: "npm:^4.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/eee9f66ff6fae5670a5eeccc0b5e2639112f868475273ce307d3079cfe7deb9d1b0f2b8fa28b4ab30abaf8538345185a44908f461a27bbf43c4f94feda90ecac + languageName: node + linkType: hard + "retry-request@npm:^7.0.0": version: 7.0.2 resolution: "retry-request@npm:7.0.2" @@ -38048,7 +40194,7 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:~1.1.2, rolldown@npm:~1.1.4": +"rolldown@npm:~1.1.2": version: 1.1.5 resolution: "rolldown@npm:1.1.5" dependencies: @@ -38106,6 +40252,64 @@ __metadata: languageName: node linkType: hard +"rolldown@npm:~1.2.0": + version: 1.2.1 + resolution: "rolldown@npm:1.2.1" + dependencies: + "@oxc-project/types": "npm:=0.142.0" + "@rolldown/binding-android-arm64": "npm:1.2.1" + "@rolldown/binding-darwin-arm64": "npm:1.2.1" + "@rolldown/binding-darwin-x64": "npm:1.2.1" + "@rolldown/binding-freebsd-x64": "npm:1.2.1" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.2.1" + "@rolldown/binding-linux-arm64-gnu": "npm:1.2.1" + "@rolldown/binding-linux-arm64-musl": "npm:1.2.1" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.2.1" + "@rolldown/binding-linux-s390x-gnu": "npm:1.2.1" + "@rolldown/binding-linux-x64-gnu": "npm:1.2.1" + "@rolldown/binding-linux-x64-musl": "npm:1.2.1" + "@rolldown/binding-openharmony-arm64": "npm:1.2.1" + "@rolldown/binding-wasm32-wasi": "npm:1.2.1" + "@rolldown/binding-win32-arm64-msvc": "npm:1.2.1" + "@rolldown/binding-win32-x64-msvc": "npm:1.2.1" + "@rolldown/pluginutils": "npm:^1.0.0" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: ./bin/cli.mjs + checksum: 10c0/d22c80c70d71a36abde7dca7217f66d94cdb5d0c03185205de808d368ab1b8fd5e78ff4db10efc69c5f68a5f9d03d59c8ace848f1f85c4aab79330162a10b3e7 + languageName: node + linkType: hard + "rollup@npm:4.59.0": version: 4.59.0 resolution: "rollup@npm:4.59.0" @@ -38480,10 +40684,50 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.5.0": - version: 1.5.0 - resolution: "sax@npm:1.5.0" - checksum: 10c0/bc3b60a7bfecd40b18256596e96b32df2488339ae1e00a77f842b568f0831228a16c3bd357ec500241ec0b9dc7a475a1286427795c4a8c50bb8e8878f3435dd8 +"satteri@npm:^0.9.1": + version: 0.9.5 + resolution: "satteri@npm:0.9.5" + dependencies: + "@bruits/satteri-darwin-arm64": "npm:0.9.5" + "@bruits/satteri-darwin-x64": "npm:0.9.5" + "@bruits/satteri-linux-arm64-gnu": "npm:0.9.5" + "@bruits/satteri-linux-arm64-musl": "npm:0.9.5" + "@bruits/satteri-linux-x64-gnu": "npm:0.9.5" + "@bruits/satteri-linux-x64-musl": "npm:0.9.5" + "@bruits/satteri-wasm32-wasi": "npm:0.9.5" + "@bruits/satteri-win32-arm64-msvc": "npm:0.9.5" + "@bruits/satteri-win32-x64-msvc": "npm:0.9.5" + "@types/estree-jsx": "npm:^1.0.5" + "@types/hast": "npm:^3.0.4" + "@types/mdast": "npm:^4.0.4" + "@types/unist": "npm:^3.0.3" + dependenciesMeta: + "@bruits/satteri-darwin-arm64": + optional: true + "@bruits/satteri-darwin-x64": + optional: true + "@bruits/satteri-linux-arm64-gnu": + optional: true + "@bruits/satteri-linux-arm64-musl": + optional: true + "@bruits/satteri-linux-x64-gnu": + optional: true + "@bruits/satteri-linux-x64-musl": + optional: true + "@bruits/satteri-wasm32-wasi": + optional: true + "@bruits/satteri-win32-arm64-msvc": + optional: true + "@bruits/satteri-win32-x64-msvc": + optional: true + checksum: 10c0/4a204a57b7b84750cb0fc9a1ab153c0c99dd3c57cac20b785705f6b723c1a465263951ad624eb47808a9501418eb81fa526bbc71f57f8fd07472fdf114e73a9c + languageName: node + linkType: hard + +"sax@npm:^1.4.1, sax@npm:^1.5.0": + version: 1.6.1 + resolution: "sax@npm:1.6.1" + checksum: 10c0/c91a71043d60da50fdf1e2cee936fc7ad4c9376afb8d1022aef5655f279433640859ec06b3b49abfcbe578fc7da2828cc1661e45aaedf835f5393496193437fa languageName: node linkType: hard @@ -38611,7 +40855,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.8.5, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.1, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.8.5": +"semver@npm:7.8.5, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.1, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.5": version: 7.8.5 resolution: "semver@npm:7.8.5" bin: @@ -38840,7 +41084,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:0.35.3": +"sharp@npm:0.35.3, sharp@npm:^0.34.0 || ^0.35.0": version: 0.35.3 resolution: "sharp@npm:0.35.3" dependencies: @@ -38983,6 +41227,22 @@ __metadata: languageName: node linkType: hard +"shiki@npm:^4.0.2": + version: 4.4.1 + resolution: "shiki@npm:4.4.1" + dependencies: + "@shikijs/core": "npm:4.4.1" + "@shikijs/engine-javascript": "npm:4.4.1" + "@shikijs/engine-oniguruma": "npm:4.4.1" + "@shikijs/langs": "npm:4.4.1" + "@shikijs/themes": "npm:4.4.1" + "@shikijs/types": "npm:4.4.1" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + checksum: 10c0/8ef2e0c0a202daa328907bb4f5e6dc1abb3dee8157be3fb65630b9c6f1f8938e3f9f98034f9f9ee30e3f40e948980587cfc94a83f3cf62742cc46f2d532054bc + languageName: node + linkType: hard + "short-unique-id@npm:^5.2.0": version: 5.2.0 resolution: "short-unique-id@npm:5.2.0" @@ -39165,6 +41425,20 @@ __metadata: languageName: node linkType: hard +"sitemap@npm:^9.0.0": + version: 9.0.1 + resolution: "sitemap@npm:9.0.1" + dependencies: + "@types/node": "npm:^24.9.2" + "@types/sax": "npm:^1.2.1" + arg: "npm:^5.0.0" + sax: "npm:^1.4.1" + bin: + sitemap: dist/esm/cli.js + checksum: 10c0/dddd3c9f30fd09655fa2c1bd9bb182ed860717b3ba681fdc1ac66ad25b5f399dafdd8b87d5ba4ee914ea14e7b11841aadc3af61e4a45242a14f28194efe12416 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -39224,6 +41498,13 @@ __metadata: languageName: node linkType: hard +"smol-toml@npm:^1.6.0": + version: 1.7.1 + resolution: "smol-toml@npm:1.7.1" + checksum: 10c0/c2494e19e748578cf9f81f918dd66a548dfe5a0e581ab9f980d5420f2319c882c661a62d01d216a43f2db490faaccd2185bcdb6f2ba0658eb303eda9db871d76 + languageName: node + linkType: hard + "snake-case@npm:^3.0.4": version: 3.0.4 resolution: "snake-case@npm:3.0.4" @@ -39691,6 +41972,13 @@ __metadata: languageName: node linkType: hard +"stream-replace-string@npm:^2.0.0": + version: 2.0.0 + resolution: "stream-replace-string@npm:2.0.0" + checksum: 10c0/6cdf6108c57a869c1282dece0728bd7a8e314855bee71992436460192cdf46b3c976451e1e114716af209b2bfefa0e7e4581ca0eebc330d9dfcde341a72d50af + languageName: node + linkType: hard + "stream-shift@npm:^1.0.2": version: 1.0.3 resolution: "stream-shift@npm:1.0.3" @@ -39801,7 +42089,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^8.2.0": +"string-width@npm:^8.2.0, string-width@npm:^8.2.1": version: 8.2.2 resolution: "string-width@npm:8.2.2" dependencies: @@ -40230,6 +42518,23 @@ __metadata: languageName: node linkType: hard +"svgo@npm:^4.0.1": + version: 4.0.2 + resolution: "svgo@npm:4.0.2" + dependencies: + commander: "npm:^11.1.0" + css-select: "npm:^5.1.0" + css-tree: "npm:^3.0.1" + css-what: "npm:^6.1.0" + csso: "npm:^5.0.5" + picocolors: "npm:^1.1.1" + sax: "npm:^1.5.0" + bin: + svgo: ./bin/svgo.js + checksum: 10c0/d58c9446d2701dd57ef62a29b4299b157cfc32e2282f82fc68dd7cdca2e1efdf0d8bb0fb30ba0499f322d20b6304be277ffbc60a6b6a75489fa30c5300694b9a + languageName: node + linkType: hard + "swap-case@npm:^2.0.2": version: 2.0.2 resolution: "swap-case@npm:2.0.2" @@ -40644,7 +42949,7 @@ __metadata: languageName: node linkType: hard -"tiny-inflate@npm:^1.0.0": +"tiny-inflate@npm:^1.0.0, tiny-inflate@npm:^1.0.3": version: 1.0.3 resolution: "tiny-inflate@npm:1.0.3" checksum: 10c0/fab687537254f6ec44c9a2e880048fe70da3542aba28f73cda3e74c95cabf342a339372f2a6c032e322324f01accc03ca26c04ba2bad9b3eb8cf3ee99bba7f9b @@ -40665,6 +42970,13 @@ __metadata: languageName: node linkType: hard +"tinyclip@npm:^0.1.12": + version: 0.1.15 + resolution: "tinyclip@npm:0.1.15" + checksum: 10c0/8ebdcc60e0e9ba54edeae28158a7a96e94657ac9387cc03cd5ef6c51c36b92be2ebe10cf5e34c9382124ef766cf82784cca2c5db57cd7ab447b7d04880ee7579 + languageName: node + linkType: hard + "tinyexec@npm:^0.3.2": version: 0.3.2 resolution: "tinyexec@npm:0.3.2" @@ -40672,14 +42984,14 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.2": - version: 1.0.4 - resolution: "tinyexec@npm:1.0.4" - checksum: 10c0/d4a5bbcf6bdb23527a4b74c4aa566f41432167112fe76f420ec7e3a90a3ecfd3a7d944383e2719fc3987b69400f7b928daf08700d145fb527c2e80ec01e198bd +"tinyexec@npm:^1.0.2, tinyexec@npm:^1.0.4": + version: 1.2.4 + resolution: "tinyexec@npm:1.2.4" + checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff languageName: node linkType: hard -"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.17, tinyglobby@npm:^0.2.9": +"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.16, tinyglobby@npm:^0.2.17, tinyglobby@npm:^0.2.9": version: 0.2.17 resolution: "tinyglobby@npm:0.2.17" dependencies: @@ -41367,6 +43679,22 @@ __metadata: languageName: node linkType: hard +"typesafe-path@npm:^0.2.2": + version: 0.2.2 + resolution: "typesafe-path@npm:0.2.2" + checksum: 10c0/05027ea6a52a1e879da39c53fae7f5059e50cf578d586cc6c7741c3eca0b37ee2da4ef16d35ecf9f60cd0e8ca5aca98ea47902672907a19470f2260e8769b933 + languageName: node + linkType: hard + +"typescript-auto-import-cache@npm:^0.3.5": + version: 0.3.6 + resolution: "typescript-auto-import-cache@npm:0.3.6" + dependencies: + semver: "npm:^7.3.8" + checksum: 10c0/aad48d6aa9985f9a056c5c89ab91b6781c5430c0b80652586c2a997f2cc394f3ed70ec18c0c818a8423991a1cfe7e64d010cc2fded9ea192067a3188476c2b51 + languageName: node + linkType: hard + "typescript-eslint@npm:^8.46.4": version: 8.50.0 resolution: "typescript-eslint@npm:8.50.0" @@ -41509,10 +43837,10 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.6.1": - version: 1.6.1 - resolution: "ufo@npm:1.6.1" - checksum: 10c0/5a9f041e5945fba7c189d5410508cbcbefef80b253ed29aa2e1f8a2b86f4bd51af44ee18d4485e6d3468c92be9bf4a42e3a2b72dcaf27ce39ce947ec994f1e6b +"ufo@npm:^1.6.1, ufo@npm:^1.6.3": + version: 1.6.4 + resolution: "ufo@npm:1.6.4" + checksum: 10c0/3a2b29e7e3d772fbf6893d7d23bf442981457adb2fe122828abdbda89bedcb81aafd0dcc080e41b45f9a877db00cb42cbfee9639753a19d9b9bd39b5627039cf languageName: node linkType: hard @@ -41569,6 +43897,13 @@ __metadata: languageName: node linkType: hard +"ultrahtml@npm:^1.6.0": + version: 1.7.0 + resolution: "ultrahtml@npm:1.7.0" + checksum: 10c0/a56907b3ea4b0c16d30c37cb814138f8ae47044892a9d1c5ecca0731e24c2c4cd1c9cfa53d57bf508b21bc3b993e3c02bf3f9c3bfc156d18ed7e12c62a140f75 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -41598,6 +43933,13 @@ __metadata: languageName: node linkType: hard +"uncrypto@npm:^0.1.3": + version: 0.1.3 + resolution: "uncrypto@npm:0.1.3" + checksum: 10c0/74a29afefd76d5b77bedc983559ceb33f5bbc8dada84ff33755d1e3355da55a4e03a10e7ce717918c436b4dfafde1782e799ebaf2aadd775612b49f7b5b2998e + languageName: node + linkType: hard + "underscore@npm:1.13.8": version: 1.13.8 resolution: "underscore@npm:1.13.8" @@ -41612,10 +43954,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.16.0": - version: 7.16.0 - resolution: "undici-types@npm:7.16.0" - checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d languageName: node linkType: hard @@ -41698,7 +44040,7 @@ __metadata: languageName: node linkType: hard -"unified@npm:^11.0.0, unified@npm:^11.0.4": +"unified@npm:^11.0.0, unified@npm:^11.0.4, unified@npm:^11.0.5": version: 11.0.5 resolution: "unified@npm:11.0.5" dependencies: @@ -41713,6 +44055,17 @@ __metadata: languageName: node linkType: hard +"unifont@npm:~0.7.4": + version: 0.7.4 + resolution: "unifont@npm:0.7.4" + dependencies: + css-tree: "npm:^3.1.0" + ofetch: "npm:^1.5.1" + ohash: "npm:^2.0.11" + checksum: 10c0/2b1c23e13083ce52382ac3e430b24c7f7a9a8657480f82847e254d5e3aa8a639c1178fda9ad157c21d2ceff5f7a037021c547501856dca7a93bc75bef6fe2f7d + languageName: node + linkType: hard + "unionfs@npm:^4.5.1": version: 4.5.4 resolution: "unionfs@npm:4.5.4" @@ -41740,6 +44093,16 @@ __metadata: languageName: node linkType: hard +"unist-util-find-after@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-find-after@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/a7cea473c4384df8de867c456b797ff1221b20f822e1af673ff5812ed505358b36f47f3b084ac14c3622cb879ed833b71b288e8aa71025352a2aab4c2925a6eb + languageName: node + linkType: hard + "unist-util-is@npm:^6.0.0": version: 6.0.0 resolution: "unist-util-is@npm:6.0.0" @@ -41749,6 +44112,16 @@ __metadata: languageName: node linkType: hard +"unist-util-modify-children@npm:^4.0.0": + version: 4.0.0 + resolution: "unist-util-modify-children@npm:4.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + array-iterate: "npm:^2.0.0" + checksum: 10c0/63d44b09a2e4c674c72816d4328d668972e68cc965ea719fef1c642b66a3ebe3b102e284a3213b4920ebccff05e0f689b4eaae8a0e5c3dafcad117d1577496da + languageName: node + linkType: hard + "unist-util-position-from-estree@npm:^2.0.0": version: 2.0.0 resolution: "unist-util-position-from-estree@npm:2.0.0" @@ -41767,6 +44140,16 @@ __metadata: languageName: node linkType: hard +"unist-util-remove-position@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-remove-position@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/e8c76da4399446b3da2d1c84a97c607b37d03d1d92561e14838cbe4fdcb485bfc06c06cfadbb808ccb72105a80643976d0660d1fe222ca372203075be9d71105 + languageName: node + linkType: hard + "unist-util-stringify-position@npm:^4.0.0": version: 4.0.0 resolution: "unist-util-stringify-position@npm:4.0.0" @@ -41776,24 +44159,33 @@ __metadata: languageName: node linkType: hard -"unist-util-visit-parents@npm:^6.0.0": - version: 6.0.1 - resolution: "unist-util-visit-parents@npm:6.0.1" +"unist-util-visit-children@npm:^3.0.0": + version: 3.0.0 + resolution: "unist-util-visit-children@npm:3.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + checksum: 10c0/51e95f54fbf11d414952c011c761c3960864948ad3fd2abe3989eb18b18d96b8f48e7ea5ab6f23264d1a3f4f5a1ff76312dd8f2196c78b762098403505c3abb9 + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^6.0.0, unist-util-visit-parents@npm:^6.0.1, unist-util-visit-parents@npm:^6.0.2": + version: 6.0.2 + resolution: "unist-util-visit-parents@npm:6.0.2" dependencies: "@types/unist": "npm:^3.0.0" unist-util-is: "npm:^6.0.0" - checksum: 10c0/51b1a5b0aa23c97d3e03e7288f0cdf136974df2217d0999d3de573c05001ef04cccd246f51d2ebdfb9e8b0ed2704451ad90ba85ae3f3177cf9772cef67f56206 + checksum: 10c0/f1e4019dbd930301825895e3737b1ee0cd682f7622ddd915062135cbb39f8c090aaece3a3b5eae1f2ea52ec33f0931abb8f8a8b5c48a511a4203e3d360a8cd49 languageName: node linkType: hard -"unist-util-visit@npm:^5.0.0": - version: 5.0.0 - resolution: "unist-util-visit@npm:5.0.0" +"unist-util-visit@npm:^5.0.0, unist-util-visit@npm:^5.1.0": + version: 5.1.0 + resolution: "unist-util-visit@npm:5.1.0" dependencies: "@types/unist": "npm:^3.0.0" unist-util-is: "npm:^6.0.0" unist-util-visit-parents: "npm:^6.0.0" - checksum: 10c0/51434a1d80252c1540cce6271a90fd1a106dbe624997c09ed8879279667fb0b2d3a685e02e92bf66598dcbe6cdffa7a5f5fb363af8fdf90dda6c855449ae39a5 + checksum: 10c0/a56e1bbbf63fcb55abe379e660b9a3367787e8be1e2473bdb7e86cfa6f32b6c1fa0092432d7040b8a30b2fc674bbbe024ffe6d03c3d6bf4839b064f584463a4e languageName: node linkType: hard @@ -41936,6 +44328,81 @@ __metadata: languageName: node linkType: hard +"unstorage@npm:^1.17.5": + version: 1.17.5 + resolution: "unstorage@npm:1.17.5" + dependencies: + anymatch: "npm:^3.1.3" + chokidar: "npm:^5.0.0" + destr: "npm:^2.0.5" + h3: "npm:^1.15.10" + lru-cache: "npm:^11.2.7" + node-fetch-native: "npm:^1.6.7" + ofetch: "npm:^1.5.1" + ufo: "npm:^1.6.3" + peerDependencies: + "@azure/app-configuration": ^1.8.0 + "@azure/cosmos": ^4.2.0 + "@azure/data-tables": ^13.3.0 + "@azure/identity": ^4.6.0 + "@azure/keyvault-secrets": ^4.9.0 + "@azure/storage-blob": ^12.26.0 + "@capacitor/preferences": ^6 || ^7 || ^8 + "@deno/kv": ">=0.9.0" + "@netlify/blobs": ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + "@planetscale/database": ^1.19.0 + "@upstash/redis": ^1.34.3 + "@vercel/blob": ">=0.27.1" + "@vercel/functions": ^2.2.12 || ^3.0.0 + "@vercel/kv": ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: ">=0.2.1" + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + "@azure/app-configuration": + optional: true + "@azure/cosmos": + optional: true + "@azure/data-tables": + optional: true + "@azure/identity": + optional: true + "@azure/keyvault-secrets": + optional: true + "@azure/storage-blob": + optional: true + "@capacitor/preferences": + optional: true + "@deno/kv": + optional: true + "@netlify/blobs": + optional: true + "@planetscale/database": + optional: true + "@upstash/redis": + optional: true + "@vercel/blob": + optional: true + "@vercel/functions": + optional: true + "@vercel/kv": + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + checksum: 10c0/52bc07d0951129f493cc2d2a5204f49df90c85e7ab3faac0019e3a31f22f507f7a8263fca7ca4d019950c14cbfed4d43e15c9afe4b5dcb307249ebb60d2e538e + languageName: node + linkType: hard + "until-async@npm:^3.0.2": version: 3.0.2 resolution: "until-async@npm:3.0.2" @@ -42012,6 +44479,13 @@ __metadata: languageName: node linkType: hard +"url-extras@npm:^0.1.0": + version: 0.1.0 + resolution: "url-extras@npm:0.1.0" + checksum: 10c0/353ca8190921d793bedaf765b55f94c1d719d1fd696c9a5dafeaf1b4532c1de936649925a0d699aa9613555b8dac82078e51d0a65cb1e48b92817ed68a527b60 + languageName: node + linkType: hard + "url-parse@npm:^1.5.3": version: 1.5.10 resolution: "url-parse@npm:1.5.10" @@ -42435,19 +44909,19 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.1.4 - resolution: "vite@npm:8.1.4" +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.13": + version: 8.2.0 + resolution: "vite@npm:8.2.0" dependencies: fsevents: "npm:~2.3.3" - lightningcss: "npm:^1.32.0" + lightningcss: "npm:^1.33.0" picomatch: "npm:^4.0.5" - postcss: "npm:^8.5.16" - rolldown: "npm:~1.1.4" + postcss: "npm:^8.5.23" + rolldown: "npm:~1.2.0" tinyglobby: "npm:^0.2.17" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.3.0 + "@vitejs/devtools": ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 @@ -42488,7 +44962,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/4262cdd0dd00ca99b00a8aef91fc00e58b66e80b54adbef49353bfbbae7342c2d6ddf1180351956086ff53ad186bec03784f57b96e36ab4dad1246547236d79b + checksum: 10c0/bd6a5e7b28973bac06f0e8e54833800e16d1bf38a8aef2acbfea5bbaed6d8fc715fd7cc9b0ec75ef1adbde149bb9167b085c17fe7edcd3a190b4eda16d474e5c languageName: node linkType: hard @@ -42547,6 +45021,18 @@ __metadata: languageName: node linkType: hard +"vitefu@npm:^1.1.2": + version: 1.1.3 + resolution: "vitefu@npm:1.1.3" + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + checksum: 10c0/6cfcd52a17339f63f71ef08787a68850a17a9d9dd7a688e29e41067f963eb2cd5192bb65d38df87fa3a733a21e0229aaa6382201655c1599699e5d6669e3e094 + languageName: node + linkType: hard + "vitest-browser-react@npm:^2.0.2": version: 2.2.0 resolution: "vitest-browser-react@npm:2.2.0" @@ -42633,7 +45119,192 @@ __metadata: languageName: node linkType: hard -"vscode-languageserver-textdocument@npm:^1.0.8": +"volar-service-css@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-css@npm:0.0.71" + dependencies: + vscode-css-languageservice: "npm:^6.3.0" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/fd07e55786a18db2f671eea087e2d43116d8c0f504aac699a3b47fc9d6f53d66c0b3652062ca6ae08d8e59905564f72c63af9727d956165061849ab488462e2e + languageName: node + linkType: hard + +"volar-service-emmet@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-emmet@npm:0.0.71" + dependencies: + "@emmetio/css-parser": "npm:^0.4.1" + "@emmetio/html-matcher": "npm:^1.3.0" + "@vscode/emmet-helper": "npm:^2.9.3" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/a3c95991859d0e7e06a23d1201fcbdddbb7be0c525a553881f14ab941efe331adbb27431caf67962ec43a077e424f7e30bada6a52254272572b93b01f55bcf73 + languageName: node + linkType: hard + +"volar-service-html@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-html@npm:0.0.71" + dependencies: + vscode-html-languageservice: "npm:^5.3.0" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/b1ff293413945cf7357aadc9484080863e7a855a919ccc4f379123ebabe16f9905f5bd19db60e08028a7282c2926762a006daddabd0e373d5040d32afe2ab364 + languageName: node + linkType: hard + +"volar-service-prettier@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-prettier@npm:0.0.71" + dependencies: + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + prettier: + optional: true + checksum: 10c0/f3498260e51a5ee6d762fac0f332e745a5705db9ccd9aca9d154985cff733d22e8be6a41239bdce4cd5dcdaa05f8bd6e1baf27c5585f9b968e93155eb74e2118 + languageName: node + linkType: hard + +"volar-service-typescript-twoslash-queries@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-typescript-twoslash-queries@npm:0.0.71" + dependencies: + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/deaea07b08c6dc54763c98217b1b206c885e4434a07018f058e22602f8445ee29ceac70217b1271b6182c979e8ccfe2fbdaa464e327ab8ae8a0eb87517d0adba + languageName: node + linkType: hard + +"volar-service-typescript@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-typescript@npm:0.0.71" + dependencies: + path-browserify: "npm:^1.0.1" + semver: "npm:^7.6.2" + typescript-auto-import-cache: "npm:^0.3.5" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-nls: "npm:^5.2.0" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/c197666fca9810e72e9db81d15c527b21dbebd493966199b5e8d690204ba17cbc6cfaeed6be7d72b285bb795ff7fbb45a939463db0cb804360c4966e5811bbc0 + languageName: node + linkType: hard + +"volar-service-yaml@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-yaml@npm:0.0.71" + dependencies: + vscode-uri: "npm:^3.0.8" + yaml-language-server: "npm:~1.23.0" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/6d56a6c77a100208e1ae0c5cbc80f547a066012a21574f476b37b59dfb212129efaf1a98fdc4cde8bcc71c61e9d26e0ac326d6c8ea300dffd681bf2c55b50f35 + languageName: node + linkType: hard + +"vscode-css-languageservice@npm:^6.3.0": + version: 6.3.10 + resolution: "vscode-css-languageservice@npm:6.3.10" + dependencies: + "@vscode/l10n": "npm:^0.0.18" + vscode-languageserver-textdocument: "npm:^1.0.12" + vscode-languageserver-types: "npm:3.17.5" + vscode-uri: "npm:^3.1.0" + checksum: 10c0/3f6228d85579c0306343c734716c17128757f3a6e44625deac06622fe248f05fe15da1901581aa50cb8d9539137a49d19f05dca6d13feb2e6bc9f3d9784236bd + languageName: node + linkType: hard + +"vscode-html-languageservice@npm:^5.3.0, vscode-html-languageservice@npm:^5.6.2": + version: 5.6.2 + resolution: "vscode-html-languageservice@npm:5.6.2" + dependencies: + "@vscode/l10n": "npm:^0.0.18" + vscode-languageserver-textdocument: "npm:^1.0.12" + vscode-languageserver-types: "npm:^3.17.5" + vscode-uri: "npm:^3.1.0" + checksum: 10c0/5ef5f3cb959435a3f29d9b5844fb223140b8b5b616c3f54d0c627427853e8c8806d5d93f3a4e3354a3c9d2e9acdf6cb8043479837d20d9dd30ad6d07bf22286d + languageName: node + linkType: hard + +"vscode-json-languageservice@npm:4.1.8": + version: 4.1.8 + resolution: "vscode-json-languageservice@npm:4.1.8" + dependencies: + jsonc-parser: "npm:^3.0.0" + vscode-languageserver-textdocument: "npm:^1.0.1" + vscode-languageserver-types: "npm:^3.16.0" + vscode-nls: "npm:^5.0.0" + vscode-uri: "npm:^3.0.2" + checksum: 10c0/9165703884e5eef52d4bf52294051df6623461cc7a3d458ef1f4fa3f98ac93c3a79efc8c4a15fda6ddf6e43d155c207c3c13534dcac3fe7982ddf1926dbf22a7 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: 10c0/0789c227057a844f5ead55c84679206227a639b9fb76e881185053abc4e9848aa487245966cc2393fcb342c4541241b015a1a2559fddd20ac1e68945c95344e6 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:9.0.1": + version: 9.0.1 + resolution: "vscode-jsonrpc@npm:9.0.1" + checksum: 10c0/6ca63393dd78675ed33b5537c147e673865557ff93915cd5189ceae039ca7fe01808ffd9a14016732d4789f2102a037d68d4216f557c3380d1c7f3ea50fd85b4 + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-protocol@npm:3.17.5" + dependencies: + vscode-jsonrpc: "npm:8.2.0" + vscode-languageserver-types: "npm:3.17.5" + checksum: 10c0/5f38fd80da9868d706eaa4a025f4aff9c3faad34646bcde1426f915cbd8d7e8b6c3755ce3fef6eebd256ba3145426af1085305f8a76e34276d2e95aaf339a90b + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:^3.17.5": + version: 3.18.2 + resolution: "vscode-languageserver-protocol@npm:3.18.2" + dependencies: + vscode-jsonrpc: "npm:9.0.1" + vscode-languageserver-types: "npm:3.18.0" + checksum: 10c0/c77862958063b2d6edcdbab7da1abece58b251051338083296c468d5caafc2c249aba63f1a15011d8b8354e145937252520fe6a9632c146606a833bdeae9784c + languageName: node + linkType: hard + +"vscode-languageserver-textdocument@npm:^1.0.1, vscode-languageserver-textdocument@npm:^1.0.11, vscode-languageserver-textdocument@npm:^1.0.12, vscode-languageserver-textdocument@npm:^1.0.8": version: 1.0.13 resolution: "vscode-languageserver-textdocument@npm:1.0.13" checksum: 10c0/1de174f1de3bfa9e1660a4b3c1b1c711497196d761e33f4d00785fdbfc556ae5a1f440db04771cbc0f6bd66c2a6f303062bc17d8dc46e76820e39fa2d5626564 @@ -42647,14 +45318,32 @@ __metadata: languageName: node linkType: hard -"vscode-languageserver-types@npm:^3.17.3": +"vscode-languageserver-types@npm:3.18.0, vscode-languageserver-types@npm:^3.15.1, vscode-languageserver-types@npm:^3.16.0, vscode-languageserver-types@npm:^3.17.3, vscode-languageserver-types@npm:^3.17.5": version: 3.18.0 resolution: "vscode-languageserver-types@npm:3.18.0" checksum: 10c0/d8c51cd286cc55b4c1f333e87fa3cefc66d2aae8c65f6209e7ecf88032d3326308b333d62b05243c194be8a3304760445b87630b298116c6c612607f011b4de0 languageName: node linkType: hard -"vscode-uri@npm:^3.0.8": +"vscode-languageserver@npm:^9.0.0, vscode-languageserver@npm:^9.0.1": + version: 9.0.1 + resolution: "vscode-languageserver@npm:9.0.1" + dependencies: + vscode-languageserver-protocol: "npm:3.17.5" + bin: + installServerIntoExtension: bin/installServerIntoExtension + checksum: 10c0/8a0838d77c98a211c76e54bd3a6249fc877e4e1a73322673fb0e921168d8e91de4f170f1d4ff7e8b6289d0698207afc6aba6662d4c1cd8e4bd7cae96afd6b0c2 + languageName: node + linkType: hard + +"vscode-nls@npm:^5.0.0, vscode-nls@npm:^5.2.0": + version: 5.2.0 + resolution: "vscode-nls@npm:5.2.0" + checksum: 10c0/dc9e48f58ebbc807f435d351008813a2ea0c9432d51e778bcac9163c0642f929ddb518411ad654e775ce31e24d6acfa8fb7db8893c05b42c2019894e08b050f9 + languageName: node + linkType: hard + +"vscode-uri@npm:^3.0.2, vscode-uri@npm:^3.0.8, vscode-uri@npm:^3.1.0": version: 3.1.0 resolution: "vscode-uri@npm:3.1.0" checksum: 10c0/5f6c9c10fd9b1664d71fab4e9fbbae6be93c7f75bb3a1d9d74399a88ab8649e99691223fd7cef4644376cac6e94fa2c086d802521b9a8e31c5af3e60f0f35624 @@ -43484,6 +46173,13 @@ __metadata: languageName: node linkType: hard +"xxhash-wasm@npm:^1.1.0": + version: 1.1.0 + resolution: "xxhash-wasm@npm:1.1.0" + checksum: 10c0/35aa152fc7d775ae13364fe4fb20ebd89c6ac1f56cdb6060a6d2f1ed68d15180694467e63a4adb3d11936a4798ccd75a540979070e70d9b911e9981bbdd9cea6 + languageName: node + linkType: hard + "y18n@npm:^3.2.1 || ^4.0.0, y18n@npm:^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3" @@ -43526,6 +46222,37 @@ __metadata: languageName: node linkType: hard +"yaml-language-server@npm:~1.23.0": + version: 1.23.0 + resolution: "yaml-language-server@npm:1.23.0" + dependencies: + "@vscode/l10n": "npm:^0.0.18" + ajv: "npm:^8.17.1" + ajv-draft-04: "npm:^1.0.0" + ajv-i18n: "npm:^4.2.0" + prettier: "npm:^3.8.1" + request-light: "npm:^0.5.7" + vscode-json-languageservice: "npm:4.1.8" + vscode-languageserver: "npm:^9.0.0" + vscode-languageserver-textdocument: "npm:^1.0.1" + vscode-languageserver-types: "npm:^3.16.0" + vscode-uri: "npm:^3.0.2" + yaml: "npm:2.8.3" + bin: + yaml-language-server: bin/yaml-language-server + checksum: 10c0/d5ed44bb542ad9b9a38171983257f47c391120fb703e043395f3490c22d99f0807d90a0cdb7b4b7955f39557162936f41bd0d6f121c95b473bc1deac597c41a1 + languageName: node + linkType: hard + +"yaml@npm:2.8.3": + version: 2.8.3 + resolution: "yaml@npm:2.8.3" + bin: + yaml: bin.mjs + checksum: 10c0/ddff0e11c1b467728d7eb4633db61c5f5de3d8e9373cf84d08fb0cdee03e1f58f02b9f1c51a4a8a865751695addbd465a77f73f1079be91fe5493b29c305fd77 + languageName: node + linkType: hard + "yaml@npm:^1.10.0, yaml@npm:^1.5.0": version: 1.10.3 resolution: "yaml@npm:1.10.3" @@ -43533,7 +46260,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.1": +"yaml@npm:^2.3.1, yaml@npm:^2.8.3": version: 2.9.0 resolution: "yaml@npm:2.9.0" bin: @@ -43643,6 +46370,20 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^18.0.0": + version: 18.1.0 + resolution: "yargs@npm:18.1.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^8.2.1" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10c0/86052bbed90a9aaaaf1eabe004f47c7420e1b7f8d314170cfb0ad4721f518ec2fb7ba5a6daa20708b1595aba257f1554b2d5b3f5606356f86aa9dae18de0bec5 + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0" @@ -43686,7 +46427,7 @@ __metadata: languageName: node linkType: hard -"yocto-queue@npm:^1.0.0": +"yocto-queue@npm:^1.0.0, yocto-queue@npm:^1.2.1": version: 1.2.2 resolution: "yocto-queue@npm:1.2.2" checksum: 10c0/36d4793e9cf7060f9da543baf67c55e354f4862c8d3d34de1a1b1d7c382d44171315cc54abf84d8900b8113d742b830108a1434f4898fb244f9b7e8426d4b8f5 @@ -43776,7 +46517,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:4.4.3, zod@npm:^3.25 || ^4.0, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.0.0, zod@npm:^4.1.5": +"zod@npm:4.4.3, zod@npm:^3.25 || ^4.0, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.0.0, zod@npm:^4.1.5, zod@npm:^4.3.6": version: 4.4.3 resolution: "zod@npm:4.4.3" checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3 @@ -43812,7 +46553,7 @@ __metadata: languageName: node linkType: hard -"zwitch@npm:^2.0.0": +"zwitch@npm:^2.0.0, zwitch@npm:^2.0.4": version: 2.0.4 resolution: "zwitch@npm:2.0.4" checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e