{"_id":"@amulet-laboratories/astrolabe","_rev":"2-882e608b987359593b35f75c3632ce6d","name":"@amulet-laboratories/astrolabe","dist-tags":{"latest":"0.4.0"},"versions":{"0.4.0":{"name":"@amulet-laboratories/astrolabe","version":"0.4.0","keywords":["nuxt","vue","static-analysis","architecture","diagram","dependency-graph","dead-code","documentation","code-visualization"],"author":{"name":"Amulet Laboratories"},"license":"MIT","_id":"@amulet-laboratories/astrolabe@0.4.0","maintainers":[{"name":"sndhl","email":"sndhl@pm.me"}],"homepage":"https://github.com/Amulet-Laboratories/astrolabe#readme","bugs":{"url":"https://github.com/Amulet-Laboratories/astrolabe/issues"},"bin":{"astrolabe":"bin/astrolabe.mjs"},"dist":{"shasum":"adb4ceeb5033841e534b0cbc27686cfd5afbd2ec","tarball":"https://registry.npmjs.org/@amulet-laboratories/astrolabe/-/astrolabe-0.4.0.tgz","fileCount":26,"integrity":"sha512-PyitRocwdFe5+brza6AqKI55oFtYE48BD6TNTjMKo/ertFFGbrPdxZx56br1CEyt4wztHzaTWm3G80Faef/2mQ==","signatures":[{"sig":"MEYCIQDTfP7LZ9kDomEbkAKB5l+2MUxPmDHCBk7rU3AK+2GxywIhANYmBR7GDIOd4VEBvZkNdJkne5dYCyfeW8I55LRNx+Mr","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@amulet-laboratories%2fastrolabe@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":170959},"type":"module","types":"./src/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./src/index.d.ts","default":"./src/index.mjs"},"./schema":{"types":"./src/index.d.ts","default":"./src/core/schema.mjs"}},"gitHead":"f90d66b1457ded3a67277728112fe9c5591f1a94","scripts":{"lint":"eslint .","test":"vitest run","check":"eslint . && prettier --check . && tsc --noEmit && vitest run","start":"node bin/astrolabe.mjs","types":"tsc --noEmit","format":"prettier --write .","test:watch":"vitest","format:check":"prettier --check ."},"_npmUser":{"name":"sndhl","email":"sndhl@pm.me"},"repository":{"url":"git+https://github.com/Amulet-Laboratories/astrolabe.git","type":"git"},"_npmVersion":"10.9.8","description":"Astrolabe maps a Nuxt or Vue app into one interactive diagram — routes, components, composables, stores, data sources and the services they talk to — plus the findings the diagram won't make you notice.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"@vue/compiler-sfc":"^3.5.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.30.2","devDependencies":{"jsdom":"^30.0.0","eslint":"^10.7.0","vitest":"^4.1.10","globals":"^17.7.0","prettier":"^3.9.5","@eslint/js":"^10.0.1","typescript":"^7.0.2","eslint-config-prettier":"^10.1.8"},"_npmOperationalInternal":{"tmp":"tmp/astrolabe_0.4.0_1785181876392_0.4520448481542334","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Superseded by @amulet-laboratories/observatory — the source lens now ships there as observatory-astrolabe"}},"time":{"created":"2026-07-27T19:51:16.129Z","modified":"2026-07-29T08:42:25.957Z","0.4.0":"2026-07-27T19:51:16.627Z"},"bugs":{"url":"https://github.com/Amulet-Laboratories/astrolabe/issues"},"author":{"name":"Amulet Laboratories"},"license":"MIT","homepage":"https://github.com/Amulet-Laboratories/astrolabe#readme","keywords":["nuxt","vue","static-analysis","architecture","diagram","dependency-graph","dead-code","documentation","code-visualization"],"repository":{"url":"git+https://github.com/Amulet-Laboratories/astrolabe.git","type":"git"},"description":"Astrolabe maps a Nuxt or Vue app into one interactive diagram — routes, components, composables, stores, data sources and the services they talk to — plus the findings the diagram won't make you notice.","maintainers":[{"name":"sndhl","email":"sndhl@pm.me"}],"readme":"# Astrolabe\n\n[![npm](https://img.shields.io/npm/v/@amulet-laboratories/astrolabe.svg)](https://www.npmjs.com/package/@amulet-laboratories/astrolabe)\n[![CI](https://github.com/Amulet-Laboratories/app-graph/actions/workflows/ci.yml/badge.svg)](https://github.com/Amulet-Laboratories/app-graph/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@amulet-laboratories/astrolabe.svg)](./LICENSE)\n\nStatic analyzer that maps a **Nuxt or Vue** app into one interactive diagram — routes, components, composables, stores, content collections, server routes and the external services they talk to, plus the edges between them.\n\nThe goal is to open one file and understand an app you've never seen: what the pages are, what each renders, and where its data comes from.\n\n![An Astrolabe report for nuxt/movies](docs/hero.png)\n\n> _One self-contained HTML file for [nuxt/movies](https://github.com/nuxt/movies) — pan, zoom, ⌘K to any node, and the findings the diagram won't make you notice._\n\n## Install\n\n```bash\n# run it without installing\nnpx @amulet-laboratories/astrolabe ./my-app --open\n\n# or add it to a project\nnpm i -D @amulet-laboratories/astrolabe\n```\n\n```bash\nastrolabe ./my-app --open        # interactive report\nastrolabe ./my-app --svg -o docs/architecture.svg\nastrolabe diff main-graph.json pr-graph.json   # what changed\n```\n\nOr import it:\n\n```js\nimport { scan, renderHtml } from '@amulet-laboratories/astrolabe'\nconst graph = await scan('./my-app') // typed graph JSON\n```\n\n## Usage\n\n```\nastrolabe [dir] [options]\nastrolabe diff <base> <head> [options]\n\n  dir              Path to the Nuxt or Vue app (default: current directory)\n  diff             Compare two apps or two graph JSON files, base → head\n\n  -o, --out FILE   Output path (default: astrolabe.<html|json|svg>)\n  --json           Emit graph JSON instead of HTML (for diff: JSON not Markdown)\n  --svg            Emit a static SVG snapshot instead of HTML\n  --view NAME      View for --svg: layers (default), render, data, modules, sitemap, cleanup\n  --schema         Print the canonical graph schema and exit\n  --open           Open the result in your default browser\n  -h, --help       Show this help\n```\n\nThe HTML is fully self-contained — no network, no build step. Drag to pan, scroll to zoom, click any node to see its file, its data access, and everything connected to it in both directions. The chips along the top filter entity types, and the diagram re-flows to fit whatever is left.\n\n## Living docs\n\nThe graph is only useful if it stays current, so three outputs are built to run in CI and keep it honest:\n\n- **A static SVG** (`--svg`) of any view, rendered server-side with no browser — for embedding in a README or a docs page that can't run JavaScript. It uses the exact same layout engine as the interactive report, so the two can't disagree.\n- **A graph diff** (`astrolabe diff base head`) that reports what changed _architecturally_ between two versions — \"added 2 routes, a new external service dependency, 1 orphan\" — rather than line by line. Each side is either an app directory or a saved `--json` graph. Default output is Markdown for a PR comment; `--json` gives the structured delta.\n- **A ready-made PR workflow** in [`examples/architecture-diff.yml`](examples/architecture-diff.yml): on every pull request it scans base and head, posts the diff as a sticky comment, and uploads the full report + SVG as artifacts.\n\n## Views\n\nThere is one graph. A view is a projection of it — which nodes it keeps, which edges it keeps, and what a column means. Everything else (layout, grouping, the detail panel) is shared, so each view is a different question asked of the same data rather than a different diagram.\n\n| View            | The question it answers                                                                                                                                                    |\n| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Layers**      | _What is in this app?_ Every entity, in columns by kind. The overview.                                                                                                     |\n| **Render tree** | _What renders what?_ Only `renders`/`wraps` edges, columns are nesting depth, so the leftmost column is the true entry points.                                             |\n| **Data flow**   | _Where does data come from and where does it end up?_ Every edge reversed, so it reads origin → who reads it → where it surfaces. Nodes that never touch data are dropped. |\n| **Modules**     | _Who consumes what?_ Folders and packages collapsed to one block each, with line weight showing how many references cross between them. A whole app in ~30 boxes.          |\n| **Sitemap**     | _What are the pages, and what powers each?_ Laid out from the routes, forward — everything a page reaches. Whatever no page reaches (dead code) drops away.                |\n| **Cleanup**     | _What's safe to delete?_ Only what the findings flag as warnings — dead components, uncalled composables, empty collections, render cycles — gathered in one place.        |\n\n**Group** buckets nodes inside each column by owner — the directory a file lives in, or the package it came from. Directories already encode how the app is organised (`components/hub`, `server/api/sync`), so this is grouping you wrote rather than grouping the tool invented. It's off in Modules, where the module already _is_ the group.\n\n**Isolate** (in the detail panel, `f`, or Esc to clear) reduces any view to one node plus everything it reaches and everything that reaches it. This is the fastest way to ask \"if I change this, what breaks?\" It persists across view switches, so you can isolate a collection and then flip to Data flow to see where it surfaces.\n\n## The overview\n\nWith nothing selected, the side panel is a briefing on the repo rather than a prompt: size, findings, composition, the most-referenced entities, the largest files, the stack it runs on, and a legend. Every name in it is clickable and jumps to that node.\n\n**Findings** are the things the diagram won't make you notice — dead components, composables nothing calls, endpoints with no handler, render cycles, collections whose glob matched nothing. They're computed once in [`src/core/findings.mjs`](src/core/findings.mjs) and shared by the CLI, `--json`, and the report, so all three can never disagree.\n\nThe bar for a finding is that it is **actionable and true**. A confidently wrong finding costs more trust than a missing one earns, so anything the analyzer can't be sure of is worded for what was actually checked — \"17 server routes with no fetch call\" (which is a fact) rather than \"17 dead routes\" (which would be a guess, and wrong for redirects reached by an `href`).\n\n## Keyboard\n\n`1`–`6` switch views · `/` search · `g` group · `f` isolate the selection · `Esc` clear\n\nThe view, selection and isolation are kept in the URL, so any particular reading of the graph can be shared as a link.\n\n## What it reads\n\nEverything comes from Nuxt's own conventions, so there's nothing to configure:\n\n| Entity                       | Source                                                                                                                      |\n| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| Routes                       | `pages/**` file-based routing, including `[param]` and `[...catchall]`                                                      |\n| Components                   | `components/**`, resolved to their auto-import names                                                                        |\n| Composables                  | `composables/**` plus auto-imports from libraries — including their bodies, which is where most apps keep their data access |\n| Content collections          | `content.config.ts` collections, their Zod schema fields, and their file counts (markdown, JSON and YAML alike)             |\n| Server routes                | `server/api/**`, `server/routes/**`, with HTTP method                                                                       |\n| Layouts, middleware, plugins | `layouts/**`, `middleware/**`, `plugins/**`                                                                                 |\n| Stack                        | Nuxt version, modules, dependencies, engines, package manager                                                               |\n| Size                         | non-blank lines per file, aggregated per node and repo                                                                      |\n\nA caller's `` `/api/debate/${slug}/vote` `` and its handler `server/api/debate/[slug]/vote.post.ts` are the same endpoint, so dynamic segments are collapsed to `*` when matching. Without that the caller invents a phantom endpoint and the real handler looks uncalled — two wrong facts from one mismatch.\n\nEdges are extracted per file: `renders` from template tags and from components used in markdown (all three MDC syntaxes — `::block`, `:inline{}`, and plain `<PascalCase />` tags), `queries` from `queryCollection()`, `fetches` from `$fetch`/`useFetch` (splitting internal API routes from external hosts), `uses` from composable calls, `reads` from `useAppConfig`/`useRuntimeConfig`/`useState`, and `wraps`/`guards` from `definePageMeta`.\n\nBecause markdown render paths count, a component used only from content is not mistaken for an orphan — and one that really is unused gets flagged as such.\n\n### `.nuxt` makes it sharper\n\nWhen `.nuxt/` is present (after any `nuxt dev`, `nuxt build`, or `nuxt prepare`), the tool reads `components.d.ts` and `imports.d.ts` for the exact set of auto-imports Nuxt resolved. That's what lets it show **library** components and composables — the ones coming from Rig, `@nuxt/content`, and Nuxt itself — attributed to their package.\n\nWithout `.nuxt/`, it falls back to scanning `components/` by convention and prints a warning. You still get your own app's structure; you just lose the library layer.\n\n### Layers\n\nApps that `extends` a local base layer are followed: the layer's pages, components, composables, layouts, middleware, server routes and content are merged into the graph as if they were the app's own, with the app winning on any name collision (its `pages/index.vue` overrides the layer's). Only local layers — a relative or absolute path — are resolved; remote layers (`github:…`, an npm package) would need a package install and are left out. A layer that itself extends another is followed too.\n\nLibrary entities that nothing references are dropped: a dependency exports far more than any one app uses, and listing the unused half describes the dependency rather than your app. Unconnected _local_ components are kept and flagged as \"Nothing renders this component\" — that's a finding, not noise.\n\n## Reading the diagram\n\nColumns run left to right in dependency order: entry and layouts → routes → your components → library components → composables → data and services. Within a column, nodes are ordered by barycenter so connected things line up. Selecting a node dims everything it doesn't touch, which is the fastest way to answer \"what does this page actually depend on?\"\n\n## Architecture\n\nThe engine is split into a framework-neutral **core** and per-framework **adapters**, so\nsupporting another framework is adding an adapter rather than changing everything downstream:\n\n| Path                        | What lives there                                                                                                                                                                                                    |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `src/core/`                 | The `Graph` builder, the `finalize` pipeline (prune → group → findings → stats), the HTML renderer + client, and `schema.mjs` — the canonical node/edge vocabulary every adapter targets. Knows about no framework. |\n| `src/adapters/nuxt/`        | Everything Nuxt-specific: file-based routing, `content.config.ts`, `.nuxt/*.d.ts`, SFC parsing. Emits canonical nodes and edges.                                                                                    |\n| `src/adapters/registry.mjs` | Adapter list + `detectAdapter(dir)` — first adapter whose `detect()` matches wins.                                                                                                                                  |\n| `src/index.mjs`             | Public API: `scan(dir)` detects the adapter, runs it, and finalizes; plus `renderHtml`, the schema, and the registry.                                                                                               |\n\nAn adapter is a module exporting `{ name, detect(dir), scan(dir) }`, where `scan` returns\n`{ project, graph, warnings }` in the canonical schema. [`test/conformance.test.mjs`](test/conformance.test.mjs)\nis the contract every adapter must pass, independent of any framework — pointing a new adapter at\nit is what proves the core stayed neutral.\n\n## Developing\n\n```bash\npnpm install\npnpm test          # vitest, runs against fixture apps in test/fixtures/\npnpm check         # lint + format + test, what CI runs\n```\n\nLint and format mirror `@amulet-laboratories/config` rule-for-rule. They're inlined rather than imported because that package isn't published, and its only consumable form (`file:../Rig/config`) has no sibling checkout in CI. Swap to the shared package once it ships to the registry.\n\n### The testing bar\n\nThis tool's entire value is being right about a repo. It almost never fails loudly — when it's wrong it reports something false with total confidence, and someone deletes code because of it. So the standard is:\n\n**Every bug gets a fixture, not just a fix.** `test/fixtures/` holds small real Nuxt apps; tests assert against the graph they produce. Each regression test names the false claim it prevents. The bugs already pinned there — composable bodies unparsed, `type: 'data'` collections reported empty, phantom endpoints, missed MDC tags, mis-parsed one-line props — were all found by hand on real repos, and every one would have shipped again without a fixture.\n\nThat's also why fixtures beat testing against your own apps: the one-line props bug was invisible across all 8 Amulet repos because they all happen to format multi-line. Fixtures let you write the shape you _don't_ have.\n\n## Limitations\n\nAnalysis is lexical, not type-aware. It recognizes the conventional Nuxt call shapes, which is what the diagram is for. Known blind spots, all of which affect what you should conclude from a finding:\n\n- A collection or endpoint named at runtime (`queryCollection(someVar)`, `$fetch(url)`) isn't traced — only literal and template strings are.\n- A component rendered via `<component :is>` isn't traced.\n- **Links aren't traced.** A server route reached by an `href` (a `/go/:slug` redirect) shows no incoming edge, which is why the finding says \"no fetch call\" rather than \"dead\".\n- A composable file's data access is attributed to every composable that file exports. Files almost always export one; where they don't, this over-attributes rather than dropping the edge.\n- Dynamic route params are shown as the pattern, not the pages they expand to.\n- Only local `extends` layers are followed. A remote layer (`github:…`, an npm package) is not fetched, so its entities are absent rather than wrong.\n","readmeFilename":"README.md"}