arkaik 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ # The fragment contract
2
+
3
+ A fragment is the one file a bootstrap agent writes: one JSON object at
4
+ `.arkaik/bootstrap/fragments/<unit>.json`, where `<unit>` is your work-unit
5
+ id from `.arkaik/bootstrap/manifest.json`. The filename **is** the unit id —
6
+ `arkaik bootstrap merge` derives the path from the id and ignores the
7
+ manifest's `fragment` field. A missing fragment just means the unit hasn't
8
+ run yet (the run is resumable); invalid JSON, a top-level array, or a
9
+ non-array value under any of the keys below fails the merge loudly, naming
10
+ your unit.
11
+
12
+ The top level:
13
+
14
+ ```jsonc
15
+ {
16
+ "unit": "<your unit id>",
17
+ "wave": 1,
18
+ // then one or more of the six keys below, each an array of objects:
19
+ "nodes": [], "edges": [], // greenfield anatomy / acceptances
20
+ "add": [], "update": [], "retire": [], // brownfield reconcile
21
+ "events": [] // story (wave 3)
22
+ }
23
+ ```
24
+
25
+ Merge reads **only** those six keys and ignores everything else. Two
26
+ consequences:
27
+
28
+ - You may have seen sketches of the wave-3 shape with `deliverables`,
29
+ `releases`, or `decisions` keys — **they do not exist in the contract**,
30
+ and anything written under them is silently lost. Deliverables and
31
+ releases are entries in `events` (`deliverable.shipped`, `release.tagged`);
32
+ the decisions unit's `DEC-` nodes go in `nodes` (or `add`) with their edges
33
+ in `edges`, like any other node. Merge never branches on `wave` — every key
34
+ works in every fragment.
35
+ - One ignored key is used on purpose: **`notes`**, a free-text string where
36
+ you record judgment calls (e.g. why a PR was judged not user-visible).
37
+ Merge skips it; the wave reviewer reads it.
38
+
39
+ ## Nodes — `nodes` and `add`
40
+
41
+ A node as an agent writes it: the bundle's node fields **minus `project_id`**
42
+ (merge stamps it), plus an optional `created_ts`.
43
+
44
+ | Field | Required | Meaning |
45
+ |---|---|---|
46
+ | `id` | yes | Species-prefixed kebab-case id (`F-`, `V-`, `DM-`, `API-`, `AC-`, `DEC-`) |
47
+ | `species` | yes | `flow`, `view`, `data-model`, `api-endpoint`, `acceptance`, `decision` |
48
+ | `title` | yes | Non-empty; concept titles capitalized, table titles the exact identifier |
49
+ | `created_ts` | no | When this surface first shipped — becomes the `node.created` event's timestamp |
50
+ | anything else | no | `status`, `platforms`, `metadata` (playlists, gherkin, values…) as the species requires |
51
+
52
+ `created_ts` should be the merge instant of the PR that first shipped the
53
+ surface, straight from your slice. It is consumed, not stored: merge uses it
54
+ to synthesize the `node.created` event and drops it from the node. Absent,
55
+ merge falls back to `project.created_at`. A malformed timestamp fails your
56
+ unit loudly rather than minting a wrong event.
57
+
58
+ `add` (brownfield) takes exactly the same node shape. Re-declaring a node
59
+ that already exists with the **same title** is a safe no-op — no duplicate
60
+ node, no duplicate `node.created`. The same id with a **different title** is
61
+ a collision, and merge fails naming both titles and both units. That is the
62
+ whole coordination model: ids are derived deterministically from titles, so
63
+ independent agents converge on the same id for the same thing and collide
64
+ loudly on different things.
65
+
66
+ ## Edges — `edges`
67
+
68
+ | Field | Required | Meaning |
69
+ |---|---|---|
70
+ | `source_id` | yes | A node id — from this fragment, another unit's fragment, or the existing map |
71
+ | `target_id` | yes | Same |
72
+ | `kind` | yes | `composes`, `calls`, `displays`, `queries`, or `covers` (semantics: the `arkaik` skill) |
73
+
74
+ No edge `id` — merge mints the canonical `e-{source}-{target}`. Endpoints
75
+ resolve across fragments, so you never coordinate with another agent; an
76
+ endpoint **nobody** created is a loud merge error. Two fragments disagreeing
77
+ on one edge's kind is an error naming both; agreeing is a silent no-op.
78
+
79
+ ## Reconcile ops — `update` and `retire` (brownfield)
80
+
81
+ ```jsonc
82
+ "update": [{ "id": "V-x", "patch": { ... }, "changed_ts": "<ISO instant>" }],
83
+ "retire": [{ "id": "V-y", "reason": "why", "changed_ts": "<ISO instant>" }]
84
+ ```
85
+
86
+ - **`update`** — `patch` fields replace the node's. `metadata` is special:
87
+ its **top-level keys merge** — keys you don't name survive — but a key you
88
+ **do** name is **replaced whole**, not merged recursively. Patching one
89
+ nested field means writing the key's full value: all of `platformStatuses`,
90
+ the whole `playlist`. When the patch **changes** `status` (or
91
+ `metadata.decision_status`), merge emits the matching `node.status_changed`
92
+ (or `decision.status_changed`) event at `changed_ts`; a patch restating the
93
+ same value emits nothing.
94
+ - **`retire`** — never a delete. Merge sets `status: "archived"`, records
95
+ `metadata.retired_reason`, and emits the status event. The node count is
96
+ unchanged; a human decides actual removal.
97
+ - Both ops must target a node that exists — an unknown id fails the merge.
98
+
99
+ **The `changed_ts` one-shot rule.** When a status-changing op omits
100
+ `changed_ts`, merge substitutes one constant fallback timestamp for the whole
101
+ run. One such op per node is safe; a **second** status-changing op for the
102
+ **same node** in the same run that also omits it lands on the identical
103
+ timestamp, and merge refuses rather than guess an order. Give every
104
+ status-changing op past the first, for the same node, its own explicit
105
+ `changed_ts`.
106
+
107
+ ## Story events — `events`
108
+
109
+ Plain journal event objects: `type`, the type's payload fields, and a real
110
+ `ts`. **No `id`, no `actor`** — merge mints deterministic ids, which is what
111
+ makes a re-run over unchanged fragments byte-identical. The story types:
112
+
113
+ | Type | Payload |
114
+ |---|---|
115
+ | `deliverable.shipped` | `deliverable_id`, `title`, `summary?`, `url?`, `node_ids?`, `platform?` |
116
+ | `release.tagged` | `version`, `notes?`, `platform?` |
117
+ | `node.status_changed` | `node_id`, `from`, `to`, `platform?` |
118
+ | `decision.status_changed` | `node_id`, `from`, `to` |
119
+ | `idea.proposed` | `title`, `description?`, `node_id?` |
120
+
121
+ The full vocabulary is the `arkaik` skill's event table. Timestamps come from
122
+ the corpus — PR merge instants, not invented dates. Two **unscoped**
123
+ `node.status_changed` for **one node at the identical `ts`** that disagree on
124
+ `to` are refused (read-back order between them would be undefined);
125
+ platform-scoped events — those carrying `platform` — are exempt. Real merge
126
+ timestamps are full instants, so distinct instants are free — use them.
127
+
128
+ ## Examples
129
+
130
+ Each example below is a fixture from the CLI's own test suite, copied
131
+ verbatim so the docs and the tested contract cannot drift. If a fixture
132
+ changes, change it here too.
133
+
134
+ **Greenfield anatomy** (`tests/cli/bootstrap-e2e.test.js`, the `w1-home`
135
+ fragment — note the playlist ↔ `composes` agreement and `created_ts`):
136
+
137
+ ```json
138
+ {
139
+ "unit": "w1-home",
140
+ "wave": 1,
141
+ "nodes": [
142
+ {
143
+ "id": "F-notes",
144
+ "species": "flow",
145
+ "title": "Notes",
146
+ "status": "live",
147
+ "platforms": ["web"],
148
+ "created_ts": "2026-01-05T10:00:00.000Z",
149
+ "metadata": { "playlist": { "entries": [{ "type": "view", "view_id": "V-home" }] } }
150
+ },
151
+ {
152
+ "id": "V-home",
153
+ "species": "view",
154
+ "title": "Home",
155
+ "status": "live",
156
+ "platforms": ["web"],
157
+ "created_ts": "2026-01-05T10:00:00.000Z"
158
+ }
159
+ ],
160
+ "edges": [{ "source_id": "F-notes", "target_id": "V-home", "kind": "composes" }]
161
+ }
162
+ ```
163
+
164
+ **Brownfield `add`** (`tests/cli/bootstrap-merge.test.js`, the "node.created
165
+ is not duplicated" probe — `V-home` already exists in the map and is a no-op;
166
+ `V-new` is genuinely new):
167
+
168
+ ```json
169
+ {
170
+ "unit": "w1-a",
171
+ "wave": 1,
172
+ "add": [
173
+ { "id": "V-home", "species": "view", "title": "Home", "status": "live", "platforms": ["web"] },
174
+ { "id": "V-new", "species": "view", "title": "New", "status": "live", "platforms": ["web"], "created_ts": "2026-03-01T00:00:00.000Z" }
175
+ ]
176
+ }
177
+ ```
178
+
179
+ **Brownfield `update` + `retire`** (`tests/cli/bootstrap-merge.test.js`, the
180
+ combined reconcile fixture):
181
+
182
+ ```json
183
+ {
184
+ "unit": "w1-a",
185
+ "wave": 1,
186
+ "update": [{ "id": "V-home", "patch": { "status": "live" }, "changed_ts": "2026-02-01T00:00:00.000Z" }],
187
+ "retire": [{ "id": "V-legacy", "reason": "replaced by V-home", "changed_ts": "2026-02-02T00:00:00.000Z" }]
188
+ }
189
+ ```
190
+
191
+ **Wave-3 story** (`tests/cli/bootstrap-merge.test.js`, the journal de-dup
192
+ fixture — `V-home` is a node that already exists in the map):
193
+
194
+ ```json
195
+ {
196
+ "unit": "w3-decisions",
197
+ "wave": 3,
198
+ "events": [
199
+ { "type": "deliverable.shipped", "deliverable_id": "pr-1", "title": "Ship it", "ts": "2026-01-05T00:00:00.000Z" },
200
+ { "type": "node.status_changed", "node_id": "V-home", "from": "backlog", "to": "live", "ts": "2026-01-05T00:00:00.000Z" }
201
+ ]
202
+ }
203
+ ```
@@ -0,0 +1,120 @@
1
+ # Waves & gates
2
+
3
+ A bootstrap run is four waves, each fanning out over work units that
4
+ `arkaik bootstrap plan` writes to `.arkaik/bootstrap/manifest.json`:
5
+
6
+ | Wave | Units | Emits |
7
+ |---|---|---|
8
+ | 0 · Recon | `w0-recon` (one agent) | `.arkaik/bootstrap/profile.json` |
9
+ | 1 · Anatomy / Reconcile | `w1-<area>`, one per area | `{nodes, edges}` (greenfield) or `{add, update, retire}` (brownfield) |
10
+ | 2 · Acceptances + values | `w2-<area>`, one per area (+ a values-balance reviewer role — not a unit) | acceptances with gherkin, values, `covers` edges, platform scoping |
11
+ | 3 · Story | `w3-<era>` per era, plus `w3-decisions` and `w3-status-arcs` | `events` (deliverables, releases, arcs) and `DEC-` nodes |
12
+
13
+ Working a unit is always the same loop: `arkaik bootstrap slice <unit>` →
14
+ judge → write the fragment → set the unit's status to `done` in
15
+ `.arkaik/bootstrap/manifest.json`. **Resumability is structural:** statuses
16
+ (`pending` / `done` / `rejected`) live in the manifest and output lives in
17
+ fragment files, so a killed session resumes at the first `pending` unit, and
18
+ re-running `plan` preserves the status of any unit whose slice is unchanged.
19
+
20
+ **Every wave ends the same way:** an adversarial reviewer — checking against
21
+ the product, not just the schema — then `arkaik bootstrap merge` (preview
22
+ with `--dry-run`), then `arkaik validate` on the bundle, **warning-clean**.
23
+ Merge itself blocks only on errors; warnings never block the write, so
24
+ warning-clean is the reviewer's gate to enforce, not the CLI's. A wave that
25
+ does not gate green does not advance.
26
+
27
+ ## Wave 0 · Recon
28
+
29
+ One agent reads the corpus and the repo (`slice` gives it the docs manifest)
30
+ and writes `.arkaik/bootstrap/profile.json`: `products`, the `platforms`
31
+ axis, `areas` (id, title, code paths — typically 8–12 for a real product),
32
+ and `eras` (slug, title, date window). Then re-run `arkaik bootstrap plan` to
33
+ expand waves 1–3.
34
+
35
+ **Reviewer checklist — wave 0:**
36
+
37
+ - [ ] Area ids and era slugs are lowercase kebab-case (they become fragment
38
+ filenames — `plan` rejects anything else).
39
+ - [ ] Every area has at least one real path, and the areas together cover the
40
+ product's code. An area with wrong paths starves its agents of exactly
41
+ the PRs and surfaces they need.
42
+ - [ ] Every era has at least one date bound; windows are half-open, so
43
+ adjacent eras may share a boundary date but must never overlap. Two
44
+ eras with only a `from` always overlap — give the earlier one a `to`.
45
+ - [ ] The eras cover the PR timeline. A PR falling inside no era is silently
46
+ absent from the story.
47
+ - [ ] The platform axis matches how the product actually ships; `products`
48
+ is declared only when the repo really is a family of apps.
49
+
50
+ ## Wave 1 · Anatomy / Reconcile
51
+
52
+ One unit per area. Greenfield: map the area's flows, views, data models, API
53
+ endpoints, and the edges between them. Brownfield: reconcile the existing map
54
+ against the code — `add` what's missing, `update` what drifted, `retire`
55
+ what's gone. Never delete.
56
+
57
+ **Reviewer checklist — wave 1:**
58
+
59
+ - [ ] Species are right: the route handler is `API-`, the page it feeds is
60
+ `V-`, the journey between pages is `F-`, the stored thing is `DM-`.
61
+ - [ ] No `DM-` concept/table collisions; concept titles are capitalized
62
+ words, table titles the exact DB identifier.
63
+ - [ ] Every flow has a real playlist that agrees with its `composes` edges,
64
+ and no flow contains itself.
65
+ - [ ] Edge kinds match their semantics (`composes` / `calls` / `displays` /
66
+ `queries`) — spot-check a few against the actual code.
67
+ - [ ] `created_ts` values come from the PRs that first shipped each surface,
68
+ not from today.
69
+ - [ ] **Churn guard (brownfield): a unit proposing `retire` or `update` on
70
+ more than 20% of the existing nodes stops for human review.** Churn at
71
+ that scale is usually a wrong slice or a misread map, not a real
72
+ product change.
73
+ - [ ] Nothing is deleted; every `retire` carries a reason a human could act
74
+ on.
75
+
76
+ ## Wave 2 · Acceptances + values
77
+
78
+ One unit per area writes acceptances for its surfaces; one values-balance
79
+ reviewer then reads the whole wave.
80
+
81
+ **Reviewer checklist — wave 2:**
82
+
83
+ - [ ] Exactly one Given/When/Then per acceptance — a second scenario is a
84
+ second acceptance.
85
+ - [ ] Every `covers` edge points at an id that exists (`arkaik bootstrap
86
+ index`), and each acceptance anchors inside a single product.
87
+ - [ ] Platform scoping is the fewest platforms that are true; no unscoped
88
+ promotion claiming every platform.
89
+ - [ ] Values are 1–3 per acceptance, from the table, most specific element
90
+ first — or omitted where unsure. (Skipped entirely if the repo has no
91
+ values reference.)
92
+ - [ ] **Values balance: if more than half the acceptances land on one value
93
+ element, the wave is rejected and re-run.** An unchecked acceptance
94
+ wave collapses into ~90% `simplifies`; the pyramid must show real
95
+ spread.
96
+
97
+ ## Wave 3 · Story
98
+
99
+ Era units (`w3-<era>`) turn each era's user-visible PRs into
100
+ `deliverable.shipped` events and tag the era's `release.tagged`.
101
+ `w3-decisions` mines the design docs into `DEC-` nodes, their edges, and
102
+ their events. `w3-status-arcs` gives each anatomy node an honest 1–3 event
103
+ arc ending at its snapshot status.
104
+
105
+ **Reviewer checklist — wave 3:**
106
+
107
+ - [ ] The user-visible filter held: every PR with a Lab Note became a
108
+ deliverable; chores, CI, and refactors did not; borderline calls are
109
+ explained in the fragment's `notes`.
110
+ - [ ] Deliverable and release timestamps are real merge instants from the
111
+ corpus, never invented dates.
112
+ - [ ] Every decision traces to a real document in the corpus — decisions are
113
+ mined, not imagined.
114
+ - [ ] Every arc ends at the node's snapshot status, contains no transition
115
+ that did not happen, and gives same-node events distinct instants.
116
+ - [ ] Nothing project-shaped was deleted or rewritten — story only ever adds.
117
+
118
+ After wave 3 gates green, the run is done: the bundle and its journal are the
119
+ product. Landing them (and removing this skill) is the operator's move, not a
120
+ wave.
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: arkaik-bootstrap
3
+ version: 1.0.0
4
+ description: >
5
+ Bootstrap the Arkaik product graph map for {{PRODUCT_NAME}} from its
6
+ repository history — map this repo, bootstrap the map, retro-populate or
7
+ backfill the map from merged PRs, design docs, and code surfaces. Use this
8
+ skill when working a unit of an `arkaik bootstrap` run: reading a slice,
9
+ writing a fragment, or reviewing a wave. One-time onboarding only — ongoing
10
+ map maintenance belongs to the `arkaik` skill, not this one.
11
+ ---
12
+
13
+ # Arkaik Bootstrap — the judgment half
14
+
15
+ The bootstrap method has two halves. The `arkaik bootstrap` CLI owns
16
+ everything deterministic: mining the corpus, planning work units, slicing
17
+ what you read, merging what you write, validating the result. This skill owns
18
+ what code cannot decide: which species a thing is, what a flow's playlist
19
+ really contains, what a PR actually shipped, which value an acceptance
20
+ genuinely earns. You are the judgment half.
21
+
22
+ Two references ship beside this file:
23
+
24
+ - [references/fragments.md](references/fragments.md) — the exact shape of the
25
+ file you write
26
+ - [references/waves.md](references/waves.md) — the wave catalog and the
27
+ reviewer checklist each wave gates on
28
+
29
+ > **Template parameters.** Like the maintenance skill, this file is rendered
30
+ > by `arkaik init`. If you are reading an unrendered copy, treat the defaults
31
+ > in parentheses as the values:
32
+ >
33
+ > | Parameter | Meaning | Default |
34
+ > |---|---|---|
35
+ > | `{{PRODUCT_NAME}}` | The product being mapped | the current product |
36
+ > | `{{BUNDLE_PATH}}` | Where the merged map lands | `docs/arkaik/bundle.json` |
37
+
38
+ ## When this skill applies
39
+
40
+ This skill drives a **one-time onboarding run**: building the map for
41
+ {{PRODUCT_NAME}} out of the repository's own history. Greenfield (no bundle
42
+ yet, or an `arkaik init` stub with zero nodes) means mapping from scratch;
43
+ brownfield (a bundle that already carries nodes) means reconciling the
44
+ existing map against the code. Same method either way — only the wave-1
45
+ fragment shape differs.
46
+
47
+ Ongoing edits belong to the **`arkaik` skill installed beside this one**
48
+ (`../arkaik/SKILL.md`): if you are updating the map as a side-effect of a
49
+ code change, that is its job, not this skill's. Once the bootstrap run has
50
+ landed, this skill has done its one job and can be removed:
51
+
52
+ ```bash
53
+ arkaik init --remove-bootstrap
54
+ ```
55
+
56
+ ## The contract you work under
57
+
58
+ **You never read the bundle. You never write the bundle. You never write
59
+ merge logic.** You read a slice, you write a fragment:
60
+
61
+ ```bash
62
+ arkaik bootstrap slice <unit> > slice.json # what to read
63
+ arkaik bootstrap index # existing node ids, when you need to reference them
64
+ ```
65
+
66
+ The slice is exactly the corpus subset your unit needs — matching PRs,
67
+ matching surfaces, and the docs manifest when your unit asks for it — instead
68
+ of the whole repository. The index is one tab-separated line per existing
69
+ node (`id`, `species`, `title`, `product`): how you reference real nodes
70
+ without loading their bodies.
71
+
72
+ Your output is one JSON file at `.arkaik/bootstrap/fragments/<unit>.json`,
73
+ in the shape defined by [references/fragments.md](references/fragments.md).
74
+ `arkaik bootstrap merge` owns everything after that — ID-collision detection,
75
+ cross-fragment edge resolution, journal event synthesis, validation, landing
76
+ the result at `{{BUNDLE_PATH}}` with its journal sidecar — and a malformed
77
+ fragment fails with your unit's name attached rather than corrupting the
78
+ map. Never edit another unit's fragment. When yours is
79
+ written, set your unit's status to `done` in
80
+ `.arkaik/bootstrap/manifest.json`.
81
+
82
+ ## Species discrimination
83
+
84
+ Four anatomy species, one question each:
85
+
86
+ - **Flow (`F-`)** — a journey the user moves through. It has a playlist.
87
+ - **View (`V-`)** — one surface the user looks at: a screen, page, or panel.
88
+ - **Data model (`DM-`)** — a thing the product stores or reasons about.
89
+ - **API endpoint (`API-`)** — a callable contract across a boundary: a route
90
+ handler, an RPC, a webhook.
91
+
92
+ A route file is usually one `API-` node; the page it feeds is a `V-`; the
93
+ navigation between pages is the `F-`. Later waves add **acceptances (`AC-`)**
94
+ — testable promises — and **decisions (`DEC-`)** mined from design docs.
95
+
96
+ **The `DM-` rule — concept vs physical table.** A conceptual model takes the
97
+ singular concept name: the concept "Project" is `DM-project`. A physical
98
+ table or DB view takes its exact identifier: the table `projects` is
99
+ `DM-projects`. Both may legitimately exist in one map; they must never
100
+ kebab-case into the same id. Full ID and title rules live in the `arkaik`
101
+ skill beside this one and its `../arkaik/references/schema.md`.
102
+
103
+ ## Playlists
104
+
105
+ Every flow ships a **real** `metadata.playlist` — actual entries referencing
106
+ actual views and sub-flows, never a placeholder. Two invariants:
107
+
108
+ - The playlist and the flow's `composes` edges **agree**: every view or flow
109
+ in the playlist has a `composes` edge from the flow, and every `composes`
110
+ edge appears in the playlist.
111
+ - **No cycles** — a flow cannot contain itself, directly or through
112
+ sub-flows.
113
+
114
+ Entry shapes — including the branching `condition` and `junction` entries —
115
+ are in `../arkaik/references/schema.md`.
116
+
117
+ ## Acceptances
118
+
119
+ An acceptance is a testable promise. When you write one:
120
+
121
+ - **Exactly one Given/When/Then** in `metadata.gherkin`. A second scenario is
122
+ a second acceptance.
123
+ - **`covers` edges to real anchors** — views or flows whose ids exist in the
124
+ index or in your own fragment. An acceptance covering nothing is an intake
125
+ idea, not mapped behavior.
126
+ - **Single-product anchoring** — its `covers` edges may span several views
127
+ and flows, but they must all resolve to the same product. A promise that
128
+ genuinely spans two products is two acceptances.
129
+
130
+ The full doctrine (per-platform statuses, anchorless intake, the parity
131
+ layer) is the `arkaik` skill's "Acceptances" section — follow it, don't
132
+ reinvent it.
133
+
134
+ ## Values
135
+
136
+ Assign **1–3 Bain value elements** in `metadata.values` per acceptance. The
137
+ **most specific element wins**; claim a higher tier of the pyramid **only
138
+ when the acceptance genuinely operates there**. The 30-element table with
139
+ one-line definitions is `../arkaik/references/values.md` in the skill beside
140
+ this one. If that file is absent (the maintenance skill was installed with
141
+ `--no-values`), value mapping is out of scope for this repo — skip it
142
+ entirely. When unsure, omit: a wrong value is worse than a missing one, and
143
+ the wave-2 balance check (see [references/waves.md](references/waves.md))
144
+ rejects a lopsided wave anyway.
145
+
146
+ ## Platform scoping
147
+
148
+ Per-platform truth lives on acceptances (`metadata.platformStatuses`), and
149
+ history is full of platform-scoped shipping — a PR that says `AC-x@ios`, or
150
+ that only touched the iOS target, shipped iOS and nothing else.
151
+
152
+ **An unscoped promotion claims every platform.** Moving an acceptance's base
153
+ `status` asserts the behavior everywhere the acceptance's platforms reach —
154
+ exactly like an unscoped `AC-x` mention in a PR. When the evidence says one
155
+ platform, write that one platform's status. **Scope to the fewest platforms
156
+ that are true**, and keep `platforms` itself to where the behavior is
157
+ actually expected — mobile-only behavior is `["ios", "android"]`, not
158
+ backlog-on-web.
159
+
160
+ ## What counts as user-visible
161
+
162
+ Wave 3 turns PRs into story. The filter:
163
+
164
+ - **A PR with a Lab Note is user-visible by definition** — and the note is a
165
+ benefit-first title and summary already written for you. Use it.
166
+ - **Chores, CI, refactors, dependency bumps, and docs-only changes are not.**
167
+ - **Judge the rest** — pre-pipeline PRs carry no note. Ask: could a user of
168
+ {{PRODUCT_NAME}} notice the difference? Record the judgment and the reason
169
+ in your fragment's `notes` so the reviewer can check it.
170
+
171
+ ## Status arcs
172
+
173
+ Every anatomy node gets an honest arc of **1–3 events ending at its snapshot
174
+ status**: the `node.created` that merge synthesizes from `created_ts`, plus
175
+ **0–2 `node.status_changed` written by you** — one per transition that
176
+ actually happened, the final one landing on the node's snapshot status.
177
+ **Never invent a transition that did not happen.** A born-live node's arc is
178
+ its `node.created` alone — write no status event for it, not a fabricated
179
+ idea → development → live staircase. Build the arc from evidence: the PR
180
+ that first shipped the surface, the era it was built in. Timestamp mechanics
181
+ (distinct instants, `changed_ts`) are in
182
+ [references/fragments.md](references/fragments.md).
183
+
184
+ ## Never delete
185
+
186
+ Bootstrap never deletes. In a brownfield reconcile, a node that exists in
187
+ the map but no longer in the product is **retired** — a `retire` op with a
188
+ `reason` — and merge archives it with the reason on record. Removal stays a
189
+ human decision, made outside this run. If you find yourself wanting to
190
+ delete, you are holding the wrong tool.
@@ -18,10 +18,10 @@ block below — run `npm run generate`.
18
18
 
