Skip to content

Node IDs

Greg Chambers edited this page Sep 29, 2026 · 1 revision

Every node in the chart has a string id, and every edge, lock pin, annotation and CLI argument refers to nodes by that id. This page lists every id convention: entities and facts (yours to choose) and code ids (generated by the ingestor, one format per node kind and language). It also covers how scopes prefix code ids, why the ids are designed to be stable, and how the CLI resolves shorthand refs. Every example below was produced by starchart query on the demo or on the ingestor's test fixtures.

Stable ids

Code ids are built from names, not positions: scope + path + qualified name, never line numbers or hashes. Move a function down a file, reformat it or change its body, and its id stays put. Only its hash changes, which is exactly what the lockfile tracks. Rename or move a symbol and you get a new id. That's a real change, and STARCHART treats it as one (the old id vanishes from the graph, so dependents pinned to it go stale).

The same goes for facts: addon:pro.price.usd means the same thing forever, which is why it can live in YAML, annotations, lockfiles and PR comments.

Fact layer ids

Kind Format Example
entity any string you choose; convention <kind>:<name> addon:pro, app:nebula
fact <entity>.<key>[.<key>…] addon:pro.price.usd
implicit status fact <entity>.status addon:pro.status
design token entity tokens (fixed) tokens
design token tokens.<dtcg path> tokens.color.brand.primary

Pick entity ids with a colon. @starchart annotations read targets until the first word that has neither a : nor a . (see Annotations). Dotted ids without a colon work, so // @starchart anchors tokens.color.brand.primary anchors a token, but a bare entity id with neither character (the tokens entity itself, or a hypothetical brand) can't be an annotation target. Avoid . inside fact keys; it's the path separator.

Code layer ids

Code ids always start with a kind prefix. Most of them include the scope name from code.scopes in Configuration:

Kind Format Example
file file:<scope>/<path in scope> file:ios/Sources/Core/Pricing.swift
test file test:<scope>/<path in scope> (replaces file:) test:ios/Tests/PaywallTests.swift
symbol symbol:<scope>/… (per language, below) symbol:ios/Pricing.proUSD
screen screen:<scope>/<Name> screen:ios/Paywall
route route:<scope>/<url path> route:web/pricing
root route route:<scope>/ route:web/
i18n key i18n:<scope>/<key> i18n:ios/paywall.title
package pkg:<manager>/<name> (no scope) pkg:npm/stripe
env var env:<NAME> (no scope) env:STRIPE_SECRET_KEY
feature flag flag:<key> (no scope) flag:new-paywall
analytics event event:<name> (no scope) event:pro_checkout_started

Packages, env vars, flags and events are global: two scopes reading STRIPE_SECRET_KEY point at the same env:STRIPE_SECRET_KEY node.

Symbols per language

Language Format Examples
TypeScript / JavaScript symbol:<scope>/<path without extension>#<qualified name> symbol:web/lib/pricing#PRO_PRICE_USD, symbol:web/app/pricing/page#PricingPage, symbol:web/lib/starchart-facts#ADDON_PRO.name (object member), symbol:web/app/api/checkout/route#POST
Swift symbol:<scope>/<Type path>.<member> (no file, no module) symbol:ios/Pricing.proUSD, symbol:ios/StarchartFacts.AddonPro.Price.usd (nested types)
Kotlin symbol:<scope>/<Type path>.<member> (no file, no package) symbol:android/Pricing.PRO_USD, symbol:android/Tier.isPaid
Go symbol:<scope>/<package dir>.<name> symbol:api/internal/pricing.ProUSD, symbol:api/internal/pricing.Plan.Total (method), symbol:api/main.main

