Repository navigation
Architecture
How STARCHART is built: the package layout, how data flows from YAML and source files to plans and outputs, the design decisions behind it, which modules may depend on which, what we know about performance, and how it's tested. Read this before Contributing.
| Path | What lives there |
|---|---|
packages/starchart/ |
The library, the CLI (starchart / sc) and the MCP server binary (starchart-mcp). Published to npm as @space-pirate-zero/starchart. |
packages/xray/ |
Reality X-Ray, a Manifest V3 browser extension (plain JS, no build step). |
action/ |
The GitHub Action (action.yml, a composite action). |
examples/pro-universe/ |
The demo universe: SwiftUI app + Next.js site + Stripe + App Store, with a full chart. |
.github/workflows/ |
ci.yml (typecheck, test, build, demo check) and wiki.yml (publishes docs/wiki/ to the GitHub wiki). See Contributing. |
| Directory / file | Responsibility |
|---|---|
core/ |
The graph model (model.ts: node kinds, layers, edge types, propagation), the Graph class, impact traversal and classification (impact.ts), rollout ordering (order.ts), the lockfile and drift (lock.ts: buildLock, relockArtifacts, staleArtifacts, changedSince). No I/O. |
config/ |
Zod schemas for config, entities, artifacts and edges (schema.ts); finding the root and loading every YAML document under .starchart/ (load.ts). |
compiler/ |
YAML documents → fact and world layers plus declared edges; flattening nested facts; resolving code-authority facts once code is ingested. |
code/ |
The code layer. File discovery and classification, a TypeScript-compiler-API parser for TS/JS, a shared lexer plus parsers for Swift, Kotlin and Go, Next.js routes, packages from manifests and lockfiles, i18n files, env/flag/event signals, @starchart annotations, secret redaction of literal values (redact.ts), and git diff → changed nodes (with revision validation). |
bridge/ |
The literal scanner behind scan and discovery, and heuristic edge discovery. |
adapters/ |
fs, url, stripe, appstore, the registry (registerAdapter, and the canWrite(id, settings, node?) policy that also asks the adapter's canApply), and shared text find-and-replace helpers. |
engine/ |
audit with break detection, apply / revert / ack with journals (apply also runs the codegen pass for generated constants), the Future Universe preview, adapter context. |
rules/ |
The invariant engine, the built-in packs (core, appstore, privacy, seo) and registerPack for plugin packs, the SDK privacy catalog, and a plist parser for PrivacyInfo.xcprivacy. |
analysis/ |
Orphans, Reality Score (and badge), change cost. |
render/ |
The {{ fact | filter }} template engine, OG image rendering (SVG → PNG via resvg), JSON-LD. |
codegen/ |
Facts → TypeScript, Swift and Kotlin constants. |
viewer/ |
The self-contained HTML star chart, the local server (serve), the X-Ray payload. |
format/ |
Plan and stale-report formatting: text, markdown, JSON. |
mcp/, hooks/
|
The MCP server and the Claude Code PostToolUse hook. |
cli/ |
Commander wiring (main.ts), init and discovery (init.ts), the bin entry. |
project.ts |
buildProject(): load → plugins → compile → tokens → ingest → resolve code facts → read lock. |
plugins.ts |
loadPlugins(root, specifiers): imports each module in config plugins: once per process and registers its adapters and packs. Called from buildProject, so the CLI, MCP server, serve and the hook all see plugins. |
api.ts |
High-level operations shared by CLI, MCP, hook and Action: planFromLock, planFromDiff, planFromSeeds, check, resolveRef, why, query. |
tokens.ts, history.ts
|
DTCG design-token import; the fact time machine over git history. |
paths.ts |
resolveInRoot(root, path): resolves a path against the project root and throws unless it stays inside, following symlinks (a path that doesn't exist yet is judged by its nearest existing parent). Every write goes through it: the fs adapter, codegen, and the binding rewrites in apply / revert. |
regex-safety.ts |
unsafeRegexReason(pattern): rejects config regexes over 500 characters or with nested quantifiers ((a+)+). Used by rule value.pattern validation and fs regex: selectors. A heuristic, not a proof. |
version.ts |
VERSION, read from package.json at runtime, so --version, about and the MCP server version can't drift from the published package. |
index.ts |
The public library surface. See Library API. |
flowchart LR
Y[".starchart/*.yaml"] --> L["config/load<br/>zod-validated docs"]
L --> P["plugins.ts<br/>register adapters + packs"]
L --> C["compiler<br/>facts + world + declared edges"]
T["DTCG tokens"] -.-> G
S["source files<br/>manifests, i18n"] --> I["code/ingest<br/>files, symbols, routes,<br/>screens, packages, …"]
C --> G(("Graph"))
I --> G
G --> R["resolveCodeFacts<br/>authority: code → anchors"]
R --> G2(("Project<br/>graph + lock"))
K["starchart.lock"] --> G2
G2 --> IM["impact · plan · why"]
G2 --> LK["lock · check"]
G2 --> RU["rules · privacy · orphans · score · cost"]
G2 --> AD["adapters: audit · apply · revert"]
P -.-> AD
IM --> O["text / markdown / JSON<br/>PR comment · hook · MCP"]
AD --> J["files · external systems<br/>codegen · journal · relock"]
G2 --> X["codegen · JSON-LD · viewer · X-Ray"]
Every command starts with buildProject(), which rebuilds the whole graph in memory from the repo. There's no daemon and no persistent cache: the repo is the input, the graph is a pure function of it. Plugins listed in config are imported once per process before compiling, so their adapters and packs are registered before anything asks for them. Three commands skip code ingestion because they don't need it: journals, history and adapters. (emit jsonld always ingests code, so code-authority facts have values.)
starchart.lock pins, per artifact, the value of every fact and the hash of every code node it depends on. dependencies() walks at most maxCodeDepth code hops (config code.maxCodeDepth, default 4). When the config sets it, the lock records it as maxCodeDepth, so staleness is computed with the same depth the dependencies were pinned with.
-
buildLock(graph, previous, artifactIds?, { maxCodeDepth })pins the given artifacts (all of them when omitted). Fullstarchart lockuses it and resets everything. -
relockArtifacts(graph, previous, ids, opts)pinsidsbut keeps the previous value of every fact that a still-stale artifact depends on.apply,ackand partialstarchart lock <ids>use it, so shipping one artifact doesn't make the others forget what changed. -
planFromLock(api.ts) seeds the impact walk fromchangedSince(lock)plus the changed dependencies of every stale artifact, then drops locked artifacts that are already in sync. That's whyplanandcheckalways agree: after an apply,plankeeps listing the remaining manual, review, code and retire items until they're acked or fixed.
apply runs only auto steps, in rollout order. Each artifact goes to its adapter's apply(), which returns an undo record for the journal. Generated fact constants (// @starchart generated symbols) aren't applied one by one: the first one triggers a single codegen pass over every codegen: target, reported as [codegen] steps. Codegen output isn't journaled (it's reproducible from the facts; git undoes it), and with no targets configured those steps fail with "generated constants found but no codegen targets are configured". Finally the journal is written and the applied artifacts are relocked with relockArtifacts. See Apply, Revert and Journals.
STARCHART trusts your .starchart/ config like code (plugins run as JavaScript) and treats everything else as data. The 0.1.1 hardening put each boundary in one place:
| Boundary | Where it's enforced |
|---|---|
| Writes stay in the project |
paths.ts (resolveInRoot), called by adapters/fs.ts, codegen/index.ts, engine/apply.ts
|
| Secrets stay out of the graph |
code/redact.ts (redactLiteral), called by code/ingest.ts
|
| Git revisions can't become flags |
code/diff.ts (assertSafeRev plus --end-of-options), and a leading-- check in mcp/server.ts
|
| Config regexes can't hang the process |
regex-safety.ts, called by rules/engine.ts and adapters/fs.ts
|
serve talks only to loopback and the extension |
viewer/serve.ts (Host check, extension-only CORS, --allow-remote) |
The full trust model and how to report a vulnerability: Security.
Git is the database. The chart is YAML in .starchart/, the state is starchart.lock, the undo log is .starchart/journal/*.json. All reviewed in PRs, all versioned. The time machine (history) is just git log over the lock. Nothing is written to the outside world without apply.
Extract code, author the world. Code nodes are always derived from source and never hand-maintained. You only declare facts, world artifacts and the occasional bridge. If a code id is wrong, you fix the code or the scope, not the YAML.
Plan / apply split. plan and impact never write. apply runs only auto steps, journals an undo record per adapter write, and relocks only what it applied. Everything else becomes a checklist. External adapters are read-only until adapters.<id>.write: true; fs writes by default because it only touches your repo. Write access is also per binding: an adapter can implement canApply(node) to refuse artifacts it can't write (App Store screenshots and IAPs), and those stay manual even with write: true.
Fact authority. Each fact records where truth lives: graph (the YAML, default), code (read from a symbol), or an external system name like appstore. Authority settles who wins and keeps sync from looping. A code-authority fact changes when the code changes, and the plan treats that like any YAML edit.
Stable ids. kind:scope/name, e.g. symbol:ios/Entitlements.proFeatures, route:web/pricing, addon:pro.price.usd. Ids are derived deterministically from paths and qualified names, so they're identical across machines and platforms. Renaming a scope or moving a file changes ids; there's no moniker-based identity yet (SCIP isn't integrated). See Node IDs.
Precision over recall. Impact is edge-typed and direction-aware (each edge type declares how change propagates), bridges are required to cross layers, confidence decays across coarse code edges and items under 0.3 are dropped, code hops are capped at 4 by default, and only surface code (screens, routes, tests, anchoring symbols) is reported unless you ask. Every line carries its "why" path. One false alarm a week and people uninstall.
Zero infra. One npm package, no server, no database, no account, no LLM. The viewer is a single HTML file. serve is optional and binds to localhost.
JSON-LD is an export, not a runtime. Internally it's a plain in-memory graph. schema.org vocabulary plus an sc: namespace show up only at the boundary (emit jsonld).
Derived from the actual imports in src/ (simplified: a few secondary edges, like viewer → adapters, are left out):
flowchart BT
core
config
compiler --> core & config
code --> core & config
bridge --> code & core
render --> compiler & config & core
adapters --> core & render
rules --> core
plugins["plugins.ts"] --> adapters & rules & config
project["project.ts"] --> code & compiler & config & core & plugins
api["api.ts"] --> project & adapters & code & core
codegen --> project & config & core
engine --> api & adapters & project & codegen
analysis --> rules & adapters & core
format --> api & core
mcp & hooks & viewer --> api & format
cli --> api & engine & rules & analysis & viewer & mcp & hooks
The rules to keep:
-
core/andconfig/import nothing else fromsrc/.core/does no I/O at all. -
code/knows nothing about facts or the world. It depends only oncore/andconfig/. -
Adapters don't import the engine. They receive an
AdapterContext(root, graph, settings, env, fetch, previous fact values, dryRun) and return diffs or anApplyResultwith an undo record. The interface isid,capabilities,audit(), and optionalapply(),revert(),canApply(node)andlist(). See Writing an Adapter. -
Plugins go through the registries.
plugins.tsonly callsregisterAdapterandregisterPack; built-in packs can't be replaced. -
Only
cli/sees everything. Integrations (MCP, hook, Action) go throughapi.tsso they all compute the same plan. - Everything the outside needs is re-exported from
index.ts.
What we can state from measurements:
- Ingest. A synthetic tree of 2,000 source files (1,200 TS, 800 Swift) ingests into 16,418 nodes and 30,800 edges. On an Apple M3 Max running Node 22 under heavy unrelated load (load average around 50), three runs took 3.1–4.1 s. That's a pessimistic ceiling, not a benchmark; an idle machine is faster. There's no controlled benchmark yet.
-
Concurrency. File reads during ingest run 32 at a time;
auditchecks 4 artifacts at a time. - Limits. Code files over 2 MB and text files over 1 MB are skipped.
-
No incremental cache. Every command re-ingests. The design target in PLAN.md is under 1 s for
impacton a warm cache and under 5 s on a PR in CI; content-hash caching is how we intend to get there.
-
Vitest, configured in
packages/starchart/vitest.config.tsto runsrc/**/*.test.tsandtest/**/*.test.tswith a 20 s timeout. Tests are colocated with the code they cover: 31 test files undersrc/plus 3 undertest/. -
Fixtures.
src/code/__fixtures__/universe/is a multi-language mini-repo (Next.js, SwiftUI, Compose/Gradle, Go,.xcstrings,strings.xml,Package.resolved,libs.versions.toml) used by the ingest, parser and diff tests.src/rules/fixtures/holds a privacy-manifest app and a rules file. -
End-to-end.
test/e2e.test.tscopiesexamples/pro-universeto a temp dir and runs the real CLI in-process (run([... "-C", dir, "-q", ...])), capturing stdout. It covers: in sync as committed; a price change planned across code, facts and world (asserting the class of each item); apply → check → revert from the journal (file contents and the PNG header); privacy drift and the unbound EUR price;whyfrom a Swift file to a screenshot; JSON-LD and the Reality Score; andinit --discoveron a stripped copy.test/regressions.test.tspins fixes that came out of documenting the tool (plan/check agreement after apply, per-binding writes, codegen in apply, plugins, CLI polish).test/security.test.tspins the 0.1.1 security fixes: redaction,serveCORS and Host checks, option-shaped git revisions, symlink escapes in apply and codegen, and unsafe regexes. -
Adapters without the network. Stripe, App Store and url tests inject
fetchthrough the adapter context, so the suite never touches external APIs. -
CI (
.github/workflows/ci.yml) runs on Ubuntu and macOS with Node 22:pnpm install --frozen-lockfile,pnpm typecheck,pnpm test,pnpm build, thenstarchart -C examples/pro-universe checkto prove the committed demo is in sync. - PLAN.md reports 278 tests at v0.1.0. At v0.1.1 the suite is 301 tests in 34 files.
★ 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