arkaik 0.1.0

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,344 @@
1
+ ---
2
+ name: arkaik
3
+ version: 3.0.0
4
+ description: >
5
+ Maintain the Arkaik product graph map for {{PRODUCT_NAME}} — add, update, or
6
+ remove nodes and edges in the ProjectBundle JSON that describes its screens,
7
+ flows, data models, and API endpoints, and record every change as a journal
8
+ event. Use this skill whenever you create, rename, move, or delete a
9
+ view/screen, route, data model, or API endpoint in the codebase. Also use it
10
+ when changing a feature's status (idea → development → live), adding a new user
11
+ journey, or restructuring navigation. If your code change touches product
12
+ architecture, this skill applies — even if no one explicitly asked you to
13
+ update the map.
14
+ ---
15
+
16
+ # Arkaik Map Maintenance
17
+
18
+ You are maintaining an **Arkaik ProjectBundle** — a JSON file that describes the
19
+ product architecture of {{PRODUCT_NAME}} as a graph of nodes (screens, flows,
20
+ data models, API endpoints) and edges (relationships between them) — plus a
21
+ **journal**, an append-only log of typed events recording how that graph changed
22
+ over time.
23
+
24
+ The map lives at `{{BUNDLE_PATH}}` in the repository. If it doesn't exist at
25
+ that path yet, check the project root and `docs/` for any `*arkaik*.json` file.
26
+ If you find one elsewhere, use it where it is — don't move files without the
27
+ user's approval.
28
+
29
+ The journal is a sidecar file next to the snapshot at `{{JOURNAL_PATH}}` —
30
+ append-only, one JSON event per line (JSONL), **never inside the snapshot**. If
31
+ the journal doesn't exist yet, the map is a plain snapshot with no history; start
32
+ one the first time you make a change (see [Dual-write](#dual-write-snapshot--journal)).
33
+
34
+ > **Template parameters.** This skill is *rendered*, not copied — `arkaik init`
35
+ > (and `arkaik init --update`, which reads the `version` stamp in the frontmatter
36
+ > above to upgrade cleanly instead of blind-overwriting local edits) substitutes
37
+ > these per project. If you are reading a raw, unrendered copy, treat the defaults
38
+ > in parentheses as the values:
39
+ >
40
+ > | Parameter | Meaning | Default |
41
+ > |---|---|---|
42
+ > | `{{PRODUCT_NAME}}` | The product this map describes | the current product |
43
+ > | `{{PROJECT_ID}}` | Kebab-case `project.id` for this map (distinct from the display name above) | a kebab-case slug of the product name |
44
+ > | `{{BUNDLE_PATH}}` | Path to the snapshot | `docs/arkaik/bundle.json` |
45
+ > | `{{JOURNAL_PATH}}` | Path to the journal sidecar | `docs/arkaik/journal.jsonl` |
46
+
47
+ ## When to Update the Map
48
+
49
+ Update the map as a side-effect of your main work whenever you:
50
+
51
+ - **Add** a new screen, page, route, component, model, or endpoint
52
+ - **Remove** or deprecate a feature, screen, or endpoint
53
+ - **Rename** a view, model, or route
54
+ - **Change status** of a feature (e.g., moving from `idea` to `development`)
55
+ - **Restructure** navigation or user flows
56
+ - **Add or change** API contracts
57
+
58
+ Do NOT regenerate the entire map. Make **surgical patches** — touch only the
59
+ nodes and edges affected by your change. This keeps diffs reviewable and avoids
60
+ accidental regressions.
61
+
62
+ ## Dual-write: snapshot + journal
63
+
64
+ The snapshot is authoritative for **current state**; the journal is authoritative
65
+ for **history**. Every change is a **dual-write**: in the *same* change you
66
+
67
+ 1. patch the snapshot (`{{BUNDLE_PATH}}`) surgically, **and**
68
+ 2. append the matching event(s) to the journal (`{{JOURNAL_PATH}}`).
69
+
70
+ Appending is a one-line addition to the JSONL sidecar — structurally incapable of
71
+ corrupting existing history or the snapshot, and safe under concurrent edits
72
+ (git's `merge=union` reorders lines; consumers order by `ts`, tiebreaking by `id`).
73
+
74
+ **Never re-project the snapshot from the journal, or vice versa.** If the two ever
75
+ disagree, that divergence is a signal to surface, not to launder — the validator
76
+ below is what catches it.
77
+
78
+ ### Which events to append
79
+
80
+ Each graph operation has a matching event. Append one line per operation:
81
+
82
+ | Graph operation | Journal event |
83
+ |---|---|
84
+ | Add a node | `node.created` (`node_id`, `species`, `title`) |
85
+ | Add an edge | `edge.added` (`edge_id`, `source_id`, `target_id`, `edge_type`) |
86
+ | Change a node's `status` | `node.status_changed` (`node_id`, `from`, `to`; add `platform` when a per-platform node status — an acceptance, or a not-yet-covered view — moved) |
87
+ | Change any other node field (rename, description, playlist…) | `node.updated` (`node_id`, `fields[]`; `from`/`to` for short scalars like `title`) |
88
+ | Remove a node | `node.deleted` (`node_id`) — **implies** cascade removal of its edges; do NOT also emit `edge.removed` for those |
89
+ | Remove an edge on its own (node stays) | `edge.removed` (`edge_id`) |
90
+ | Attach / detach an external reference | `ref.added` / `ref.removed` |
91
+
92
+ Other event types you may append when the change warrants it: `release.tagged`
93
+ (a version shipped), `idea.proposed` (an idea, before or linked to a node),
94
+ `request.filed` (an external ask), `ref.status_changed` (a mirrored external
95
+ status moved). See the [event vocabulary](#event-vocabulary) for full payloads.
96
+
97
+ ### Event envelope
98
+
99
+ Every event is one JSON object on its own line with these envelope fields, plus
100
+ the type-specific payload flat on the object:
101
+
102
+ ```json
103
+ { "id": "<ULID>", "ts": "<ISO 8601>", "actor": "claude-code", "type": "node.status_changed", "node_id": "V-home", "from": "development", "to": "live" }
104
+ ```
105
+
106
+ - `id` — a ULID (sortable, collision-free). If you cannot generate a real ULID,
107
+ any strictly increasing, unique, 26-char Crockford-base32 string works; order
108
+ is recovered from `ts` first, `id` only as a tiebreak.
109
+ - `ts` — an ISO 8601 timestamp (the moment of the change).
110
+ - `actor` — who wrote it; use `"claude-code"` for your own writes.
111
+ - Events carry **no** `project_id`; scope is the file they live in.
112
+
113
+ ## How to Update
114
+
115
+ ### 1. Read the current map
116
+
117
+ Always read the snapshot first. Parse it and identify the relevant nodes/edges
118
+ before making changes. If a journal sidecar exists, you don't need to read all of
119
+ it — you only ever **append**.
120
+
121
+ ### 2. Decide what to change
122
+
123
+ Map your code change to graph operations (and the events they pair with):
124
+
125
+ | Code change | Graph operation | Journal event(s) |
126
+ |---|---|---|
127
+ | New screen/page | Add a `V-` view node + `displays` edges to its data models + `calls` edges to its APIs | `node.created` + `edge.added` per edge |
128
+ | New route/endpoint | Add an `API-` node + `queries` edges to the data models it reads/writes | `node.created` + `edge.added` per edge |
129
+ | New model/table | Add a `DM-` node | `node.created` |
130
+ | New user journey | Add a `F-` flow node with a playlist + `composes` edges to all views/sub-flows in the playlist | `node.created` + `edge.added` per edge |
131
+ | Screen added to a flow | Add entry to the flow's playlist + a `composes` edge | `node.updated` (playlist) + `edge.added` |
132
+ | Feature removed | Remove the node + all edges referencing it + remove from any playlists | `node.deleted` (edges cascade — do not emit `edge.removed`) |
133
+ | Status change | Update the node's `status` field | `node.status_changed` |
134
+ | Rename (label only) | Update the node's `title`; keep the `id` stable so edges stay intact | `node.updated` (`fields: ["title"]`, with `from`/`to`) |
135
+ | Rename (id must change) | Update the `id`, then repoint every edge's `source_id`/`target_id` **and** the edge `id` (`e-{source}-{target}`), plus any playlist `view_id`/`flow_id` and `root_node_id` | `node.updated` + `edge.removed`/`edge.added` for each repointed edge |
136
+ | Ship/add user-visible behavior on a platform | Find or create the covering `AC-` acceptance + `covers` edge to the view/flow; set `metadata.platformStatuses.<platform>` on the acceptance | `node.created` + `edge.added` (new acceptance) + `node.status_changed` (`platform`) |
137
+
138
+ ### 3. Apply the change
139
+
140
+ Edit the snapshot JSON using the Edit tool for surgical changes, or Write for
141
+ larger restructuring. Follow these rules strictly:
142
+
143
+ **Node rules:**
144
+ - IDs are prefixed by species: `F-` (flow), `V-` (view), `DM-` (data-model), `API-` (api-endpoint)
145
+ - IDs use lowercase kebab-case after the prefix (e.g., `V-user-profile`)
146
+ - **IDs must be globally unique.** Derive each ID deterministically from the title,
147
+ then check it against every existing node ID before adding — a duplicate ID
148
+ breaks the entire graph render (elkjs/React Flow key nodes by ID).
149
+ - **Data-model IDs come in two flavors that must not collide:** conceptual models
150
+ use a singular `DM-<concept>` (title "Bounce" → `DM-bounce`); physical tables/views
151
+ use the exact identifier `DM-<table_name>` (title `bounces` → `DM-bounces`,
152
+ `karma_events` → `DM-karma-events`). See the schema reference for the full table.
153
+ - **Every node must have a non-empty `title`.** Concepts/views/flows/APIs use 2–5
154
+ capitalized words; physical tables/views use the exact DB identifier verbatim.
155
+ - Every `node.project_id` must match `project.id`
156
+ - `platforms` must contain at least one of: `"web"`, `"ios"`, `"android"`
157
+ - Flow nodes must have `metadata.playlist` with at least one entry
158
+
159
+ **Edge rules:**
160
+ - Edge IDs follow the pattern `e-{source_id}-{target_id}`
161
+ - Every `source_id` and `target_id` must reference existing node IDs
162
+ - Edge type semantics:
163
+ - `composes`: flow -> view, flow -> flow (sub-flow), view -> flow (triggers)
164
+ - `calls`: view -> api-endpoint, flow -> api-endpoint, api-endpoint -> api-endpoint (endpoint fan-out to internal/external APIs)
165
+ - `displays`: view -> data-model
166
+ - `queries`: api-endpoint -> data-model
167
+ - `covers`: acceptance -> view, acceptance -> flow
168
+ - Every view/flow referenced in a playlist MUST also have a `composes` edge
169
+
170
+ **Playlist rules:**
171
+ - Entry types: `view` (with `view_id`), `flow` (with `flow_id`), `condition` (with `label`, `if_true`, `if_false`), `junction` (with `label`, `cases`)
172
+ - All `view_id` / `flow_id` values must reference existing nodes
173
+ - No cycles — a flow cannot contain itself directly or indirectly
174
+
175
+ ### 4. Append the matching journal event
176
+
177
+ In the **same change**, append one line per graph operation to `{{JOURNAL_PATH}}`
178
+ (see [Dual-write](#dual-write-snapshot--journal)). Create the file if it doesn't
179
+ exist yet. This is not optional bookkeeping — the validator in the next step
180
+ cross-checks the two by value and **rejects a snapshot that its journal
181
+ contradicts** (e.g. a status the last `node.status_changed` never reached, or a
182
+ node with no `node.created`).
183
+
184
+ ### 5. Validate — hard gate, not optional
185
+
186
+ After **every** change (surgical patch or full generation), run the validator:
187
+
188
+ ```bash
189
+ node <skill-path>/scripts/validate-bundle.js <path-to-bundle.json>
190
+ ```
191
+
192
+ The validator auto-discovers the `journal.jsonl` sidecar next to the bundle and
193
+ folds it into the check, so pointing it at the snapshot gates **both** the patch
194
+ and the appended event in one run.
195
+
196
+ **A non-zero exit code is a hard stop.** Do not commit, hand off, or declare the
197
+ task done until it exits 0 — and that includes fixing the journal, not just the
198
+ snapshot. The map that ships is the one the app imports, so run the validator
199
+ against *that* file (e.g. the seed the app loads), not just a local working copy.
200
+ Where possible, wire this script into the consuming repo's CI or a pre-commit hook
201
+ so a broken bundle cannot land regardless of who edits it.
202
+
203
+ The validator enforces the full [Validation Checklist](references/schema.md#validation-checklist).
204
+ Common issues it catches:
205
+ - Duplicate node IDs (e.g. a concept and its backing table both kebab-cased to the same `DM-` id)
206
+ - A node with a missing or empty `title`
207
+ - Forgetting to add a `composes` edge when adding a view to a playlist
208
+ - Stale edge IDs/references after renaming or removing a node
209
+ - Missing `project_id` on new nodes
210
+ - An invalid `view_card_variant` (the app's import throws on it)
211
+
212
+ **Snapshot ↔ journal cross-checks** (the dual-write gate). The validator compares
213
+ the two **by value, never by timestamp** (per-node timestamps don't exist and
214
+ clocks lie) and errors on any mismatch, naming both sides:
215
+ - The last project-level `node.status_changed.to` for a node must equal its
216
+ current `status`. (Platform-scoped transitions — those carrying `platform` —
217
+ move a per-platform node status — an acceptance, or a not-yet-covered view —
218
+ not `node.status`, and are excluded.)
219
+ - Every node in the snapshot must have a `node.created` event.
220
+ - No event may reference a node or edge that never existed. The `node.deleted`
221
+ edge cascade is applied, so you never emit the cascaded `edge.removed` events.
222
+ - Each JSONL line must be one valid event object; a malformed line is reported by
223
+ its line number and fails the run.
224
+
225
+ ### 6. Update timestamps
226
+
227
+ Set `project.updated_at` to the current ISO 8601 timestamp.
228
+
229
+ ## Acceptances — the parity layer
230
+
231
+ An **acceptance** is a testable promise: a short `title` (the What), exactly one
232
+ Given/When/Then scenario in `metadata.gherkin` (the How), 1–3 value elements in
233
+ `metadata.values` (the Why), and a status per applicable platform. Acceptances
234
+ are where per-platform truth lives; views and flows are computed aggregates.
235
+
236
+ **Discipline — when you ship user-visible behavior on a platform:**
237
+
238
+ 1. Find the acceptance covering that behavior (`covers` edge into the view or
239
+ flow). If none exists, create one (`species: "acceptance"`, id prefix `AC-`).
240
+ 2. Set `metadata.platformStatuses.<platform>` on the **acceptance** — never on
241
+ the view. View `platformStatuses` is legacy fallback for views no acceptance
242
+ covers yet; do not write it on covered views.
243
+ 3. Append the matching `node.status_changed` event with the `platform` field.
244
+
245
+ Creating a new acceptance + covers edge is itself a dual-write: append
246
+ `node.created` and `edge.added` alongside the `node.status_changed`.
247
+
248
+ **Rules:**
249
+
250
+ - One Given/When/Then per acceptance — a second scenario is a second acceptance.
251
+ - `Given` encodes render variants ("Given the pebble has a picture attached…").
252
+ - `platforms` lists only the platforms where the behavior is *expected* — a
253
+ mobile-only behavior is `["ios", "android"]`, not backlog-on-web.
254
+ - `covers` edges: acceptance → view or acceptance → flow. Zero edges = a
255
+ product-level acceptance (legal). Several = the behavior spans surfaces.
256
+ - Statuses reuse the standard lifecycle; "shipped" = `live`.
257
+
258
+ **Example** — iOS ships the draw-in animation:
259
+
260
+ ```json
261
+ {
262
+ "id": "AC-pebble-draw-in-animation",
263
+ "project_id": "{{PROJECT_ID}}",
264
+ "species": "acceptance",
265
+ "title": "Pebble draw-in animation",
266
+ "status": "backlog",
267
+ "platforms": ["web", "ios", "android"],
268
+ "metadata": {
269
+ "gherkin": "When I'm on the Pebble Detail, Then I see the Pebble appearing in a drawing animation.",
270
+ "values": ["fun-entertainment", "design-aesthetics"],
271
+ "platformStatuses": { "ios": "live" }
272
+ }
273
+ }
274
+ ```
275
+
276
+ plus `{"type": "node.status_changed", "node_id": "AC-pebble-draw-in-animation", "from": "backlog", "to": "live", "platform": "ios"}` in the journal.
277
+
278
+ <!-- values:start -->
279
+ ### Value mapping
280
+
281
+ Assign 1–3 `metadata.values` from the 30-element Bain pyramid when creating an
282
+ acceptance. **If unsure, omit them** — enrichment passes exist; a wrong value is
283
+ worse than a missing one. Consult `references/values.md` (one-line definitions
284
+ per element) only when actually mapping — do not load it otherwise.
285
+ <!-- values:end -->
286
+
287
+ ## Full Schema Reference
288
+
289
+ For the complete TypeScript types, allowed values for `status`, `species`,
290
+ `edge_type`, `platform`, the journal event payloads, and detailed playlist
291
+ structures, read:
292
+
293
+ ```
294
+ <skill-path>/references/schema.md
295
+ ```
296
+
297
+ Consult this reference whenever you're unsure about a field's type or allowed
298
+ values. It is generated from `@arkaik/schema` and is the source of truth.
299
+
300
+ ## Event Vocabulary
301
+
302
+ The v1 event types and their payloads (envelope fields `id`, `ts`, `type`,
303
+ optional `actor` are implied on every one). The vocabulary grows without version
304
+ bumps: an unknown `type` is preserved on rewrite and ignored on read, so append a
305
+ known type whenever one fits.
306
+
307
+ | Type | Payload | Meaning |
308
+ |---|---|---|
309
+ | `node.created` | `node_id`, `species`, `title` | Node added to the graph |
310
+ | `node.updated` | `node_id`, `fields[]`, optional `from`/`to` for scalars | Non-status fields changed |
311
+ | `node.status_changed` | `node_id`, `from`, `to`, `platform?` | Lifecycle transition; `platform` present when a per-platform node status — an acceptance, or a not-yet-covered view — moved |
312
+ | `node.deleted` | `node_id` | Node removed. **Implies** cascade removal of every edge referencing it — do not emit the cascaded `edge.removed` events |
313
+ | `edge.added` | `edge_id`, `source_id`, `target_id`, `edge_type` | Relationship created |
314
+ | `edge.removed` | `edge_id` | Relationship removed (non-cascade) |
315
+ | `release.tagged` | `version`, `notes?`, `platform?` | A version shipped. `platform` absent = project-wide; present = that platform's rhythm |
316
+ | `idea.proposed` | `title`, `description?`, `node_id?` | An idea, before (or linked to) any node |
317
+ | `request.filed` | `title`, `description?`, `source?`, `node_id?` | An external ask (user feedback, stakeholder request) |
318
+ | `ref.added` | `node_id`, `ref_id`, `ref_type`, `url` | External reference attached |
319
+ | `ref.removed` | `node_id`, `ref_id` | External reference detached |
320
+ | `ref.status_changed` | `node_id`, `ref_id`, `from?`, `to`, `synced_at` | Mirrored external status moved (issue closed, PR merged) |
321
+
322
+ ## Bootstrap: Generating a Map from Scratch
323
+
324
+ If the project doesn't have a map yet and the user asks you to create one:
325
+
326
+ 1. Scan the codebase for routes/pages, models, and API endpoints
327
+ 2. Read any product specs or PRDs in the repo
328
+ 3. Generate the full ProjectBundle following the schema reference. Keep a running
329
+ registry of assigned IDs and assert each new ID is unique **as you emit it** —
330
+ don't rely on the final validator to catch a collision after the fact. Watch
331
+ especially for concept-vs-table `DM-` pairs (see Node rules) and give every
332
+ node a non-empty title.
333
+ 4. Seed the journal to match: emit a `node.created` for every node and an
334
+ `edge.added` for every edge (and a `node.status_changed` for any node that
335
+ isn't at its starting status), so the snapshot↔journal cross-check passes from
336
+ the very first validation.
337
+ 5. Validate with the bundled script and fix everything until it exits 0 (a large
338
+ from-scratch bundle is exactly where duplicate IDs, missing titles, and
339
+ snapshot↔journal gaps slip in)
340
+ 6. Save the snapshot to `{{BUNDLE_PATH}}` and the journal to `{{JOURNAL_PATH}}`
341
+ (or ask the user where they want them)
342
+
343
+ Full generation is the **only** sanctioned non-surgical case. For every
344
+ subsequent change, use a surgical patch paired with an appended event.