Repository navigation
Orphans
Orphans are dead stars: things in your chart that nothing depends on anymore. That includes facts no code or artifact uses, artifacts linked to nothing, promos that expired but are still live, dependencies no file imports, and Stripe prices no artifact or code refers to. It's impact analysis run backwards. This page lists every orphan kind with its exact detection rule and the guards that keep the noise down, and shows real starchart orphans output. Source: analysis/orphans.ts.
starchart orphans # text
starchart orphans -f json # [{ id, kind, message }]
starchart orphans --external # also ask adapters what exists in the real world (Stripe only)-f takes text or json, nothing else. -f markdown is rejected before anything runs (exit 1):
$ starchart orphans -f markdown
error: option '-f, --format <format>' argument 'markdown' is invalid. Allowed choices are text, json.
With nothing to report, the text format prints ✓ no orphans.
starchart orphans always exits 0. It reports, it doesn't gate. To fail CI on something specific, turn it into a rule (see Rules Engine). The core pack already covers fact-used and promo-not-expired.
Results are sorted by kind in this order: expired, external-unreferenced, artifact-unlinked, fact-unused, package-unused, env-unused, flag-unused, event-unconsumed. Within a kind, they're sorted by id.
| Kind | Detected when | Message |
|---|---|---|
expired |
An artifact isn't status: retired and its validThrough has passed (a bare date lasts through the end of that day). |
<id> expired on <date> but is not retired |
external-unreferenced |
With --external: an active resource an adapter lists isn't referenced by any artifact binding or code literal. |
<adapter> <label> (<externalId>) is active but no artifact or code references it |
artifact-unlinked |
An artifact has no outgoing edges at all. | <id> does not embed, render, mirror or describe anything |
fact-unused |
A leaf fact has no dependents, directly or through its containers or entity. | <id> is not used by any code or artifact |
package-unused |
A direct dependency of a linked package manager has no incoming dependsOn edge. |
<id> is a dependency but no file uses it |
env-unused |
An env node has meta.declaredOnly: true and no incoming edges. |
<id> is declared but nothing reads it |
flag-unused |
A flag node has meta.rollout of 100 or "100%". |
<id> is rolled out to 100%; remove the flag and its dead branch |
event-unconsumed |
An event node has no edges other than emits, and at least one other event does. |
<id> is emitted but no dashboard, funnel or artifact consumes it |
This uses the same check as the core pack's fact-used rule:
- Only leaf facts are considered, meaning facts with no child facts. Containers like
addon:pro.priceare skipped. Their leaves are checked instead. - A fact is used if any edge other than
partOfpoints at the fact, or at any of its containers or its entity. A change to a leaf propagates up throughpartOf, so an artifact thatdescribes: addon:prouses everyaddon:pro.*fact. - The compiler-generated
<entity>.statusfact is always skipped. Nobody is expected to bind to it. - A
partOfedge from another entity (of: app:nebula) doesn't count as use. That's why the demo's fourapp:nebula.*facts show up.
Any outgoing edge counts: embeds, renders, describes, mirrors, promotes, captures, derivedFrom, even the ordering-only after and blocks. The demo's privacy:appstore-label declares its data types in meta, not as edges, so it's reported. That's expected for disclosure artifacts, and you can ignore it.
Unused-dependency detection is notoriously noisy, so three guards apply:
-
Linked package managers only. A package is only considered if at least one package from the same package manager (the
pkg:<type>/prefix:npm,swift,gradle,go, …) has an incomingdependsOnedge, which proves the code layer links files to packages there. If nothing is linked, nothing is reported for that package manager. -
Direct dependencies only. Packages with
meta.transitive: true(for examplego.mod // indirect, orPackage.resolvedpins the project never declares) are skipped. -
Implicit packages. npm packages used without an import are skipped: frameworks that own the build or runtime, toolchains and type packages. The exact list (
IMPLICIT_NPM):next,react,react-dom,typescript,@types/*,eslint*,@eslint/*,prettier,tailwindcss,@tailwindcss/*,postcss,autoprefixer,vite,vitest,jest,@vitejs/*,tsx,ts-node,turbo,nodemon,husky,lint-staged
The implicit list covers npm only. A package that's wired in through config instead of an import (for example, Sentry's Next.js integration set up in next.config) can still be reported. The demo's @sentry/nextjs is one.
An event is consumed when it has any incoming or outgoing edge other than emits, for example a dashboard artifact that describes or embeds it. The kind is only reported once the project models at least one consumer. If no event has a consumer, nothing is reported. Otherwise every analytics event would be an orphan on day one.
Both depend on metadata that no built-in ingestor sets today. Code ingestion creates env nodes only for variables the code reads, and flag nodes only for flags the code checks. Neither sets declaredOnly or rollout. In practice:
-
flag-unused fires when you add
meta.rollout: 100to a flag node. Code nodes can't be declared in YAML directly, but a YAML document with the same id (id: flag:new_paywall,meta: { rollout: 100 }) is merged into the ingested node. This is the same mechanism as privacy overrides (see Privacy Drift). -
env-unused needs an
envnode withmeta.declaredOnly: truethat nothing reads. Since ingestion never creates unread env nodes, this is only reachable through the Library API today.
With --external, STARCHART collects every adapter used by at least one artifact binding. For each adapter that supports list, it calls list() to enumerate live resources. Only the Stripe adapter lists today (active prices), and it needs STRIPE_SECRET_KEY. Not supported yet: App Store in-app purchases, url targets and fs files are never enumerated, so --external finds nothing for them. For each active listed resource, it's referenced if:
- any code-layer node's string value equals its external id (for example, a
price_…literal instripe.ts), or - an artifact bound to the same adapter has any binding value equal to the external id, or
- an artifact's binding matches all of the listed resource's binding keys.
Unreferenced resources are reported with the id <adapter>:<externalId>. If an adapter's list() fails (for example, missing credentials), a warning goes to stderr and that adapter is skipped:
warn stripe: Stripe secret key not found: set the STRIPE_SECRET_KEY environment variable
The starchart_orphans MCP tool never runs external listing.
From a temp copy of examples/pro-universe:
$ starchart orphans
expired reel:spring-2026 reel:spring-2026 expired on 2026-06-30 but is not retired
artifact-unlinked privacy:appstore-label privacy:appstore-label does not embed, render, mirror or describe anything
fact-unused app:nebula.appStoreId app:nebula.appStoreId is not used by any code or artifact
fact-unused app:nebula.category app:nebula.category is not used by any code or artifact
fact-unused app:nebula.name app:nebula.name is not used by any code or artifact
fact-unused app:nebula.operatingSystem app:nebula.operatingSystem is not used by any code or artifact
package-unused pkg:npm/@sentry/nextjs pkg:npm/@sentry/nextjs is a dependency but no file uses it
starchart orphans --external without Stripe credentials prints the warning above, then the same list.
JSON (first two of the seven entries):
[
{
"id": "reel:spring-2026",
"kind": "expired",
"message": "reel:spring-2026 expired on 2026-06-30 but is not retired"
},
{
"id": "privacy:appstore-label",
"kind": "artifact-unlinked",
"message": "privacy:appstore-label does not embed, render, mirror or describe anything"
}
]| Kind | Typical fix |
|---|---|
expired |
Take it down and set status: retired (it then leaves the Reality Score too), or extend validThrough. |
external-unreferenced |
Archive the Stripe price, or add the artifact that should mirror it. |
artifact-unlinked |
Add embeds / describes / mirrors so changes reach it, or delete it. |
fact-unused |
Bind it (an artifact embeds it, code anchors it) or delete it. |
package-unused |
Remove the dependency, or accept it if it's wired in through config. |
flag-unused |
Delete the flag and its dead branch. |
event-unconsumed |
Add the dashboard or funnel that should use it, or stop emitting it. |
★ STARCHART
☠ a Space Pirate Zero joint
STARCHART Every dependency. Code to cosmos.
Guides
Concepts
- Three-Layer Model
- Facts and Entities
- Artifacts and Bindings
- Edge Types
- Node IDs
- Impact Analysis
- Lockfile and Drift
- Rollout Ordering
Authoring
Code layer
Commands & API
Adapters & engine
- Adapters Overview
- Adapter: fs
- Adapter: url
- Adapter: Stripe
- Adapter: App Store Connect
- Writing an Adapter
- Audit and Break Detection
- Apply, Revert and Journals
- Future Universe Preview
- Viewer and Serve
- Reality X-Ray
Rules & analysis
Integrations
Project