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.
- package/dist/assets/bootstrap-skill/references/fragments.md +203 -0
- package/dist/assets/bootstrap-skill/references/waves.md +120 -0
- package/dist/assets/bootstrap-skill/skill.md +190 -0
- package/dist/assets/skill/references/schema.md +76 -3
- package/dist/assets/skill/scripts/validate-bundle.js +4 -4
- package/dist/assets/skill/skill.md +84 -6
- package/dist/index.js +2695 -286
- package/dist/io.js +497 -61
- package/package.json +1 -1
|
@@ -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" | "
|
|
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
|
|