Repository navigation
Codegen
Codegen turns your facts into typed constants for TypeScript, Swift and Kotlin, so code reads the price instead of retyping it. Every generated constant carries its own @starchart anchors comment, so the chart knows exactly which fact each line mirrors, and a fact change classifies those symbols as auto instead of "go find the hardcoded value". This page covers the config, the output shape for each language (real output from the demo), naming and escaping rules, how generated files are detected, the workflow, and the limits.
Source: codegen/.
# .starchart/config.yaml
codegen:
- lang: ts
out: apps/web/lib/starchart-facts.ts
entities: [addon:pro]
- lang: swift
out: apps/ios/Sources/Core/StarchartFacts.swift
entities: [addon:pro]
name: StarchartFacts # Swift enum name (default StarchartFacts)
- lang: kotlin
out: apps/android/app/src/main/java/com/nebula/Facts.kt
name: StarchartFacts # Kotlin object name (default StarchartFacts)
package: com.nebula.facts # Kotlin package (optional)| Field | Required | Meaning |
|---|---|---|
lang |
yes |
ts, swift or kotlin
|
out |
yes | Output path, relative to the project root. Parent directories are created. Must resolve inside the project root, symlinks followed (see Output stays in the project) |
entities |
no | Only these entity ids. Default: every entity that has at least one fact with a value |
name |
no | Root enum (Swift) / object (Kotlin) name. Ignored for TS |
package |
no | Kotlin package line. Must be dotted identifiers, or codegen throws invalid Kotlin package name "…"
|
$ starchart codegen
unchanged apps/web/lib/starchart-facts.ts
unchanged apps/ios/Sources/Core/StarchartFacts.swift
Each target prints wrote or unchanged. Output is deterministic (entities and keys are sorted), and a file whose content would not change is not rewritten, so mtimes and diffs stay quiet. No codegen: targets is an error: starchart: no codegen targets in .starchart/config.yaml (exit 2).
Since 0.1.1 every out is resolved with the same symlink-aware check the fs adapter uses. Before, out: ../x.ts quietly wrote next to your repo. Now it throws, and nothing is written:
$ starchart codegen # out: ../x.ts
starchart: path "../x.ts" is outside the project root
$ echo $?
2
A path that looks local but runs through a symlink pointing out of the repo gets the second message:
$ starchart codegen # apps/web/lib -> a directory outside the project
starchart: path "apps/web/lib/starchart-facts.ts" resolves outside the project root (symlink)
writeCodegen() throws the same errors to library callers, and inside apply the codegen step fails (see Apply, Revert and Journals). generateCode() only returns a string, so its out is never checked.
collectEntities() walks the partOf hierarchy the compiler builds from YAML. Each entity becomes a namespace, each fact group a nested namespace, each leaf fact a constant. Facts without a value (an unresolved code-authority fact) are skipped, and so are container facts whose children were all skipped. An entity's status: is emitted too, because the compiler tracks it as a fact.
Real output for addon:pro in the demo:
// @starchart generated — do not edit
// Facts as code, generated by STARCHART from .starchart/. Regenerate with `starchart codegen`.
/* eslint-disable */
// ── addon:pro ──
// @starchart anchors addon:pro.billing
export const ADDON_PRO_BILLING = "monthly";
// @starchart anchors addon:pro.features
export const ADDON_PRO_FEATURES = ["Themes", "iCloud sync"] as const;
// @starchart anchors addon:pro.name
export const ADDON_PRO_NAME = "Nebula Pro";
// @starchart anchors addon:pro.price.eur
export const ADDON_PRO_PRICE_EUR = 4.99;
// @starchart anchors addon:pro.price.usd
export const ADDON_PRO_PRICE_USD = 4.99;
// @starchart anchors addon:pro.productId
export const ADDON_PRO_PRODUCT_ID = "nebula_pro_monthly";
// @starchart anchors addon:pro.status
export const ADDON_PRO_STATUS = "active";
export const ADDON_PRO = {
billing: ADDON_PRO_BILLING,
features: ADDON_PRO_FEATURES,
name: ADDON_PRO_NAME,
price: {
eur: ADDON_PRO_PRICE_EUR,
usd: ADDON_PRO_PRICE_USD,
},
productId: ADDON_PRO_PRODUCT_ID,
status: ADDON_PRO_STATUS,
} as const;
/** Every leaf fact by id. */
export const FACTS = {
"addon:pro.billing": ADDON_PRO_BILLING,
"addon:pro.features": ADDON_PRO_FEATURES,
"addon:pro.name": ADDON_PRO_NAME,
"addon:pro.price.eur": ADDON_PRO_PRICE_EUR,
"addon:pro.price.usd": ADDON_PRO_PRICE_USD,
"addon:pro.productId": ADDON_PRO_PRODUCT_ID,
"addon:pro.status": ADDON_PRO_STATUS,
} as const;
export type FactId = keyof typeof FACTS;Three shapes, one source:
| Export | Shape | Use it for |
|---|---|---|
ADDON_PRO_PRICE_USD |
one top-level const per leaf, each with its own anchor |
anything; this is the node the chart links |
ADDON_PRO |
nested as const object built from those constants |
ADDON_PRO.price.usd ergonomics |
FACTS / FactId
|
flat map keyed by fact id | lookups by id, typed with FactId
|
Object and array values get as const. Values are JSON-style literals (NaN, Infinity, -Infinity for non-finite numbers). Object keys that are not identifiers are quoted.
Real output:
// @starchart generated — do not edit
// Facts as code, generated by STARCHART from .starchart/. Regenerate with `starchart codegen`.
public enum StarchartFacts {
/// addon:pro
public enum AddonPro {
// @starchart anchors addon:pro.billing
public static let billing: String = "monthly"
// @starchart anchors addon:pro.features
public static let features: [String] = ["Themes", "iCloud sync"]
// @starchart anchors addon:pro.name
public static let name: String = "Nebula Pro"
public enum Price {
// @starchart anchors addon:pro.price.eur
public static let eur: Double = 4.99
// @starchart anchors addon:pro.price.usd
public static let usd: Double = 4.99
}
// @starchart anchors addon:pro.productId
public static let productId: String = "nebula_pro_monthly"
// @starchart anchors addon:pro.status
public static let status: String = "active"
}
}Caseless enums are namespaces (they cannot be instantiated). Type mapping:
| Fact value | Swift |
|---|---|
| string | String |
| boolean | Bool |
| safe integer | Int |
| other number |
Double (always with a ., Double.nan, Double.infinity) |
| homogeneous string / bool / int / double array |
[String] / [Bool] / [Int] / [Double] (empty array → [String]) |
null |
String? = nil |
| object | nested public enum with its keys sorted |
| mixed array |
String holding the JSON, with a // JSON-encoded (not a homogeneous scalar list) comment |
Generated from the demo facts with package: com.nebula.facts and entities: [addon:pro, app:nebula]:
// @starchart generated — do not edit
// Facts as code, generated by STARCHART from .starchart/. Regenerate with `starchart codegen`.
package com.nebula.facts
object StarchartFacts {
/** addon:pro */
object AddonPro {
// @starchart anchors addon:pro.billing
const val BILLING: String = "monthly"
// @starchart anchors addon:pro.features
val FEATURES: List<String> = listOf("Themes", "iCloud sync")
// @starchart anchors addon:pro.name
const val NAME: String = "Nebula Pro"
object Price {
// @starchart anchors addon:pro.price.eur
const val EUR: Double = 4.99
// @starchart anchors addon:pro.price.usd
const val USD: Double = 4.99
}
// @starchart anchors addon:pro.productId
const val PRODUCT_ID: String = "nebula_pro_monthly"
// @starchart anchors addon:pro.status
const val STATUS: String = "active"
}
/** app:nebula */
object AppNebula {
// @starchart anchors app:nebula.appStoreId
const val APP_STORE_ID: String = "6450000000"
// @starchart anchors app:nebula.category
const val CATEGORY: String = "ProductivityApplication"
// @starchart anchors app:nebula.name
const val NAME: String = "Nebula"
// @starchart anchors app:nebula.operatingSystem
const val OPERATING_SYSTEM: String = "iOS 18+"
}
}const val for strings, booleans and numbers; plain val for lists and nullable values (Kotlin does not allow const there).
| Fact value | Kotlin |
|---|---|
| string | const val X: String |
| boolean | const val X: Boolean |
| integer within 32-bit | const val X: Int |
| larger safe integer | const val X: Long = 123L |
| other number |
const val X: Double (Double.NaN, Double.POSITIVE_INFINITY, …) |
| homogeneous list |
val X: List<String|Boolean|Int|Long|Double> = listOf(…) (empty string/bool list → emptyList()) |
null |
val X: String? = null |
| object | nested object
|
| mixed array |
const val X: String with the JSON and the same // JSON-encoded … comment |
Names come from fact keys and entity ids, split into words on camelCase boundaries and on any non-alphanumeric character (addon:pro → addon, pro; productId → product, id).
| Target | Entity namespace | Group / object key | Leaf constant |
|---|---|---|---|
| TS |
UPPER_SNAKE of the entity id (ADDON_PRO) |
camelCase key inside the nested object |
UPPER_SNAKE of the full fact id (ADDON_PRO_PRICE_USD) |
| Swift |
PascalCase (AddonPro) |
PascalCase enum (Price) |
camelCase (productId) |
| Kotlin |
PascalCase object |
PascalCase object |
UPPER_SNAKE (PRODUCT_ID) |
- A name that starts with a digit gets a leading
_(_2026). - An empty name becomes
value/Value/VALUE. - Collisions within one scope get
_2,_3, … (TS reservesFACTSat top level, so an entity calledfactsbecomesFACTS_2). - Swift: keywords are backticked (
`default`); types that would shadowType,Self,Protocol,Any,String,Int,Double,Bool,Array,Optionalget a trailing_. A rootnamethat is not a valid identifier (or is a keyword) is PascalCased. - Kotlin: types that would shadow
Any,String,Int,Long,Double,Boolean,List,Unit,Nothing,Companionget a trailing_; hard keywords in the package path are backticked.
| Target | Strings |
|---|---|
| TS | JSON.stringify |
| Swift |
\\, \", \n, \r, \t, \0; every other control or non-ASCII character as \u{XXXX} (so 日本 is emitted as "\u{65E5}\u{672C}") |
| Kotlin |
\\, \", \$ (no accidental string templates), \n, \r, \t, \b; other control / non-ASCII as \uXXXX
|
Comments (entity headers, anchor lines) have */ defused and line breaks collapsed, so a fact id cannot break out of a comment.
Every file starts with:
// @starchart generated — do not edit
The code ingester checks the first five lines for @starchart generated. When found:
- the file node and every symbol in it get
meta.generated: true; -
starchart scantreats all literals in the file as bound; -
starchart init --discoverignores its constants.
Each leaf constant is preceded by // @starchart anchors <fact id>. That comment is a normal annotation: it attaches to the next declaration and creates symbol --anchors--> fact with origin annotation. You get one precise bridge per constant, not one vague bridge per file.
anchors propagates both ways. When a fact changes, the impact walk reaches every symbol that anchors it, and classify() decides:
| Symbol | Class | Reason |
|---|---|---|
generated (meta.generated) |
auto |
regenerate fact constants (codegen) |
| hand-written, anchors a fact | code |
hardcoded value anchors this fact; update or switch to codegen |
| hand-written, anchors an artifact (e.g. a Stripe price id) | code |
holds this artifact's external id; update it if the id changes |
From the demo, starchart impact addon:pro.price.usd:
~ 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)
…
⌘ 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
starchart cost uses the same split to tell you what switching to codegen would save. It only counts code items reached directly from a fact. PRICE_PRO_MONTHLY anchors the Stripe price, not a fact, and codegen can't produce a Stripe price id, so it's left out:
████████████████████ 9.4h addon:pro.price.usd (21 impacted)
→ 1 hardcoded code anchor: generate constants with `starchart codegen` to make these auto
- Add
codegen:targets, runstarchart codegen, commit the output. - Replace hardcoded values with the generated constants (
StarchartFacts.AddonPro.Price.usd,ADDON_PRO_PRICE_USD). Delete their old@starchart anchorscomments; the generated file carries them now. - When a fact changes, run
starchart apply. It regenerates the codegen targets for you (below) and updates the lock.starchart codegenon its own still works when you just want the files.
When a plan contains generated constants (auto via anchors), apply runs one codegen pass over every configured target. Each generated symbol in the plan reports that pass under the codegen adapter. Real output after adding "Widgets" to the Swift proFeatures array in the demo:
✓ symbol:ios/StarchartFacts.AddonPro.features [codegen] regenerated apps/web/lib/starchart-facts.ts; regenerated apps/ios/Sources/Core/StarchartFacts.swift
✓ symbol:web/lib/starchart-facts#ADDON_PRO_FEATURES [codegen] regenerated apps/web/lib/starchart-facts.ts; regenerated apps/ios/Sources/Core/StarchartFacts.swift
-
apply --dry-runprintswould regenerate apps/web/lib/starchart-facts.ts, apps/ios/Sources/Core/StarchartFacts.swiftand writes nothing. - If the files already match:
generated constants already current. - Codegen output is not journaled.
starchart revertdoesn't undo it; git does. (The run above wrote no journal at all:starchart journalsprintedno journals.) - Generated constants in the plan but no
codegen:targets in config: those items fail withgenerated constants found but no codegen targets are configured. - The Future Universe Preview labels these items "regenerated by starchart codegen on apply".
In CI, fail when the generated files are out of date:
- run: npx @space-pirate-zero/starchart codegen
- run: git diff --exit-code -- apps/web/lib/starchart-facts.ts apps/ios/Sources/Core/StarchartFacts.swiftAs a pre-commit hook (any hook runner):
npx @space-pirate-zero/starchart codegen && git add apps/web/lib/starchart-facts.ts apps/ios/Sources/Core/StarchartFacts.swiftFrom code:
import { buildProject, generateCode, writeCodegen } from "@space-pirate-zero/starchart";
const project = await buildProject();
const { files, changed } = writeCodegen(project); // every config target
const kotlin = generateCode(project.graph, { lang: "kotlin", out: "Facts.kt", package: "com.nebula.facts" });-
Mixed arrays become JSON strings.
[1, "a"]or an array of objects is emitted as a JSON-encodedStringconstant, not a typed structure. - Arrays are only typed when every element is the same scalar kind; a list mixing integers and decimals becomes
Double. - Arrays of objects are not expanded into nested types.
- Code-authority facts are only emitted when the code layer resolved them (they need ingestion to have a value).
- TS, Swift and Kotlin only. No Go, Dart or Python output.
-
applyregenerates every codegen target, not just the entities in the plan, and doesn't journal the result.
★ 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