Details that matter:

  • TS: the extension (.ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .mdx) is stripped; index is kept (lib/index#foo). export default <expr> becomes #default. Class and object members are Owner.member.
  • Swift: ids come from the type path only, so an extension Pricing in Pricing+EUR.swift adds members to the same symbol:ios/Pricing node. When one id is declared in several files, STARCHART keeps one node, combines the hashes and lists the other files in meta.alsoIn. Top-level functions are symbol:<scope>/<name>.
  • Kotlin: members use the enclosing type path. A top-level extension function fun Tier.label() becomes Tier.label.
  • Go: the namespace is the file's directory relative to the scope. A file at the scope root uses its package name (main), falling back to main. Methods are <ns>.<Receiver>.<Method>.
  • Test files get no symbol nodes. They're a single test: node with tests edges.

Screens

Source Rule Example
SwiftUI top-level struct X: View, name minus a trailing View or Screen PaywallView → screen:ios/Paywall
Jetpack Compose @Composable fun XScreen(...), name minus Screen PaywallScreen → screen:android/Paywall
TS/React no automatic detection; use @starchart screen <Name> screen:web/Checkout
Annotation with a full id @starchart screen screen:web/Checkout used as-is

Routes (Next.js)

Routes are detected only in scopes that have a next.config.* file or a next dependency in package.json.

Router File Route id
App app/page.tsx route:web/
App app/pricing/page.tsx route:web/pricing
App app/(marketing)/about/page.tsx route:web/about (route groups dropped)
App app/blog/[slug]/page.tsx route:web/blog/[slug]
App app/api/checkout/route.ts route:web/api/checkout (API handler)
Pages pages/index.tsx route:web/
Pages pages/about.tsx route:web/about

src/app and src/pages work too. Segments starting with _ are private and produce no route. @slot parallel-route segments are dropped from the URL. The route's label and meta.url carry the leading slash (/pricing).

Packages

Package manager Manifest(s) Id Example
npm package.json (plus bare imports of undeclared packages) pkg:npm/<name> pkg:npm/@sentry/nextjs
SwiftPM Package.resolved, Package.swift, project.pbxproj pkg:swift/<identity, lowercased> pkg:swift/purchases-ios
Gradle build.gradle(.kts), libs.versions.toml pkg:gradle/<group>:<artifact> pkg:gradle/com.revenuecat.purchases:purchases
Go go.mod pkg:go/<module path> pkg:go/github.com/stripe/stripe-go/v76

i18n

i18n:<scope>/<key>, where the key comes from .xcstrings, *.lproj/*.strings, Android res/values*/strings.xml, or JSON files under a locales, locale, i18n, messages, lang or translations directory. The node's value maps locale → string:

i18n     i18n:ios/paywall.title = {"en":"Unlock Nebula Pro","ja":"Nebula Pro を解放"}
i18n     i18n:web/pricing.cta = {"en":"Get Nebula Pro for $4.99/month"}

Scopes

code.scopes maps a scope name to a directory. The name becomes the id prefix, so pick short, stable names (ios, web, api). Rules from code/files.ts:

  • Default is { app: "." }.
  • Paths in ids are relative to the scope directory: apps/ios/Sources/Core/Pricing.swift in scope ios: apps/ios is file:ios/Sources/Core/Pricing.swift.
  • Nested scopes win: each file belongs to exactly one scope, the most specific one.
  • Files outside every scope are not ingested.
  • Renaming a scope renames every code id under it. Relock afterwards.

Resolving refs on the CLI

Commands that take a node ref (impact, why, node) resolve it with resolveRef in api.ts:

  1. An exact node id.
  2. A file path, relative to your current directory. It resolves to the file (or test) node plus every symbol it contains.
  3. A unique id suffix (Pricing.proUSD, or anything after a /) or an exact label. Multiple matches return up to 25 ids.
starchart impact apps/ios/Sources/Paywall/PaywallView.swift
Change: file:ios/Sources/Paywall/PaywallView.swift
Change: symbol:ios/PaywallView
Change: symbol:ios/PaywallView.body
…

Use starchart query --prefix <p>, --kind <k> or --text <t> to find ids.

See also

Clone this wiki locally