Repository navigation
Impact Analysis
Impact analysis answers one question: if this changes, what else has to change? STARCHART walks the graph from the changed nodes across all three layers, then classifies each thing it reaches: fix automatically, fix by hand, review, retire, update code, or run tests. This page documents the traversal step by step, the code-reporting modes, every branch of the classifier, the impact classes and their symbols, confidence, and the "why" paths. Source: core/impact.ts and api.ts.
| Entry point | Seeds |
|---|---|
starchart impact <refs…> |
the nodes you name (ids, file paths, unique suffixes) |
starchart impact --diff <base> |
code nodes touched by git diff <base>, plus facts changed since the lock (Git Diff Impact) |
starchart plan |
every fact and code node whose hash differs from starchart.lock, plus the changed dependencies of artifacts that are still stale (Lockfile and Drift) |
starchart why <from> <to> |
one seed, then reports the single path to <to>
|
computeImpact(graph, seeds, options) |
library (Library API) |
All of them call computeImpact, keep the actionable items (auto, review, manual, retire, break, code) and hand those to Rollout Ordering.
plan (planFromLock) does one more thing: it drops artifacts that are locked and already in sync, so plan and check always agree. After an apply, the artifacts it fixed are relocked and vanish from the plan, while the manual, review, code and retire items stay listed until you fix them or starchart ack them. It never falls back to "no changes" while something is still stale. After changing the price to 5.99 and running starchart apply --yes, starchart plan prints:
Change: addon:pro.price {"usd":4.99,"eur":4.99} → {"usd":5.99,"eur":4.99}
Change: addon:pro.price.usd 4.99 → 5.99
Change: symbol:ios/StarchartFacts.AddonPro.Price.usd ∅ → 5.99
Change: symbol:web/lib/starchart-facts#ADDON_PRO_PRICE_USD ∅ → 5.99
! manual appstore:iap/pro-monthly mirrors update in appstore (adapter is read-only)
…
⌘ code symbol:web/lib/stripe#PRICE_PRO_MONTHLY anchors holds this artifact's external id; update it if the id changes
✗ retire reel:spring-2026 embeds expired 2026-06-30
✓ tests test:ios/Tests/PaywallTests.swift tests run these tests
…
Plan: 5 manual · 1 review · 2 code · 1 retire · 1 tests
The ∅ → 5.99 lines are the constants apply just regenerated. Their hashes moved and the still-stale artifacts depend on them, so they're seeds too; the lock pins code by hash only, so there's no old value to show.
| Option | Default | Meaning |
|---|---|---|
maxDepth |
12 |
Maximum hops from a seed. |
maxCodeDepth |
4 (or code.maxCodeDepth from config) |
Maximum consecutive hops inside the code layer. Stops "everything imports everything" from swallowing the plan. The lockfile records the same value, so check walks the same depth. |
minConfidence |
0.3 |
Paths whose confidence drops below this are pruned. |
edgeTypes |
all | Only traverse these edge types. |
includeCode |
"surface" |
Which code nodes to report (all are traversed): surface, all, none. |
canWrite(adapter, node) |
adapter === "fs" in the library; the CLI asks the adapter registry with your config and the artifact node |
Decides auto vs manual, per artifact. |
now |
current time | Used for validThrough expiry. |
The CLI exposes --all-code (includeCode: "all") and -v/--verbose (show info items and why-paths). why runs with includeCode: "all" and minConfidence: 0. The other options are library-only.
- Seed. Drop seeds that aren't in the graph. Each remaining seed enters the queue at depth 0, code depth 0, confidence 1, empty path, and is marked visited.
-
Pop the next entry (breadth-first, FIFO). If its depth is already
maxDepth, don't expand it. -
Find neighbours that a change to this node impacts (
neighbors()):- for each incoming edge whose propagation is
reverseorboth: the edge'sfrom; - for each outgoing edge whose propagation is
forwardorboth: the edge'sto; - for each incoming
containsedge: the containing file, with the edge confidence forced to 0.8. A changed symbol means its file changed.
- for each incoming edge whose propagation is
-
Filter each neighbour, in order:
- edge type not in
edgeTypes→ skip; - already visited → skip (the first, shortest path wins);
- node missing → skip;
- if both ends are code nodes, code depth + 1, else reset to 0; above
maxCodeDepth→ skip; - confidence = current × edge confidence (default 1) ×
DECAY[type](default 1); belowminConfidence→ skip.
- edge type not in
-
Enqueue the neighbour with depth + 1, the new code depth and confidence, and the path extended by one hop
{from: current, to: neighbour, type}. Mark it visited. - When the queue drains, report every visited non-seed node. The item's
viais the edge type of the path's last hop. Code nodes are filtered byincludeCode. Everything left is classified. - Sort by depth, then id. (The text formatter re-sorts by class urgency for display.)
Because visited is set on enqueue, each item carries its shortest path (fewest hops). Its confidence is the confidence of that path, not the best possible path.
The decay table:
| Edge | Decay |
|---|---|
references |
0.95 |
readsEnv, readsFlag
|
0.9 |
imports |
0.85 |
dependsOn |
0.7 |
| others | 1 |
Every code node is traversed, because the path to a screenshot often runs through five symbols. What gets reported depends on includeCode:
| Mode | Code nodes reported |
|---|---|
surface (default) |
screens, routes, tests, and any code node reached via anchors
|
all (--all-code) |
every code node reached |
none |
none; only facts and world artifacts |
In surface mode, a file annotated with @starchart displays … is traversed but not listed. Use --all-code to see it:
· info file:web/public/banner.html displays file affected
why: addon:pro.price.usd --displays--> file:web/public/banner.html (confidence 1)
classify() looks at the reached node's layer, then (for artifacts) at expiry, retired entities and the last edge type. Branches in evaluation order:
| # | Condition | Class | Reason text |
|---|---|---|---|
| 1 | code node, kind: test
|
test |
run these tests |
| 2 | code node reached via anchors, meta.generated set |
auto |
regenerate fact constants (codegen) |
| 3 | code node reached via anchors from an artifact (the last hop's from is an artifact) |
code |
holds this artifact's external id; update it if the id changes |
| 4 | code node reached via anchors, otherwise (from a fact) |
code |
hardcoded value anchors this fact; update or switch to codegen |
| 5 | any other code node | info |
<kind> affected |
| 6 | fact-layer node (fact or entity) | info |
derived fact changes |
| 7 | artifact with validThrough in the past |
retire |
expired <date> |
| 8 | a node on the path (a hop's from) has status: retired, and via is promotes, embeds or describes
|
retire |
depends on a retired entity |
| 9 | via renders
|
auto |
regenerate from template |
| 10 | via embeds, artifact type is media (schema:ImageObject, schema:VideoObject, schema:MediaObject, sc:Print) |
manual |
value is burned into media |
| 11 | via embeds, the binding's adapter can write this artifact |
auto |
replace embedded value |
| 12 | via embeds, otherwise |
manual |
adapter "<id>" cannot write, or no binding
|
| 13 | via mirrors, the binding's adapter can write this artifact |
auto |
sync via <adapter> |
| 14 | via mirrors, otherwise |
manual |
update in <adapter> (adapter is read-only), or no binding
|
| 15 | via captures
|
manual |
screen changed; re-capture |
| 16 | via describes
|
review |
describes this semantically |
| 17 | via promotes
|
review |
promotes this; check it still holds |
| 18 | via derivedFrom
|
review |
derived from a changed artifact |
| 19 | via publishes
|
review |
published page changed |
| 20 | via emits
|
review |
emitted event/ID changed |
| 21 | via anything else | review |
impacted via <type> |
Things worth knowing:
- Expiry (rule 7) beats everything, including
renders. An expired promo reel isretireno matter how it was reached. - Rule 8 checks the entity's
statusfield, which comes from the entity doc'sstatus:. It only fires forpromotes,embedsanddescribes; anembedsof a leaf fact whose path doesn't pass through the entity is not retired. - Only artifacts are checked for
validThrough. An expired entity does not retire anything. - Writability is decided per artifact. The CLI calls the registry's
canWrite(adapter, settings, node): the adapter must declare write support and implementapply; if it implements the optionalcanApply(node), that must return true for this binding; thenfswrites unlessadapters.fs.write: false, while every other adapter (stripe,appstore, plugin adapters) needswrite: true.urland unknown adapters never write. - The App Store adapter only applies bindings with a
field(listing text) and noset/iap. Screenshots (set) and IAPs (the demo's hasproduct, nofield) staymanualeven withadapters.appstore.write: true, andapplyskips them instead of failing. The IAP's reason then readsappstore cannot update this binding; update it by hand: the adapter can write, just not that binding. Withoutwrite: trueit readsupdate in appstore (adapter is read-only). - Rules 3 and 4 split anchored code by where the change came from.
symbol:web/lib/stripe#PRICE_PRO_MONTHLYanchors the Stripe artifact, not a fact, so it holds an external id (price_…) rather than the fact's value: update it only if the id changes. - Generated constants (rule 2, symbols marked
// @starchart generated) areautobecausestarchart applyregenerates every configured Codegen target once when the plan contains them. No need to runstarchart codegenfirst. Codegen output isn't journaled, sorevertdoesn't undo it (git does).
| Class | Symbol | Meaning | In rollout order? |
|---|---|---|---|
break |
✗ | Reserved. classify() never produces it; audits report breaks separately (Audit and Break Detection). |
yes |
manual |
! | A human has to change it (read-only adapter, burned-in media, re-capture). | yes |
auto |
~ |
starchart apply can fix it through an adapter, a template render, or codegen. |
yes |
review |
? | Semantic dependency; someone should read it. | yes |
code |
⌘ | A hardcoded literal anchors the fact (update it or switch to codegen), or holds an anchored artifact's external id. | yes |
retire |
✗ | Expired, or depends on a retired entity; take it down. | yes |
test |
✓ | Tests to run (shown as tests). |
no |
info |
· | Context: derived facts, entities, affected screens/routes/files. Hidden unless -v. |
no |
The text output lists classes in this order: break, manual, auto, review, code, retire, tests, info.
Every item carries its path as a list of hops. explainPath renders it as:
<seed> --<type>--> <node> --<type>--> <node> …
Hops are written in propagation order (from the change outwards), not edge direction. addon:pro.price.usd --embeds--> web:pricing-page means "the price change reached the page across an embeds edge", even though the stored edge is web:pricing-page --embeds--> addon:pro.price.usd. -v prints the path and confidence under each item. starchart why prints just one:
starchart why addon:pro.price.usd test:ios/Tests/PaywallTests.swiftaddon:pro.price.usd --anchors--> symbol:ios/Pricing.proUSD --references--> symbol:ios/PaywallView.body --references--> symbol:ios/PaywallView --tests--> test:ios/Tests/PaywallTests.swift
class test · confidence 0.58 · run these tests
why exits 1 and prints <to> does not depend on <from> when there's no path.
starchart impact addon:pro.price.usdChange: addon:pro.price.usd
! manual appstore:listing/description embeds adapter "appstore" cannot write
! manual appstore:screenshots/6.9/03 embeds value is burned into media
! manual stripe:price/pro-monthly mirrors update in stripe (adapter is read-only)
! manual web:live-pricing embeds adapter "url" cannot write
! manual appstore:iap/pro-monthly mirrors update in appstore (adapter is read-only)
~ auto email:onboarding-day-3 embeds replace embedded value
~ auto symbol:ios/StarchartFacts.AddonPro.Price.usd anchors regenerate fact constants (codegen)
~ auto symbol:web/lib/starchart-facts#ADDON_PRO_PRICE_USD anchors regenerate fact constants (codegen)
~ auto web:messages-en embeds replace embedded value
~ auto web:og-pro renders regenerate from template
~ auto web:pricing-page embeds replace embedded value
? review web:landing-hero describes describes this semantically
⌘ code symbol:ios/Pricing.proUSD anchors hardcoded value anchors this fact; update or switch to codegen
⌘ code symbol:web/lib/stripe#PRICE_PRO_MONTHLY anchors holds this artifact's external id; update it if the id changes
✗ retire reel:spring-2026 embeds expired 2026-06-30
✓ tests test:ios/Tests/PaywallTests.swift tests run these tests
Order: stripe:price/pro-monthly → appstore:iap/pro-monthly → appstore:listing/description → appstore:screenshots/6.9/03 → symbol:ios/Pricing.proUSD → symbol:ios/StarchartFacts.AddonPro.Price.usd → symbol:web/lib/starchart-facts#ADDON_PRO_PRICE_USD → symbol:web/lib/stripe#PRICE_PRO_MONTHLY → email:onboarding-day-3 → web:landing-hero → web:messages-en → web:og-pro → web:pricing-page → web:live-pricing → reel:spring-2026
Plan: 5 manual · 6 auto · 1 review · 2 code · 1 retire · 1 tests
(5 informational items hidden; use --verbose)
How some of these were reached, from -v:
! manual appstore:iap/pro-monthly mirrors update in appstore (adapter is read-only)
why: addon:pro.price.usd --partOf--> addon:pro.price --mirrors--> appstore:iap/pro-monthly (confidence 1)
? review web:landing-hero describes describes this semantically
why: addon:pro.price.usd --partOf--> addon:pro.price --partOf--> addon:pro --describes--> web:landing-hero (confidence 1)
⌘ code symbol:web/lib/stripe#PRICE_PRO_MONTHLY anchors holds this artifact's external id; update it if the id changes
why: addon:pro.price.usd --mirrors--> stripe:price/pro-monthly --anchors--> symbol:web/lib/stripe#PRICE_PRO_MONTHLY (confidence 1)
· info app:nebula partOf derived fact changes
why: addon:pro.price.usd --partOf--> addon:pro.price --partOf--> addon:pro --partOf--> app:nebula (confidence 1)
Notice the Stripe price id constant: it isn't anchored to the price, it's anchored to the Stripe artifact (// @starchart anchors stripe:price/pro-monthly). Stripe prices are immutable, so a new amount usually means a new price and a new price_… id, and then the constant has to change too. The classifier knows the last hop came from an artifact and says so in the reason. anchors propagates both ways, which makes that chain possible.
starchart impact apps/ios/Sources/Paywall/PaywallView.swift -vChange: file:ios/Sources/Paywall/PaywallView.swift
Change: symbol:ios/PaywallView
Change: symbol:ios/PaywallView.body
! manual appstore:screenshots/6.9/03 captures screen changed; re-capture
why: symbol:ios/PaywallView --references--> screen:ios/Paywall --captures--> appstore:screenshots/6.9/03 (confidence 0.95)
✗ retire reel:spring-2026 captures expired 2026-06-30
why: symbol:ios/PaywallView --references--> screen:ios/Paywall --captures--> reel:spring-2026 (confidence 0.95)
✓ tests test:ios/Tests/PaywallTests.swift tests run these tests
why: symbol:ios/PaywallView --tests--> test:ios/Tests/PaywallTests.swift (confidence 0.8)
· info screen:ios/Paywall references screen affected
why: symbol:ios/PaywallView --references--> screen:ios/Paywall (confidence 0.95)
Order: appstore:screenshots/6.9/03 → reel:spring-2026
Plan: 1 manual · 1 retire · 1 tests · 1 info
Set status: retired on addon:pro and run starchart plan -v:
Change: addon:pro.status "active" → "retired"
~ auto symbol:ios/StarchartFacts.AddonPro.status anchors regenerate fact constants (codegen)
~ auto symbol:web/lib/starchart-facts#ADDON_PRO_STATUS anchors regenerate fact constants (codegen)
✗ retire email:onboarding-day-3 describes depends on a retired entity
why: addon:pro.status --partOf--> addon:pro --describes--> email:onboarding-day-3 (confidence 1)
✗ retire reel:spring-2026 promotes expired 2026-06-30
✗ retire web:landing-hero describes depends on a retired entity
…
Plan: 2 auto · 3 retire · 2 info
impact and plan take -f text|markdown|json. JSON items include id, class, layer, kind, via, reason, confidence, depth, why (the rendered path) and path (the hops). Steps include waitsFor. See CLI Reference.
- Classification is structural.
reviewmeans "a human should read this", not "this is wrong". LLM claim checking isn't built yet. - Swift, Kotlin and Go references are resolved by name (no SCIP yet), hence confidence below 1 on those edges.
- The reported confidence belongs to the shortest path, not the strongest.
★ 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