Repository navigation
Git Diff Impact
starchart impact --diff <base> answers the PR question: "this diff touches which pages, listings, prices and screenshots?" It maps every changed line to the code nodes it lands in, adds any fact values that moved since starchart.lock, and runs the normal cross-layer impact walk from there. This page explains how hunks become nodes, how untracked and deleted files are handled, how diff seeds combine with fact changes, how to use it in pull requests, and shows a real run.
Source: code/diff.ts and planFromDiff in api.ts.
flowchart LR
G1["git diff --unified=0 base"] --> P[parseUnifiedDiff]
G2["git ls-files --others --exclude-standard"] --> P
P --> M[changedNodes: hunk ↔ symbol ranges]
M --> S[diff seeds]
L["changedSince(graph, lock)<br/>facts only"] --> S
S --> I[planFromSeeds → computeImpact + orderSteps]
changedNodesFromDiff(root, codeConfig, graph, base) runs two commands in the project root (via execFile, no shell):
git -c core.quotePath=false diff --unified=0 --no-color --no-ext-diff --no-renames --relative --end-of-options <base> --
git -c core.quotePath=false ls-files --others --exclude-standard| Flag | Why |
|---|---|
--unified=0 |
Hunks carry exact changed line ranges, no context lines |
--no-renames |
A rename is a delete plus an add, so both ends are seen |
--relative |
Paths come out relative to the project root even inside a monorepo |
core.quotePath=false + unquoting |
Non-ASCII paths (café.swift) survive; C-style quoted paths are decoded too |
--end-of-options |
Everything after it is a revision or path, never a flag (see Revision validation) |
<base> -- |
Compares the working tree (staged and unstaged) with base
|
base is any revision git accepts: HEAD, main, origin/main, a SHA, or a range such as main...HEAD (then git compares commits and working-tree edits are ignored). Untracked files that are not gitignored are always added as whole-file changes.
execFile stops shell injection, but git still parses its own arguments. A base like --output=/some/file would be read as a flag and make git write wherever it points. Since 0.1.1, assertSafeRev refuses any base that is empty, starts with -, or contains a NUL, CR or LF, and git gets --end-of-options as a second fence. The CLI exits 2 before git runs:
$ starchart impact --diff=--output=/tmp/pwned
starchart: invalid git revision: "--output=/tmp/pwned"
$ echo $?
2
$ ls /tmp/pwned
ls: /tmp/pwned: No such file or directory
The same check guards the library call (changedNodesFromDiff throws) and the MCP tool starchart_diff_impact (see MCP Server). Normal revisions (HEAD, origin/main, main...HEAD, SHAs) are unaffected.
Each file in the diff becomes a FileChange:
| Field | Source |
|---|---|
ranges |
@@ -a,b +c,d @@ with d > 0: new-file lines c … c+d-1 were added or modified |
gaps |
d = 0: lines were deleted between new-file line c and c+1
|
whole |
new file mode, Binary files …, or untracked |
deleted |
deleted file mode or +++ /dev/null (the old path is kept) |
Then, per change:
-
File node. The matching
file:/test:node. If the file is not in the graph (deleted, or brand new and not yet ingested) and it classifies as code or MDX in some scope, the id is computed anyway (file:<scope>/<rel>ortest:…). Files outside every scope, and non-code files (Markdown, YAML, images) that carry no annotations, produce nothing. - Deleted files stop there: the file id is a seed, its old symbols are gone from the graph.
-
Symbols. Every symbol located in the file whose line range (or each of its overload
ranges) overlaps a changed range. A gap counts when it falls inside a symbol (start ≤ gap < end). Awholechange seeds every symbol in the file. Containers overlap too: editingEntitlements.proFeaturesalso seeds the enclosingEntitlementsenum. - Localization keys. For catalog files, each key "owns" the lines from its own line up to the next key. Keys whose span overlaps a hunk are seeds. If a catalog changed but no key span matched (say, a reformat at the top of the file), every key in that file is seeded.
Seeds are de-duplicated and sorted.
planFromDiff(project, base, extra?):
export async function planFromDiff(project: Project, base: string, extra?: ImpactOptions): Promise<Plan>- Diff seeds from step 2 (ids only, no before/after).
- Plus every fact whose value differs from
starchart.lock(changedSince(graph, lock)filtered tokind === "fact"), with its lockedbeforeand currentafter. Code-hash drift from the lock is deliberately left out; the diff already says which code moved. - De-duplicated, then
planFromSeeds(): impact walk, classification, rollout ordering.
Why step 2 matters: editing a constant that backs a code-authority fact, or a value in .starchart/entities/*.yaml, changes a fact. Those YAML edits never appear as code nodes, but the plan still carries them, with their old and new values.
A temp copy of the demo, committed, then one Swift line changed:
D=$(mktemp -d)/u && cp -R examples/pro-universe $D && cd $D
git init -q && git add -A && git commit -qm "chart the universe"
# add a Pro feature in code
sed -i '' 's/\["Themes", "iCloud sync"\]/["Themes", "iCloud sync", "Widgets"]/' apps/ios/Sources/Core/Entitlements.swift$ git diff --unified=0
diff --git a/apps/ios/Sources/Core/Entitlements.swift b/apps/ios/Sources/Core/Entitlements.swift
index 6be7fc3..2e35395 100644
--- a/apps/ios/Sources/Core/Entitlements.swift
+++ b/apps/ios/Sources/Core/Entitlements.swift
@@ -5 +5 @@ enum Entitlements {
- static let proFeatures = ["Themes", "iCloud sync"]
+ static let proFeatures = ["Themes", "iCloud sync", "Widgets"]
$ starchart impact --diff HEAD
Change: file:ios/Sources/Core/Entitlements.swift
Change: symbol:ios/Entitlements
Change: symbol:ios/Entitlements.proFeatures
Change: addon:pro.features ["Themes","iCloud sync"] → ["Themes","iCloud sync","Widgets"]
! manual appstore:screenshots/6.9/03 captures screen changed; re-capture
~ auto symbol:ios/StarchartFacts.AddonPro.features anchors regenerate fact constants (codegen)
~ auto symbol:web/lib/starchart-facts#ADDON_PRO_FEATURES anchors regenerate fact constants (codegen)
? review appstore:listing/description describes describes this semantically
? review web:pricing-page describes describes this semantically
? review email:onboarding-day-3 describes describes this semantically
? review web:landing-hero describes describes this semantically
✗ retire reel:spring-2026 promotes expired 2026-06-30
✓ tests test:ios/Tests/PaywallTests.swift tests run these tests
Order: appstore:listing/description → appstore:screenshots/6.9/03 → symbol:ios/StarchartFacts.AddonPro.features → symbol:web/lib/starchart-facts#ADDON_PRO_FEATURES → email:onboarding-day-3 → web:landing-hero → web:pricing-page → reel:spring-2026
Plan: 1 manual · 2 auto · 4 review · 1 retire · 1 tests
(3 informational items hidden; use --verbose)
Read it top to bottom:
- One hunk at line 5 seeded the file, the
proFeaturesconstant and its enclosingEntitlementsenum. -
addon:pro.featuresis a code-authority fact sourced from that constant, so its value moved relative to the lock. It joins the seeds with its before/after values. - The screenshot is
manualthrough the code path:proFeaturesis referenced byPaywallView.body, which is thePaywallscreen the screenshotcaptures. - The two generated constants are
auto:starchart applyregenerates thecodegen:targets for them (or runstarchart codegenyourself).
The same run as a PR comment (-f markdown), trimmed:
$ starchart impact --diff HEAD -f markdown
## 🌌 STARCHART blast radius
**4 changes:**
- `file:ios/Sources/Core/Entitlements.swift`
- `symbol:ios/Entitlements`
- `symbol:ios/Entitlements.proFeatures`
- `addon:pro.features` `["Themes","iCloud sync"]` → `["Themes","iCloud sync","Widgets"]`
| | Class | Count |
|---|---|---:|
| ! | Manual | 1 |
| ~ | Auto-fixable | 2 |
| ? | Needs review | 4 |
| ✗ | Retire | 1 |
| ✓ | Tests to run | 1 |
<details open><summary><b>! Manual</b> (1)</summary>
| Artifact | Via | Reason | Why |
|---|---|---|---|
| `appstore:screenshots/6.9/03` | captures | screen changed; re-capture | <code>symbol:ios/Entitlements.proFeatures --references--> symbol:ios/PaywallView.body --references--> symbol:ios/PaywallView --references--> screen:ios/Paywall --captures--> appstore:screenshots/6.9/03</code> |
</details>
…
### Rollout order
1. `appstore:listing/description` (review)
2. `appstore:screenshots/6.9/03` (manual)
…
---
<sub>Charted by <b>STARCHART</b> · run <code>starchart plan</code> locally for details</sub>
break and manual sections render open; the rest collapse. Each class section is capped at 50 rows. -f json gives the stable PlanJson structure (changes, items with why and path, steps, cycles, summary).
Committing on a branch and diffing against the merge base works the same way:
$ starchart impact --diff main...HEAD | head -4
Change: file:ios/Sources/Core/Entitlements.swift
Change: symbol:ios/Entitlements
Change: symbol:ios/Entitlements.proFeatures
Change: addon:pro.features ["Themes","iCloud sync"] → ["Themes","iCloud sync","Widgets"]
The bundled GitHub Action runs exactly this:
npx --yes "@space-pirate-zero/starchart@latest" impact --diff "origin/${base}" --format markdown > "$RUNNER_TEMP/starchart.md"(with latest replaced by the starchart-version input) and posts the result as a sticky comment. To run the same thing as plain workflow steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # the base branch must exist locally
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: git fetch origin main
- run: npx --yes @space-pirate-zero/starchart impact --diff origin/main -f markdown > blast-radius.md
- run: npx --yes @space-pirate-zero/starchart check # optional: fail on lockfile driftNever bare npx starchart unless the package is in your devDependencies: the unscoped npm package is someone else's.
Notes:
-
impact --diffnever fails the build on findings; it exits 0 whatever the plan says. Gate withstarchart check(exit 1 on stale artifacts) orstarchart rules. - Shallow clones break the diff. Fetch the base ref.
- A bad base is a hard error (exit 2):
starchart: git core.quotePath=false diff … nosuchref -- failed: fatal: bad revision 'nosuchref'. - An option-shaped base is a hard error too (exit 2):
starchart: invalid git revision: "-x". - Seeds are code nodes, so
--all-codeis often useful in review to see every impacted symbol, not just the surface (screens, routes, tests, anchors).
import { buildProject, changedNodesFromDiff, planFromDiff, formatPlanMarkdown } from "@space-pirate-zero/starchart";
const project = await buildProject(process.cwd());
const ids = await changedNodesFromDiff(project.root, project.loaded.config.code, project.graph, "origin/main");
const plan = await planFromDiff(project, "origin/main", { includeCode: "all" });
console.log(formatPlanMarkdown(plan, { title: "Blast radius", maxItems: 20 }));The MCP server exposes the same thing as starchart_diff_impact.
- Only files inside a configured code scope map to nodes. A changed Markdown email or a YAML artifact file is not a seed by itself (YAML fact edits still arrive through the lock comparison).
- Renames are delete + add, so a moved file's symbols get new ids and old pins read as removed.
- A diff that only touches lines outside every symbol (imports, blank lines between declarations) seeds just the file node, which rarely reaches the world layer. That is by design.
- The git error message prints
core.quotePath=falseas if it were a subcommand argument (cosmetic: only-cis filtered from the echo).
★ 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