19
19
  <!-- GENERATED:SCHEMA:START -->
20
20
  ```typescript
21
- type SpeciesId = "flow" | "view" | "data-model" | "api-endpoint" | "acceptance";
22
- type StatusId = "idea" | "backlog" | "prioritized" | "development" | "releasing" | "live" | "archived" | "blocked";
21
+ type SpeciesId = "flow" | "view" | "data-model" | "api-endpoint" | "acceptance" | "decision";
22
+ type StatusId = "idea" | "discovery" | "backlog" | "development" | "releasing" | "live" | "archived";
23
23
  type PlatformId = "web" | "ios" | "android";
24
- type EdgeTypeId = "composes" | "calls" | "displays" | "queries" | "covers";
24
+ type EdgeTypeId = "composes" | "calls" | "displays" | "queries" | "covers" | "supersedes" | "generates" | "impacts";
25
25
 
26
26
  type PlaylistEntry =
27
27
  | { type: "view"; view_id: string }
@@ -72,6 +72,8 @@ interface Ref {
72
72
 
73
73
  interface NodeMetadata extends Record<string, unknown> {
74
74
  stage?: string;
75
+ /** Non-empty = the node is blocked at its current status. A node id (rendered as a link) or free text. */
76
+ blocked_by?: string;
75
77
  playlist?: FlowPlaylist;
76
78
  platformNotes?: PlatformNotesMap;
77
79
  platformStatuses?: PlatformStatusMap;
@@ -81,6 +83,16 @@ interface NodeMetadata extends Record<string, unknown> {
81
83
  gherkin?: string;
82
84
  /** Acceptance nodes: value elements served — the Why (spec §3.2). */
83
85
  values?: ValueId[];
86
+ /** Product membership; meaningful on flow, view, and acceptance only. */
87
+ product?: string;
88
+ /** Decision nodes: the decision's own status (spec §2). Not a lifecycle status. */
89
+ decision_status?: DecisionStatusId;
90
+ /** Decision nodes: Context — the Why (markdown). */
91
+ context?: string;
92
+ /** Decision nodes: Consequences — the How (markdown). */
93
+ consequences?: string;
94
+ /** Decision nodes: ISO 8601 date the decision was actually made (backfill-friendly; node.created events carry the write date, not this). */
95
+ decided_at?: string;
84
96
  }
85
97
 
86
98
  interface Node {
@@ -103,9 +115,65 @@ interface Edge {
103
115
  metadata?: Record<string, unknown>;
104
116
  }
105
117
 
118
+ type MapKind = "journey" | "system";
119
+
120
+ interface MapLayoutHints extends Record<string, unknown> {
121
+ direction?: "DOWN" | "RIGHT" | (string & {});
122
+ /**
123
+ * Canvas layout algorithm: `"organic"` (force-directed with overlap
124
+ * removal) or `"layered"` (hierarchical tiers). Renderers fall back to the
125
+ * kind's default for unknown values (docs/spec/maps.md § MapDefinition).
126
+ */
127
+ algorithm?: "layered" | "organic" | (string & {});
128
+ }
129
+
130
+ type MapFlowPlatformsMode = "rings" | "bars";
131
+
132
+ type MapViewPlatformsMode = "chips" | "rows";
133
+
134
+ interface MapDisplayOptions extends Record<string, unknown> {
135
+ /** Screenshot (or cover) art on view cards. */
136
+ images?: boolean;
137
+ /** A flow card's platform delivery: the Pyramid's rings, or stacked bars. */
138
+ flow_platforms?: MapFlowPlatformsMode | (string & {});
139
+ /** A view card's platform availability: circular chips, or labelled rows. */
140
+ view_platforms?: MapViewPlatformsMode | (string & {});
141
+ /** What a minimap node's fill encodes: its status, or its species. */
142
+ minimap_color?: MapMinimapColorMode | (string & {});
143
+ }
144
+
145
+ interface MapDefinition extends Record<string, unknown> {
146
+ /** Kebab-case, unique within the project; built-in ids are reserved. */
147
+ id: string;
148
+ title: string;
149
+ description?: string;
150
+ /** Selects the renderer and the selection defaults below. */
151
+ kind: MapKind | (string & {});
152
+ /** Node filter; defaults by kind (docs/spec/maps.md § MapDefinition). */
153
+ species?: (SpeciesId | (string & {}))[];
154
+ /** Edge filter; defaults by kind. */
155
+ edge_types?: (EdgeTypeId | (string & {}))[];
156
+ /** Scope anchor; the journey renderer falls back to `project.root_node_id`. */
157
+ root_node_id?: string;
158
+ /** Product scope; absent = every product (docs/spec/bundle-format.md § Products). */
159
+ product?: string;
160
+ /** Traversal bound from the root; absent = unbounded. */
161
+ depth?: number;
162
+ layout?: MapLayoutHints;
163
+ /** Card rendering; the human twin is `project.metadata.map_display[id]`. */
164
+ display?: MapDisplayOptions;
165
+ }
166
+
106
167
  interface ProjectMetadata extends Record<string, unknown> {
168
+ /**
169
+ * @deprecated Superseded by the per-map `map_display` below. Still parsed,
170
+ * validated, and round-tripped; no renderer reads it.
171
+ */
107
172
  view_card_variant?: "compact" | "large";
108
173
  maps?: MapDefinition[];
174
+ /** Per-map display overrides keyed by map id — built-ins included. */
175
+ map_display?: Record<string, MapDisplayOptions>;
176
+ products?: ProductDefinition[];
109
177
  }
110
178
 
111
179
  interface Project {
@@ -229,10 +297,12 @@ type KnownJournalEvent =
229
297
  | NodeCreatedEvent
230
298
  | NodeUpdatedEvent
231
299
  | NodeStatusChangedEvent
300
+ | DecisionStatusChangedEvent
232
301
  | NodeDeletedEvent
233
302
  | EdgeAddedEvent
234
303
  | EdgeRemovedEvent
235
304
  | ReleaseTaggedEvent
305
+ | DeliverableShippedEvent
236
306
  | IdeaProposedEvent
237
307
  | RequestFiledEvent
238
308
  | RefAddedEvent
@@ -282,8 +352,11 @@ exist in the bundle's `nodes` array.
282
352
  | `calls` | view → api-endpoint | View calls this API |
283
353
  | `calls` | flow → api-endpoint | Flow calls this API |
284
354
  | `calls` | api-endpoint → api-endpoint | Endpoint calls another (internal or third-party) API — e.g. a server action / BFF route fanning out to external APIs |
355
+ | `calls` | api-endpoint → view | The server initiates: a webhook, an SSE stream, a push landing on this view (the View card's inbound/read affordance) |
285
356
  | `displays` | view → data-model | View displays data from this model |
286
357
  | `queries` | api-endpoint → data-model | API reads or writes this model |
358
+ | `covers` | acceptance → view | Acceptance anchors a testable promise to this view |
359
+ | `covers` | acceptance → flow | Acceptance anchors a testable promise to this flow |
287
360
 
288
361
  Any other source → target combination for a given edge type is invalid.
289
362