Repository navigation
Apply Revert and Journals
starchart plan tells you what a change touches. starchart apply fixes the parts a machine can fix, writes a journal of exactly what it did, and re-pins the lockfile. starchart revert plays the journal backwards. starchart ack marks human work as done. This page covers how applyPlan works (auto steps only, rollout order, codegen, stop on first failure, pending tasks, binding edits, the journal format, and relocking), the CLI flags, ack, revert, journals, and real output from the demo.
Source: engine/apply.ts and the apply / revert / ack / journals commands in cli/main.ts.
sequenceDiagram
actor You
participant CLI as starchart apply
participant Engine as applyPlan
participant Adapter
participant Disk as repo + .starchart/
You->>CLI: starchart apply
CLI->>Engine: planFromLock (everything out of sync with the lock)
CLI->>You: plan + "Apply N auto step(s)? [y/N]"
You->>CLI: y
loop each step, in rollout order
alt class is not auto
Engine-->>CLI: skip, add to pending
else an earlier step failed
Engine-->>Engine: add to notRun
else generated constant
Engine->>Disk: run every codegen target (once per apply)
else adapter has no apply
Engine-->>CLI: skip, add to pending
else
Engine->>Adapter: apply(node, ctx)
Adapter-->>Engine: ApplyResult (changes, undo, bindingUpdate)
Engine->>Disk: rewrite YAML binding if bindingUpdate
end
end
Engine->>Disk: write .starchart/journal/{id}.json
Engine->>Disk: rebuild project, relock, write starchart.lock
CLI->>You: ✓ / ✗ per step, "Still needs a human", journal path
-
Plan. The CLI builds the plan from the lock (
planFromLock): every fact or tracked code node that changed sincestarchart.lock, plus the changed dependencies of artifacts that are still stale, expanded into impacted items and ordered for rollout (see Rollout Ordering). Locked artifacts that are already in sync are dropped, so a secondapplydoesn't redo the first one's work. Order: explicitafter/blocksedges first, then adapter priority (stripe10 →appstore30 → code 40 →fs50 →url60 → …), then id. -
Walk the steps in order. For each step (filtered by
--onlyif given):-
Not
auto(manual, review, retire, code, break) → added topendingwith its reason and "why" path. Never touched. -
A generated constant (a code symbol marked
// @starchart generated, classedautowith "regenerate fact constants (codegen)") → the first one runs everycodegen:target inconfig.yaml, once; each generated symbol reports that same result with adaptercodegen. If an earlier step failed it goes tonotRun. With no codegen targets configured, it fails withgenerated constants found but no codegen targets are configured. Codegen output is not journaled (see Codegen in apply). -
autobut no adapter binding, or the adapter has noapply→ pending, with reason(no adapter binding)oradapter "x" cannot write. -
An earlier step failed → added to
notRun. Nothing after a failure runs. - Otherwise call
adapter.apply(node, ctx)with a fresh context (lock values aspreviousValues,dryRunfrom the flag).
-
Not
-
Record. A result with an
undo(and not a dry run) becomes a journal entry. A result withbindingUpdatetriggers a YAML rewrite.ok: falsebecomesreport.failed. A thrown error is converted into a failed result. - Dry run stops here. Nothing is journaled or relocked.
- Journal (only if there's at least one entry or a written binding edit).
- Relock (only if at least one step succeeded): rebuild the project from disk, then re-pin the applied artifacts. Regenerated constants aren't pinned themselves; they're re-read in the rebuild and pinned through the artifacts that depend on them. See Relocking.
Only auto steps run, and an artifact is auto only if its adapter can write it (see canWrite). With the default config that means fs only. Stripe and App Store steps are manual until you set write: true, and even then an adapter can refuse a binding through canApply: App Store screenshots and IAP prices stay manual, because only bindings with a field are writable.
starchart apply # show plan, confirm, apply
starchart apply --dry-run # describe changes, write nothing, no prompt
starchart apply --yes # skip the prompt (CI)
starchart apply --only web:pricing-page web:og-pro| Option | Meaning |
|---|---|
--dry-run |
Adapters describe would … changes. No files, API writes, journal or lock changes. No prompt. |
-y, --yes |
Skip the confirmation prompt |
--only <ids...> |
Only these ids. Other steps are ignored completely, and aren't listed as pending either. Codegen runs only if a generated symbol id is in the list. |
Without --dry-run or --yes, apply prints the full plan and asks Apply N auto step(s)? [y/N]. Only y or yes proceeds. N counts every auto item, codegen symbols included, so two generated constants count as two steps even though codegen runs once.
If stdin is not a TTY (CI, pipes, scripts), the prompt answers "no" automatically: apply prints aborted and exits 1. Use --yes in automation.
If nothing is auto, you get ✓ nothing to apply automatically and apply goes on to list pending items without prompting.
Each step prints one line: ✓ done, ✗ failed, · skipped, with the adapter in brackets ([codegen] for generated constants) and the changes, error or skip reason. Binding edits print as ↻. Then the human to-do list, with a hint line for each kind of leftover: mark artifacts done with: starchart ack <id…> when artifacts are pending, and code items: edit the constant, or generate it with starchart codegen when code items are. Last, the journal path.
In a copy of examples/pro-universe, change usd: 4.99 to usd: 5.99 in .starchart/entities/pro.yaml.
Dry run:
$ starchart apply --dry-run
· stripe:price/pro-monthly [stripe] update in stripe (adapter is read-only)
· appstore:iap/pro-monthly [appstore] update in appstore (adapter is read-only)
· appstore:listing/description [appstore] adapter "appstore" cannot write
· appstore:screenshots/6.9/03 [appstore] value is burned into media
· symbol:ios/Pricing.proUSD hardcoded value anchors this fact; update or switch to codegen
✓ symbol:ios/StarchartFacts.AddonPro.Price.usd [codegen] would regenerate apps/web/lib/starchart-facts.ts, apps/ios/Sources/Core/StarchartFacts.swift
✓ symbol:web/lib/starchart-facts#ADDON_PRO_PRICE_USD [codegen] would regenerate apps/web/lib/starchart-facts.ts, apps/ios/Sources/Core/StarchartFacts.swift
· symbol:web/lib/stripe#PRICE_PRO_MONTHLY holds this artifact's external id; update it if the id changes
✓ email:onboarding-day-3 [fs] would addon:pro.price.usd: 4.99 → 5.99
· web:landing-hero [fs] describes this semantically
✓ web:messages-en [fs] would addon:pro.price.usd: 4.99 → 5.99
✓ web:og-pro [fs] would render apps/web/og/pro.svg → apps/web/public/og/pro.png
✓ web:pricing-page [fs] would addon:pro.price.usd: 4.99 → 5.99
· web:live-pricing [url] adapter "url" cannot write
· reel:spring-2026 [youtube] expired 2026-06-30
Still needs a human:
! stripe:price/pro-monthly update in stripe (adapter is read-only)
…
✗ reel:spring-2026 expired 2026-06-30
mark artifacts done with: starchart ack <id…>
code items: edit the constant, or generate it with starchart codegen
Without --yes, from a script:
$ starchart apply < /dev/null
…plan…
Plan: 5 manual · 6 auto · 1 review · 2 code · 1 retire · 1 tests
(4 informational items hidden; use --verbose)
aborted
$ echo $?
1
For real:
$ starchart apply --yes
…skips as above…
✓ symbol:ios/StarchartFacts.AddonPro.Price.usd [codegen] regenerated apps/web/lib/starchart-facts.ts; regenerated apps/ios/Sources/Core/StarchartFacts.swift
✓ symbol:web/lib/starchart-facts#ADDON_PRO_PRICE_USD [codegen] regenerated apps/web/lib/starchart-facts.ts; regenerated apps/ios/Sources/Core/StarchartFacts.swift
· symbol:web/lib/stripe#PRICE_PRO_MONTHLY holds this artifact's external id; update it if the id changes
✓ email:onboarding-day-3 [fs] addon:pro.price.usd: 4.99 → 5.99
· web:landing-hero [fs] describes this semantically
✓ web:messages-en [fs] addon:pro.price.usd: 4.99 → 5.99
✓ web:og-pro [fs] render apps/web/og/pro.svg → apps/web/public/og/pro.png
✓ web:pricing-page [fs] addon:pro.price.usd: 4.99 → 5.99
· web:live-pricing [url] adapter "url" cannot write
· reel:spring-2026 [youtube] expired 2026-06-30
Still needs a human:
! stripe:price/pro-monthly update in stripe (adapter is read-only)
! appstore:iap/pro-monthly update in appstore (adapter is read-only)
! appstore:listing/description adapter "appstore" cannot write
! appstore:screenshots/6.9/03 value is burned into media
⌘ symbol:ios/Pricing.proUSD hardcoded value anchors this fact; update or switch to codegen
⌘ symbol:web/lib/stripe#PRICE_PRO_MONTHLY holds this artifact's external id; update it if the id changes
? web:landing-hero describes this semantically
! web:live-pricing adapter "url" cannot write
✗ reel:spring-2026 expired 2026-06-30
mark artifacts done with: starchart ack <id…>
code items: edit the constant, or generate it with starchart codegen
journal: .starchart/journal/2026-09-29T23-15-08-609Z-d46588.json (undo with: starchart revert .starchart/journal/2026-09-29T23-15-08-609Z-d46588.json)
Four artifacts written (three patched, one PNG rendered), two generated files regenerated, nine items handed to a human, one journal with four entries.
When the plan reaches a generated constant (// @starchart generated, class auto), apply runs every target under codegen: in config.yaml, once, like starchart codegen would. Every generated symbol in the plan reports that one result. The output says regenerated <file> per rewritten file, or generated constants already current; a dry run says would regenerate <every target>. You don't need to run starchart codegen before apply any more.
Codegen output is not journaled. Generated files are reproducible from the facts, so revert leaves them alone; git checkout puts them back. Without any codegen targets, the step fails and stops the apply:
$ starchart apply --dry-run # demo with the codegen: block removed
…
✗ symbol:ios/StarchartFacts.AddonPro.Price.usd [codegen] generated constants found but no codegen targets are configured
· symbol:web/lib/stripe#PRICE_PRO_MONTHLY holds this artifact's external id; update it if the id changes
· web:landing-hero [fs] describes this semantically
· web:live-pricing [url] adapter "url" cannot write
· reel:spring-2026 [youtube] expired 2026-06-30
…
Exit 1. The second generated symbol and all four fs writes went to notRun, so they print nothing.
Apply is sequential and stops at the first failed step. Every later auto step goes into notRun and is not attempted. Non-auto steps after the failure are still listed as pending. Steps that succeeded before the failure stay applied, are journaled, and are relocked, so revert can undo them. The command exits 1.
Typical fs failures are the safety refusals: an ambiguous old value, a list item with nowhere to go, an old value that isn't in the file.
Since 0.1.1 every file apply, codegen and revert write goes through one check, resolveInRoot in paths.ts: the path must resolve inside the project root, symlinks followed. A path that doesn't exist yet is judged by its nearest existing parent directory.
| Writer | What's checked | On a path outside the root |
|---|---|---|
| fs adapter (apply, revert, renders) | binding path, render out, template, undo record path
|
the step fails: path "<p>" is outside the project root or path "<p>" resolves outside the project root (symlink)
|
| Codegen in apply | every codegen: target out
|
the codegen step fails and stops the apply (exit 1): ✗ symbol:… [codegen] path "../x.ts" is outside the project root. --dry-run checks the same paths and fails the same way |
| Binding edits (apply and revert) | the YAML file named in meta.file or the journal's bindingEdits[].file
|
the file is skipped, never rewritten; the edit reports written: false
|
So a committed symlink (page.tsx -> ../../outside/page.tsx) or a hand-edited journal can't aim a write outside the repo. Real output from the demo with the pricing page symlinked out:
$ starchart apply --yes
…
✗ web:pricing-page [fs] path "apps/web/app/pricing/page.tsx" resolves outside the project root (symlink)
The journal file you name on the revert command line can live anywhere, because you chose it. The paths recorded inside it get no such pass. Details per writer: Adapter fs, Codegen.
When an adapter returns bindingUpdate (Stripe does, after replacing an immutable price), the engine:
- Finds the YAML file that declared the artifact (
meta.file, e.g..starchart/artifacts/money.yaml). - Replaces the old string value with the new one as a whole identifier token (not preceded or followed by
[A-Za-z0-9_]). - Updates the in-memory graph node.
- Records a
BindingEdit:
export interface BindingEdit {
artifact: string;
/** Root-relative YAML file. */
file: string;
field: string;
from: string;
to: string;
written: boolean;
}written is false in dry runs, or if the old value wasn't found in the file. Only written edits go into the journal. The token replace touches every occurrence of the old id in that YAML file, so a comment mentioning the id changes too. Code that hardcodes the id is not touched. Real example from a simulated Stripe run: Adapter Stripe.
One JSON file per apply at .starchart/journal/<id>.json, where <id> is the ISO timestamp with : and . replaced by -, plus 6 random hex characters.
export interface Journal {
version: 1;
id: string;
createdAt: string;
entries: JournalEntry[]; // { artifact, adapter, changes, undo }
bindingEdits: BindingEdit[];
/** Lock before the apply; restored on revert. */
lockBefore: LockFile;
revertedAt?: string;
}The demo journal, with base64 file contents and the lock snapshot trimmed:
{
"version": 1,
"id": "2026-09-29T23-15-08-609Z-d46588",
"createdAt": "2026-09-29T23:15:08.609Z",
"entries": [
{
"artifact": "email:onboarding-day-3",
"adapter": "fs",
"changes": ["addon:pro.price.usd: 4.99 → 5.99"],
"undo": {
"adapter": "fs",
"artifact": "email:onboarding-day-3",
"data": {
"path": "marketing/emails/onboarding-day-3.md",
"existed": true,
"encoding": "base64",
"content": "IyBEYXkgMzogdW5sb2NrIHRo…"
}
}
},
{
"artifact": "web:og-pro",
"adapter": "fs",
"changes": ["render apps/web/og/pro.svg → apps/web/public/og/pro.png"],
"undo": {
"adapter": "fs",
"artifact": "web:og-pro",
"data": { "path": "apps/web/public/og/pro.png", "existed": false, "encoding": "base64", "content": null }
}
}
],
"bindingEdits": [],
"lockBefore": { "version": 1, "facts": "…", "code": "…", "artifacts": "…" }
}fs undo records hold the full previous file content, so journals can get large with big files or images. They're plain JSON you can read, diff and delete. Whether to commit .starchart/journal/ is up to you: committing keeps the undo history with the repo, ignoring it keeps history clean.
After writes, the lock has to learn that the applied artifacts are in sync, without forgetting the ones that aren't. Two details make this correct.
Rebuild first. An applied write can change code the chart tracks (a bound pricing page is also a Next.js route, and codegen rewrites the generated constants). The engine re-reads the whole project from disk before pinning, skipping code ingestion only if the project has no code layer. Otherwise the artifact would be stale against its own edit.
Keep old values for everything still stale. Relocking goes through relockArtifacts(graph, previous, ids, { maxCodeDepth }) in core/lock.ts. It pins the applied artifacts to their current dependencies like buildLock, then walks every locked artifact that is still stale and puts the previous lock value back for each changed fact it depends on. Every still-stale artifact counts, writable or not. Two things follow:
-
planandcheckagree. After the demo apply, the lock still saysaddon:pro.price.usdis 4.99, because seven artifacts depend on it and haven't been synced.plankeeps listing them (plus the code items) until you ack or fix them:$ starchart plan 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) ! 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 ? 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 … Plan: 5 manual · 1 review · 2 code · 1 retire · 1 tests $ starchart check | tail -1 Check: 7 staleThe four applied artifacts are gone from the plan:
planFromLockdrops locked artifacts that are in sync. The regenerated constants appear as∅ → 5.99seeds: they come in as changed dependencies of the stale artifacts, and the lock only keeps before-values for facts. Runningapplyagain now just prints✓ nothing to apply automaticallyand the same to-do list. -
Turning writes on later still works. The old text stays findable. If you enable
adapters.appstore.writeafter this apply, the listing turnsauto, and the nextapplystill gets 4.99 as the previous value to search for.
Once the last artifact depending on a fact is synced or acked, that fact's lock entry moves to the new value and the plan goes quiet: ✓ no changes since starchart.lock.
ack and a partial starchart lock <ids…> use the same relock. A full starchart lock does not: it pins everything to the current graph. See Lockfile and Drift.
starchart ack web:landing-hero web:live-pricing✓ acked web:landing-hero, web:live-pricing
$ starchart check | tail -1
Check: 5 stale
ack re-pins the given artifacts to the current graph without touching the world, through the same relockArtifacts as apply, so the old values other stale artifacts need stay in the lock and they stay in plan. Use it after you've re-recorded the screenshot, updated Stripe by hand, or reviewed the landing copy. Ids must be artifacts. Anything else fails with exit code 2:
$ starchart ack symbol:ios/Pricing.proUSD
starchart: not an artifact: symbol:ios/Pricing.proUSD
Code items are fixed in code: edit the constant, or generate it with starchart codegen.
starchart journals # find the id
starchart revert 2026-09-29T23-15-08-609Z-d46588 --dry-run
starchart revert 2026-09-29T23-15-08-609Z-d46588The argument can be a full path, a path relative to the root, a file name in .starchart/journal/ with or without .json, or a unique id prefix. An ambiguous prefix lists the matches.
What revert does:
- Refuses a journal that already has
revertedAt(journal … was already reverted at …). - Runs every entry's
adapter.revert(undo)in reverse order. Every entry is attempted even if one fails: a partial rollback beats none. - Reverses every binding edit (new id → old id in the YAML file), also in reverse. A recorded file outside the project root is skipped (
written: false), not rewritten. -
All or nothing on the lock. Only if every revert succeeded: restore
lockBeforeasstarchart.lockand mark the journal withrevertedAt. If any failed, the lock is left alone and the journal stays revertible, so you can fix the cause and run it again.
--dry-run asks each adapter for would … descriptions and changes nothing.
Output is the JSON RevertReport:
$ starchart revert 2026-09-29T23-15-08-609Z-d46588
{
"journal": "/…/u/.starchart/journal/2026-09-29T23-15-08-609Z-d46588.json",
"dryRun": false,
"results": [
{ "artifact": "web:pricing-page", "ok": true, "changes": ["restored apps/web/app/pricing/page.tsx"] },
{ "artifact": "web:og-pro", "ok": true, "changes": ["removed generated apps/web/public/og/pro.png"] },
{ "artifact": "web:messages-en", "ok": true, "changes": ["restored apps/web/messages/en.json"] },
{ "artifact": "email:onboarding-day-3", "ok": true, "changes": ["restored marketing/emails/onboarding-day-3.md"] }
],
"ok": true,
"bindingEdits": [],
"lockRestored": true
}
(Reformatted: the CLI prints each result across several lines.) After this, starchart plan shows the 4.99 → 5.99 change again with the four fs artifacts back as auto. One difference from before the apply: the constants codegen rewrote are still at 5.99 (codegen isn't journaled), so they show up as changed code instead of auto items. git checkout them if you want the exact starting point.
Exit codes. revert exits 1 when any entry fails ("ok": false), and 2 when the command itself throws (journal not found, ambiguous prefix, already reverted). Here the email file was made read-only before the revert:
$ starchart revert 2026-09-29T23-21-48-379Z-1b18b1
{
…
"results": [
{ "artifact": "web:pricing-page", "ok": true, "changes": ["restored apps/web/app/pricing/page.tsx"] },
{ "artifact": "web:og-pro", "ok": true, "changes": ["removed generated apps/web/public/og/pro.png"] },
{ "artifact": "web:messages-en", "ok": true, "changes": ["restored apps/web/messages/en.json"] },
{ "artifact": "email:onboarding-day-3", "ok": false, "changes": [], "error": "EACCES: permission denied, open '/…/u/marketing/emails/onboarding-day-3.md'" }
],
"ok": false,
"bindingEdits": [],
"lockRestored": false
}
$ echo $?
1
$ starchart journals
2026-09-29T23-21-48-379Z-1b18b1 2026-09-29T23:21:48.379Z 4 artifact(s)
No reverted stamp, so fix the permission and run the same revert again.
Reverts restore snapshots. fs puts back the exact old bytes and App Store puts back the full old text, so edits made after the apply are overwritten.
$ starchart journals
2026-09-29T23-15-08-609Z-d46588 2026-09-29T23:15:08.609Z 4 artifact(s) reverted 2026-09-29T23:15:23.053Z
Newest first: id, creation time, number of distinct artifacts (from entries and binding edits), and the revert time if any. With none: no journals.
import { applyPlan, revertJournal, ackArtifacts, listJournals, buildProject, planFromLock } from "@space-pirate-zero/starchart";
const project = await buildProject(process.cwd());
const report = await applyPlan(project, planFromLock(project), {
dryRun: false,
only: ["web:pricing-page"],
onStep: (e) => console.log(e.type, e.id, e.reason ?? ""),
// fetch, env: injectable for tests
});
// report: { dryRun, applied, failed?, notRun, pending, bindingEdits, journal?, lockUpdated }See Library API.
★ 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