Repository navigation
Contributing
How to hack on STARCHART: set up the monorepo, what CI checks, how the wiki is edited, the conventions the code follows, and step-by-step recipes for the four most common contributions (a language ingestor, an adapter, a rule pack, an SDK privacy catalog entry). Read Architecture first for the module map.
You need Node 20+ (CI uses 22), pnpm 10 (the repo pins pnpm@10.33.0 via packageManager; corepack enable gets you the right one) and git.
git clone https://github.com/space-pirate-zero/starchart.git && cd starchart
pnpm install && pnpm buildThe repo is a pnpm workspace (pnpm-workspace.yaml, one pnpm-lock.yaml). npm is blocked on purpose: the root package.json sets engines.npm to please-use-pnpm, and .npmrc sets engine-strict=true, so npm install at the root refuses to run:
$ npm install
npm error code EBADENGINE
npm error engine Unsupported engine
npm error engine Not compatible with your version of node/npm: undefined
npm error notsup Not compatible with your version of node/npm: undefined
npm error notsup Required: {"npm":"please-use-pnpm","pnpm":">=10"}
npm error notsup Actual: {"npm":"10.9.8","node":"v22.22.3"}
That's the guard working, not a bug. Use pnpm install. pnpm is unaffected, and package-lock.json and yarn.lock are gitignored.
Why the guard exists: an npm install at the root once added the published @space-pirate-zero/starchart as a root devDependency plus a package-lock.json, which broke pnpm install --frozen-lockfile in CI. Never add a root dependency on the published package. The workspace builds STARCHART from packages/starchart; depending on the npm copy of itself only shadows your local changes.
Users install the published package (npm i -D @space-pirate-zero/starchart); this from-source build is for working on STARCHART itself. To try your build on another repo, alias it:
alias starchart="node $PWD/packages/starchart/dist/cli/bin.js"Root scripts run across the workspace (pnpm -r):
| Command | What it runs |
|---|---|
pnpm build |
tsc -p tsconfig.build.json then scripts/copy-assets.mjs (copies viewer assets and rule-pack data into dist/) |
pnpm test |
vitest run |
pnpm typecheck |
tsc -p tsconfig.json --noEmit |
Inside packages/starchart there's also a dev script that runs the CLI from source with tsx, no build needed:
pnpm --filter @space-pirate-zero/starchart dev --cwd ../../examples/pro-universe impact addon:pro.price.usdThe script runs from packages/starchart, so paths are relative to it. Prefer the long --cwd form here, since -C is also a pnpm flag.
The security regression tests live in packages/starchart/test/security.test.ts: redaction, serve CORS and Host checks, git revision validation, symlink escapes, unsafe regexes. Touch any of those areas and that file must stay green. See Security.
Run one test file while you work:
cd packages/starchart && pnpm vitest run src/core/core.test.tsBefore you open a PR, run what CI runs: pnpm typecheck && pnpm test && pnpm build, then make sure the demo is still in sync:
node packages/starchart/dist/cli/bin.js -C examples/pro-universe check.github/workflows/ci.yml runs on every pull request and every push to main:
| Step | What it runs |
|---|---|
| Matrix |
ubuntu-latest and macos-latest (fail-fast: false), Node 22, pnpm from packageManager
|
| Install | pnpm install --frozen-lockfile |
| Check |
pnpm typecheck, pnpm test, pnpm build
|
| Demo in sync | node packages/starchart/dist/cli/bin.js -C examples/pro-universe check |
The last step fails if a change leaves the committed demo stale against its starchart.lock. If you changed the demo on purpose, update its artifacts (or starchart lock it) and commit the new lock. A frozen lockfile means a dependency change must include the updated pnpm-lock.yaml.
The wiki source lives in the repo under docs/wiki/, one Markdown file per page (Impact-Analysis.md → "Impact Analysis"). .github/workflows/wiki.yml copies it to the GitHub wiki on every push to main that touches docs/wiki/** (or on a manual run).
-
Edit through a PR to
docs/wiki/, not the wiki web editor. The sync mirrors the folder withrsync --delete, so anything edited only on the wiki is overwritten on the next sync. - Link pages as
[Impact Analysis](Impact-Analysis), no.md. - Paste real CLI output from a temp copy of the demo, never from memory.
- A behavior change without a wiki update is an incomplete PR.
-
ESM with NodeNext resolution. Relative imports end in
.jseven in.tsfiles:import { Graph } from "../core/graph.js";. -
Strict TypeScript, including
noUncheckedIndexedAccess. Index access returnsT | undefined; handle it. Target ES2022. -
Tests are colocated:
foo.tssits next tofoo.test.ts. End-to-end tests go inpackages/starchart/test/. Fixtures live in__fixtures__/orfixtures/next to the tests that use them; they're excluded from the build. -
No placeholders. No
TODOstubs, mock implementations or fake data in shipped code. If a feature isn't done, it isn't merged; if a limitation exists, say so in the code comment and the wiki. -
No network in tests. Adapters receive
fetchthroughAdapterContext; tests pass a fake. -
Assemble fake credentials at runtime. GitHub push protection blocks commits containing credential-shaped literals, even obviously fake ones. Build them from parts in the test:
const FAKE_STRIPE_KEY = ["sk", "live", "FAKE0000000000000000000000"].join("_");. -
No shell. Git and other processes are spawned with
execFile, never through a shell string. - Deterministic output. Sort anything you print or write (nodes, edges, files, findings). Diffs of the lock and of generated code must be stable across runs and machines.
- Dependencies are few on purpose. Current runtime deps: commander, fast-glob, picocolors, yaml, zod, typescript (for the TS parser), resvg (OG images), pngjs and pixelmatch, and the MCP SDK. Adding one needs a reason in the PR.
-
Docs live in the repo. The wiki is published from
docs/wiki/(see Editing the wiki), so doc changes are reviewed in PRs like code.
Short, imperative, sentence case, no prefix tags. Say what changed, not how you felt about it. From the history:
Fix issues found while documenting: stale-aware plans, per-binding writes, codegen in apply, plugins, CLI polish
Add public library entry point (src/index.ts)
Point repository metadata at space-pirate-zero/starchart
One logical change per commit where you can. Mention the page you updated if behavior changed.
The code layer lives in packages/starchart/src/code/. TS/JS uses the TypeScript compiler API (typescript.ts); Swift, Kotlin and Go share a hand-written lexer (lexer.ts) and C-like declaration scanner (clike.ts, clike-common.ts). A new language follows the same shape:
-
Classify files. In
files.ts, teachclassify()the extension (rolecode, a newSourceLang) andisTestPath()the language's test-file conventions. Add the language toSourceLangintypes.ts. -
Parse. Write
parseX(file, text, generated): ParsedFileincode/x.ts. AParsedFilecarries symbols (id, qualified name, line range, whitespace-normalized hash, optional literalvalue), env/flag/event signals, i18n key references,@starchartannotation comments, the set of code lines, and screens. Reuse the lexer if the language is C-like. -
Wire it in. Add a branch to the
case "code"switch iningest.ts. If the language has its own name-resolution rules, extend the reference resolution inGraphBuilder. -
Manifests. If the language has a package manifest or lockfile, add it to
MANIFESTSinfiles.tsand parse it inpackages.tssopkg:<manager>/<name>nodes appear. -
Fixtures and tests. Add a small realistic sample under
src/code/__fixtures__/universe/<scope>/, then assert node ids, values and edges inparsers.test.tsandingest.test.ts. - Document the id shape in Node IDs and the coverage in Code Ingestion.
Keep it tree-sitter-free and dependency-free unless there's a strong case. Name-based resolution is acceptable (that's what Swift, Kotlin and Go do today), but be conservative: an extra false edge costs more than a missed one.
Start with Writing an Adapter for the interface in depth. The checklist:
- Implement
Adapter(src/adapters/types.ts) insrc/adapters/<id>.ts:id,capabilities,audit(), and if it can write,apply()returning anUndoRecordplusrevert(). If it can write only some bindings, implementcanApply(node)so the rest staymanualinstead of failing at apply time (the App Store adapter only writes bindings with afield).list()enablesorphans --external(only Stripe uses it today). - Read settings from
ctx.settings(adapters.<id>in config) and secrets fromctx.env. ThrowMissingCredentialsErrorwhen credentials are absent soauditskips instead of failing. - Use
ctx.fetch, honorctx.dryRun, and usectx.previousValuesto find the old value you're replacing. If the external system issues new ids (like Stripe prices), returnbindingUpdateand the engine rewrites the YAML. - Register it in
builtinAdaptersinsrc/adapters/registry.ts. Writes to external systems stay opt-in automatically; only ids inLOCAL_ADAPTERSwrite by default. - Give it a rollout slot in
ADAPTER_PRIORITY(src/core/order.ts) if the default (55) is wrong. - Test with a fake
fetch, then add anAdapter-<Name>page.
You don't need a built-in PR to use an adapter. Out-of-tree adapters load from config: list the module under plugins: in .starchart/config.yaml (a relative path from the project root, or a package name resolved from the project's node_modules) and export adapters (named or on the default export). Every adapter needs an id and an audit() function. Library callers can also call registerAdapter() directly. See Writing an Adapter.
Packs live in src/rules/packs/. Each exports pack: RulePack ({ id, description, rules }). Rules are either declarative (built with defineRule(), the same schema as .starchart/rules.yaml) or custom ({ id, pack, severity, description, check(graph, ctx) } returning findings). See seo.ts for a compact example.
- Create
src/rules/packs/<id>.ts. - Add it to
PACKSinsrc/rules/packs/index.ts. Users enable it withpacks: [<id>];@starchart/pack-<id>andpack-<id>resolve to the same pack. - Test each rule's pass and fail cases in
packs.test.ts. - Document it on Rule Packs. Rule syntax: Rules Engine.
A pack that's specific to your project doesn't need to be built in. Export packs (each with an id and a rules array) from a module listed under plugins:, then enable it by id in packs:. Plugin packs can't reuse a built-in id (core, appstore, privacy, seo); registerPack throws.
src/rules/packs/privacy-catalog.ts maps SDK packages to the data they collect, in Apple's NSPrivacyCollectedDataType vocabulary. Play Data Safety categories are derived automatically.
sdk({
id: "sentry",
name: "Sentry",
packages: ["pkg:npm/@sentry/*", "pkg:swift/sentry-cocoa", "pkg:cocoapods/Sentry", "pkg:gradle/io.sentry:*"],
collects: [
c("CrashData", ["AppFunctionality"]),
c("PerformanceData", ["AppFunctionality"]),
c("OtherDiagnosticData", ["AppFunctionality"]),
c("DeviceID", ["AppFunctionality"], { optional: true, note: "when sendDefaultPii is enabled" }),
],
tracking: false,
}),Cite the vendor's privacy documentation in the PR. Mark data collected only under a config flag as optional with a note. Add a detection test in privacy.test.ts. The catalog is a starting point, not legal advice; see Privacy Drift.
Apache-2.0. By contributing you agree your contribution is licensed under the same terms. See LICENSE.
★ 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