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.
- package/dist/assets/skill/references/schema.md +368 -0
- package/dist/assets/skill/references/values.md +47 -0
- package/dist/assets/skill/scripts/validate-bundle.js +18 -0
- package/dist/assets/skill/skill.md +344 -0
- package/dist/index.js +17693 -0
- package/dist/io.js +15653 -0
- package/package.json +38 -0
- package/src/io.ts +23 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Arkaik ProjectBundle Schema Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Types](#types)
|
|
6
|
+
2. [Playlist Entries](#playlist-entries)
|
|
7
|
+
3. [Edge Type Semantics](#edge-type-semantics)
|
|
8
|
+
4. [ID Conventions](#id-conventions)
|
|
9
|
+
5. [Validation Checklist](#validation-checklist)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Types
|
|
14
|
+
|
|
15
|
+
The canonical shape of a ProjectBundle, generated from the `@arkaik/schema` zod
|
|
16
|
+
definitions (`docs/spec/toolchain.md` § @arkaik/schema). Do not hand-edit the
|
|
17
|
+
block below — run `npm run generate`.
|
|
18
|
+
|
|
19
|
+
<!-- GENERATED:SCHEMA:START -->
|
|
20
|
+
```typescript
|
|
21
|
+
type SpeciesId = "flow" | "view" | "data-model" | "api-endpoint" | "acceptance";
|
|
22
|
+
type StatusId = "idea" | "backlog" | "prioritized" | "development" | "releasing" | "live" | "archived" | "blocked";
|
|
23
|
+
type PlatformId = "web" | "ios" | "android";
|
|
24
|
+
type EdgeTypeId = "composes" | "calls" | "displays" | "queries" | "covers";
|
|
25
|
+
|
|
26
|
+
type PlaylistEntry =
|
|
27
|
+
| { type: "view"; view_id: string }
|
|
28
|
+
| { type: "flow"; flow_id: string }
|
|
29
|
+
| { type: "condition"; label: string; if_true: PlaylistEntry[]; if_false: PlaylistEntry[] }
|
|
30
|
+
| { type: "junction"; label: string; cases: JunctionCase[] };
|
|
31
|
+
|
|
32
|
+
interface JunctionCase {
|
|
33
|
+
label: string;
|
|
34
|
+
entries: PlaylistEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FlowPlaylist {
|
|
38
|
+
entries: PlaylistEntry[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type PlatformNotesMap = Partial<Record<PlatformId, string>>;
|
|
42
|
+
type PlatformStatusMap = Partial<Record<PlatformId, StatusId>>;
|
|
43
|
+
type PlatformScreenshotsMap = Partial<Record<PlatformId, string>>;
|
|
44
|
+
|
|
45
|
+
type RefType =
|
|
46
|
+
| "figma"
|
|
47
|
+
| "github-issue"
|
|
48
|
+
| "gitlab-issue"
|
|
49
|
+
| "linear-issue"
|
|
50
|
+
| "github-pr"
|
|
51
|
+
| "gitlab-mr"
|
|
52
|
+
| "url";
|
|
53
|
+
|
|
54
|
+
interface Ref {
|
|
55
|
+
/** Unique within the node, kebab-case (e.g. "gh-142"). */
|
|
56
|
+
id: string;
|
|
57
|
+
/** One of {@link RefType}; unrecognized values are preserved and render as generic links. */
|
|
58
|
+
type: RefType | (string & {});
|
|
59
|
+
/** Canonical external URL. */
|
|
60
|
+
url: string;
|
|
61
|
+
/** Display label. */
|
|
62
|
+
title?: string;
|
|
63
|
+
/** Mirrored external state, verbatim (e.g. "open", "merged", "In Progress"). */
|
|
64
|
+
external_status?: string;
|
|
65
|
+
/** Optional mapping of external_status into the arkaik lifecycle. Advisory display data — never mutates node.status. */
|
|
66
|
+
status_mapped?: StatusId;
|
|
67
|
+
/** Optional scoping to one platform variant. */
|
|
68
|
+
platform?: PlatformId;
|
|
69
|
+
/** ISO 8601 — when external_status was last mirrored. */
|
|
70
|
+
synced_at?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface NodeMetadata extends Record<string, unknown> {
|
|
74
|
+
stage?: string;
|
|
75
|
+
playlist?: FlowPlaylist;
|
|
76
|
+
platformNotes?: PlatformNotesMap;
|
|
77
|
+
platformStatuses?: PlatformStatusMap;
|
|
78
|
+
platformScreenshots?: PlatformScreenshotsMap;
|
|
79
|
+
refs?: Ref[];
|
|
80
|
+
/** Acceptance nodes: one Given/When/Then scenario — the How (spec §3.1). */
|
|
81
|
+
gherkin?: string;
|
|
82
|
+
/** Acceptance nodes: value elements served — the Why (spec §3.2). */
|
|
83
|
+
values?: ValueId[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Node {
|
|
87
|
+
id: string;
|
|
88
|
+
project_id: string;
|
|
89
|
+
species: SpeciesId;
|
|
90
|
+
title: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
status: StatusId;
|
|
93
|
+
platforms: PlatformId[];
|
|
94
|
+
metadata?: NodeMetadata;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface Edge {
|
|
98
|
+
id: string;
|
|
99
|
+
project_id: string;
|
|
100
|
+
source_id: string;
|
|
101
|
+
target_id: string;
|
|
102
|
+
edge_type: EdgeTypeId;
|
|
103
|
+
metadata?: Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ProjectMetadata extends Record<string, unknown> {
|
|
107
|
+
view_card_variant?: "compact" | "large";
|
|
108
|
+
maps?: MapDefinition[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface Project {
|
|
112
|
+
id: string;
|
|
113
|
+
title: string;
|
|
114
|
+
description?: string;
|
|
115
|
+
/** v2: current version label, free-form (semver recommended, not required), e.g. "1.4.0" or "2026-07". Version history lives in the journal. */
|
|
116
|
+
version?: string;
|
|
117
|
+
/** Optional node id used as the primary canvas anchor/root. */
|
|
118
|
+
root_node_id?: string;
|
|
119
|
+
/** Optional project-level UI settings and preferences. */
|
|
120
|
+
metadata?: ProjectMetadata;
|
|
121
|
+
/** ISO 8601 timestamp, e.g. "2024-01-01T00:00:00.000Z" */
|
|
122
|
+
created_at: string;
|
|
123
|
+
/** ISO 8601 timestamp, e.g. "2024-01-01T00:00:00.000Z" */
|
|
124
|
+
updated_at: string;
|
|
125
|
+
/** ISO 8601 timestamp when archived; null/undefined means active. */
|
|
126
|
+
archived_at?: string | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface JournalEvent extends Record<string, unknown> {
|
|
130
|
+
/** ULID — sortable, collision-free without coordination. */
|
|
131
|
+
id: string;
|
|
132
|
+
/** ISO 8601 timestamp. */
|
|
133
|
+
ts: string;
|
|
134
|
+
/** Who/what wrote it: "alexis", "claude-code", "arkaik-sync", "ci". */
|
|
135
|
+
actor?: string;
|
|
136
|
+
/** Event type — the v1 vocabulary, or an unknown forward-compatible value. */
|
|
137
|
+
type: string;
|
|
138
|
+
/** Reserved per-event payload version, for the day a payload shape changes. */
|
|
139
|
+
v?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface NodeCreatedEvent extends JournalEvent {
|
|
143
|
+
type: "node.created";
|
|
144
|
+
node_id: string;
|
|
145
|
+
species: SpeciesId;
|
|
146
|
+
title: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface NodeUpdatedEvent extends JournalEvent {
|
|
150
|
+
type: "node.updated";
|
|
151
|
+
node_id: string;
|
|
152
|
+
fields: string[];
|
|
153
|
+
from?: unknown;
|
|
154
|
+
to?: unknown;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface NodeStatusChangedEvent extends JournalEvent {
|
|
158
|
+
type: "node.status_changed";
|
|
159
|
+
node_id: string;
|
|
160
|
+
from: StatusId;
|
|
161
|
+
to: StatusId;
|
|
162
|
+
platform?: PlatformId;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface NodeDeletedEvent extends JournalEvent {
|
|
166
|
+
type: "node.deleted";
|
|
167
|
+
node_id: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface EdgeAddedEvent extends JournalEvent {
|
|
171
|
+
type: "edge.added";
|
|
172
|
+
edge_id: string;
|
|
173
|
+
source_id: string;
|
|
174
|
+
target_id: string;
|
|
175
|
+
edge_type: EdgeTypeId;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface EdgeRemovedEvent extends JournalEvent {
|
|
179
|
+
type: "edge.removed";
|
|
180
|
+
edge_id: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface ReleaseTaggedEvent extends JournalEvent {
|
|
184
|
+
type: "release.tagged";
|
|
185
|
+
version: string;
|
|
186
|
+
notes?: string;
|
|
187
|
+
platform?: PlatformId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface IdeaProposedEvent extends JournalEvent {
|
|
191
|
+
type: "idea.proposed";
|
|
192
|
+
title: string;
|
|
193
|
+
description?: string;
|
|
194
|
+
node_id?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface RequestFiledEvent extends JournalEvent {
|
|
198
|
+
type: "request.filed";
|
|
199
|
+
title: string;
|
|
200
|
+
description?: string;
|
|
201
|
+
source?: string;
|
|
202
|
+
node_id?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface RefAddedEvent extends JournalEvent {
|
|
206
|
+
type: "ref.added";
|
|
207
|
+
node_id: string;
|
|
208
|
+
ref_id: string;
|
|
209
|
+
ref_type: string;
|
|
210
|
+
url: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface RefRemovedEvent extends JournalEvent {
|
|
214
|
+
type: "ref.removed";
|
|
215
|
+
node_id: string;
|
|
216
|
+
ref_id: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
interface RefStatusChangedEvent extends JournalEvent {
|
|
220
|
+
type: "ref.status_changed";
|
|
221
|
+
node_id: string;
|
|
222
|
+
ref_id: string;
|
|
223
|
+
from?: string;
|
|
224
|
+
to: string;
|
|
225
|
+
synced_at: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type KnownJournalEvent =
|
|
229
|
+
| NodeCreatedEvent
|
|
230
|
+
| NodeUpdatedEvent
|
|
231
|
+
| NodeStatusChangedEvent
|
|
232
|
+
| NodeDeletedEvent
|
|
233
|
+
| EdgeAddedEvent
|
|
234
|
+
| EdgeRemovedEvent
|
|
235
|
+
| ReleaseTaggedEvent
|
|
236
|
+
| IdeaProposedEvent
|
|
237
|
+
| RequestFiledEvent
|
|
238
|
+
| RefAddedEvent
|
|
239
|
+
| RefRemovedEvent
|
|
240
|
+
| RefStatusChangedEvent;
|
|
241
|
+
|
|
242
|
+
interface ProjectBundle {
|
|
243
|
+
/** Bundle Format contract version (docs/spec/bundle-format.md § Schema Versioning). Absent MUST be treated as 1. */
|
|
244
|
+
schema_version?: number;
|
|
245
|
+
project: Project;
|
|
246
|
+
nodes: Node[];
|
|
247
|
+
edges: Edge[];
|
|
248
|
+
/** Optional embedded journal — the interchange projection (Level 2). Canonical storage is the JSONL sidecar; see docs/spec/journal.md. */
|
|
249
|
+
journal?: JournalEvent[];
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
<!-- GENERATED:SCHEMA:END -->
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Playlist Entries
|
|
257
|
+
|
|
258
|
+
Flows orchestrate views through an ordered playlist (`PlaylistEntry`, above).
|
|
259
|
+
Each entry is one of `view`, `flow`, `condition`, or `junction`.
|
|
260
|
+
|
|
261
|
+
**Condition** is a binary branch (yes/no question). The `label` is a question
|
|
262
|
+
(e.g., "Email verified?"), and `if_true` / `if_false` contain the entries for
|
|
263
|
+
each branch. Either branch can be empty `[]` to mean "skip."
|
|
264
|
+
|
|
265
|
+
**Junction** is a multi-way branch. The `label` is a question (e.g., "What
|
|
266
|
+
action?"), and `cases` is an array of labeled branches, each with its own
|
|
267
|
+
entries.
|
|
268
|
+
|
|
269
|
+
Every `view_id` and `flow_id` in playlist entries must reference node IDs that
|
|
270
|
+
exist in the bundle's `nodes` array.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Edge Type Semantics
|
|
275
|
+
|
|
276
|
+
| Edge type | Valid source → target | Meaning |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `composes` | flow → view | Flow contains this view in its playlist |
|
|
279
|
+
| `composes` | flow → flow | Flow contains this sub-flow in its playlist |
|
|
280
|
+
| `composes` | view → flow | View triggers/navigates to this flow |
|
|
281
|
+
| `composes` | view → view | View contains or navigates to this view |
|
|
282
|
+
| `calls` | view → api-endpoint | View calls this API |
|
|
283
|
+
| `calls` | flow → api-endpoint | Flow calls this API |
|
|
284
|
+
| `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 |
|
|
285
|
+
| `displays` | view → data-model | View displays data from this model |
|
|
286
|
+
| `queries` | api-endpoint → data-model | API reads or writes this model |
|
|
287
|
+
|
|
288
|
+
Any other source → target combination for a given edge type is invalid.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## ID Conventions
|
|
293
|
+
|
|
294
|
+
| Species | Prefix | Example |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| flow | `F-` | `F-record-pebble` |
|
|
297
|
+
| view | `V-` | `V-pebble-detail` |
|
|
298
|
+
| data-model | `DM-` | `DM-emotion-pearl` |
|
|
299
|
+
| api-endpoint | `API-` | `API-create-pebble` |
|
|
300
|
+
|
|
301
|
+
After the prefix, use lowercase kebab-case. Keep IDs short but meaningful —
|
|
302
|
+
they appear in the Arkaik UI.
|
|
303
|
+
|
|
304
|
+
Edge IDs: `e-{source_id}-{target_id}` (e.g., `e-V-home-F-onboarding`).
|
|
305
|
+
|
|
306
|
+
### IDs must be globally unique
|
|
307
|
+
|
|
308
|
+
A node ID identifies exactly one node. The graph layout (elkjs) and the canvas
|
|
309
|
+
(React Flow) both key nodes by ID, so **two nodes sharing an ID break the entire
|
|
310
|
+
graph render**, and any edge pointing at that ID silently resolves to whichever
|
|
311
|
+
node was defined last.
|
|
312
|
+
|
|
313
|
+
Derive IDs **deterministically from the title**, then check the result against
|
|
314
|
+
every existing ID before adding the node. If a derived ID already exists but the
|
|
315
|
+
node is genuinely different, disambiguate the ID (do not reuse it).
|
|
316
|
+
|
|
317
|
+
### Data-model IDs: concepts vs. physical tables
|
|
318
|
+
|
|
319
|
+
`DM-` nodes come in two flavors that must derive **distinct** IDs so they never
|
|
320
|
+
collide:
|
|
321
|
+
|
|
322
|
+
| Flavor | Title style | ID derivation | Examples |
|
|
323
|
+
|---|---|---|---|
|
|
324
|
+
| **Conceptual model** | Capitalized noun ("Pebble", "Bounce", "Soul") | `DM-<concept>` (singular) | `DM-pebble`, `DM-bounce`, `DM-soul` |
|
|
325
|
+
| **Physical table / view** | Exact DB identifier, lowercase snake_case (`bounces`, `karma_events`, `v_analytics_kpi_daily`) | `DM-<table_name>` with underscores → hyphens, **preserving pluralisation** | `DM-bounces`, `DM-karma-events`, `DM-v-analytics-kpi-daily` |
|
|
326
|
+
|
|
327
|
+
The concept **Bounce** (`DM-bounce`) and the table **bounces** (`DM-bounces`) are
|
|
328
|
+
different nodes — kebab-casing both to `DM-bounce` is the collision that broke the
|
|
329
|
+
map. When a concept and its backing table both exist, keep the concept singular
|
|
330
|
+
and the table plural/exact so their IDs differ.
|
|
331
|
+
|
|
332
|
+
### Titles
|
|
333
|
+
|
|
334
|
+
- **Views, flows, API endpoints, conceptual data-models:** 2–5 words, descriptive
|
|
335
|
+
and capitalized (e.g., "User Profile", "Record Pebble", "GET /bounce", "Pebble").
|
|
336
|
+
- **Physical table / view data-models:** the exact database identifier verbatim
|
|
337
|
+
(e.g., `bounces`, `karma_events`, `v_analytics_kpi_daily`) — do not prettify.
|
|
338
|
+
|
|
339
|
+
Every node's `title` must be present and non-empty regardless of species.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Validation Checklist
|
|
344
|
+
|
|
345
|
+
Before saving any changes, verify:
|
|
346
|
+
|
|
347
|
+
1. All node IDs are unique across the whole bundle (no two nodes share an ID)
|
|
348
|
+
2. All node IDs have the correct species prefix
|
|
349
|
+
3. Every node has a non-empty `title`
|
|
350
|
+
4. All `node.project_id` values match `project.id`
|
|
351
|
+
5. All `edge.source_id` and `edge.target_id` reference existing node IDs
|
|
352
|
+
6. All `edge.project_id` values match `project.id`
|
|
353
|
+
7. Every edge ID follows `e-{source_id}-{target_id}` (update it when you repoint an edge)
|
|
354
|
+
8. No duplicate edge relationships (same source, target, and type)
|
|
355
|
+
9. `project.root_node_id` references an existing node
|
|
356
|
+
10. All `view_id` / `flow_id` in playlists reference existing node IDs
|
|
357
|
+
11. Every view/flow referenced in a playlist has a corresponding `composes` edge
|
|
358
|
+
12. No playlist cycles (a flow does not contain itself directly or indirectly)
|
|
359
|
+
13. All flow nodes have `metadata.playlist` with at least one entry
|
|
360
|
+
14. All `platforms` arrays have at least one value
|
|
361
|
+
15. Any `metadata.stage` is one of `beta` / `monitoring` / `deprecated`
|
|
362
|
+
16. Any `metadata.platformStatuses` / `platformNotes` use valid platforms (and statuses); `platformStatuses` keys are a subset of `node.platforms`
|
|
363
|
+
17. `project.metadata.view_card_variant`, if set, is `compact` or `large` (import **rejects** other values)
|
|
364
|
+
18. Edge types follow the valid source → target patterns
|
|
365
|
+
19. `created_at` / `updated_at` are valid ISO 8601 timestamps; `updated_at` is current
|
|
366
|
+
|
|
367
|
+
The bundled validator (`scripts/validate-bundle.js`) enforces every item above.
|
|
368
|
+
Treat a non-zero exit code as a hard stop — do not commit a bundle it rejects.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Value Elements — Bain B2C Pyramid (30)
|
|
2
|
+
|
|
3
|
+
One-line definitions for `metadata.values`. Assign 1–3 per acceptance; prefer
|
|
4
|
+
the most specific element; when torn between tiers, pick the higher tier only
|
|
5
|
+
if the acceptance genuinely operates there.
|
|
6
|
+
|
|
7
|
+
## Functional
|
|
8
|
+
|
|
9
|
+
- `saves-time` — completes the user's task in less time.
|
|
10
|
+
- `simplifies` — reduces steps, choices, or cognitive load.
|
|
11
|
+
- `makes-money` — helps the user earn.
|
|
12
|
+
- `reduces-risk` — protects against loss, error, or uncertainty.
|
|
13
|
+
- `organizes` — brings order to the user's things or life.
|
|
14
|
+
- `integrates` — ties different tools/parts of life together.
|
|
15
|
+
- `connects` — brings the user together with other people.
|
|
16
|
+
- `reduces-effort` — same outcome, less work.
|
|
17
|
+
- `avoids-hassles` — prevents friction and annoyance before it happens.
|
|
18
|
+
- `reduces-cost` — saves the user money.
|
|
19
|
+
- `quality` — superior craft or performance the user can feel.
|
|
20
|
+
- `variety` — meaningful choice and breadth.
|
|
21
|
+
- `sensory-appeal` — looks, sounds, or feels good in the moment.
|
|
22
|
+
- `informs` — tells the user something they want to know.
|
|
23
|
+
|
|
24
|
+
## Emotional
|
|
25
|
+
|
|
26
|
+
- `reduces-anxiety` — calms; makes the user feel safe.
|
|
27
|
+
- `rewards-me` — tangible perks for engagement.
|
|
28
|
+
- `nostalgia` — positive memory of the past.
|
|
29
|
+
- `design-aesthetics` — beauty as an experienced value, beyond function.
|
|
30
|
+
- `badge-value` — signals identity or status to others.
|
|
31
|
+
- `wellness` — improves physical or mental well-being.
|
|
32
|
+
- `therapeutic-value` — actively supports emotional processing or healing.
|
|
33
|
+
- `fun-entertainment` — enjoyable, playful, delightful.
|
|
34
|
+
- `attractiveness` — makes the user feel attractive.
|
|
35
|
+
- `provides-access` — grants entry to something otherwise out of reach.
|
|
36
|
+
|
|
37
|
+
## Life-changing
|
|
38
|
+
|
|
39
|
+
- `provides-hope` — reason to believe things can improve.
|
|
40
|
+
- `self-actualization` — helps the user become who they want to be.
|
|
41
|
+
- `motivation` — energizes the user toward their goals.
|
|
42
|
+
- `heirloom` — something worth passing on across time.
|
|
43
|
+
- `affiliation-belonging` — makes the user part of a group that matters.
|
|
44
|
+
|
|
45
|
+
## Social impact
|
|
46
|
+
|
|
47
|
+
- `self-transcendence` — helps beyond the user themselves.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Arkaik ProjectBundle Validator — GENERATED, DO NOT EDIT BY HAND.
|
|
4
|
+
* Built via `npm run generate` from packages/schema/src (the canonical zod
|
|
5
|
+
* definitions, docs/spec/toolchain.md § @arkaik/schema). Zero dependencies —
|
|
6
|
+
* runnable with nothing but Node.
|
|
7
|
+
*
|
|
8
|
+
* Validates a bundle.json file against the Arkaik schema rules.
|
|
9
|
+
* Exit code 0 = valid, 1 = errors found.
|
|
10
|
+
*
|
|
11
|
+
* Usage: node validate-bundle.js <path-to-bundle.json>
|
|
12
|
+
*/
|
|
13
|
+
"use strict";var P=require("node:fs"),B=require("node:path");var G=["flow","view","data-model","api-endpoint","acceptance"],C=["idea","backlog","prioritized","development","releasing","live","archived","blocked"],q=["web","ios","android"],K=["composes","calls","displays","queries","covers"];var Q=["saves-time","simplifies","makes-money","reduces-risk","organizes","integrates","connects","reduces-effort","avoids-hassles","reduces-cost","quality","variety","sensory-appeal","informs","reduces-anxiety","rewards-me","nostalgia","design-aesthetics","badge-value","wellness","therapeutic-value","fun-entertainment","attractiveness","provides-access","provides-hope","self-actualization","motivation","heirloom","affiliation-belonging","self-transcendence"];var Z={flow:"F-",view:"V-","data-model":"DM-","api-endpoint":"API-",acceptance:"AC-"};function de(f){let a=e=>typeof e=="string"?e:"";return[...f].sort((e,r)=>{let $=a(e.ts),v=a(r.ts);if($<v)return-1;if($>v)return 1;let p=a(e.id),y=a(r.id);return p<y?-1:p>y?1:0})}function ee(f){let a=[],e=[];return f.split(/\r?\n/).forEach(($,v)=>{let p=v+1;if($.trim()==="")return;let y;try{y=JSON.parse($)}catch(h){e.push({line:p,rule:"journal-line-parse",message:`Line ${p}: not valid JSON \u2014 ${h.message}`,severity:"error"});return}if(typeof y!="object"||y===null||Array.isArray(y)){e.push({line:p,rule:"journal-line-shape",message:`Line ${p}: each journal line must be a single JSON event object.`,severity:"error"});return}let _=y,E=[];if(typeof _.id!="string"&&E.push("id"),typeof _.ts!="string"&&E.push("ts"),typeof _.type!="string"&&E.push("type"),E.length>0){e.push({line:p,rule:"journal-line-shape",message:`Line ${p}: event is missing required envelope field(s): ${E.join(", ")}.`,severity:"error"});return}a.push(_)}),{events:a,findings:e}}var ce={"node.updated":["node_id"],"node.status_changed":["node_id"],"node.deleted":["node_id"],"edge.added":["source_id","target_id"],"ref.added":["node_id"],"ref.removed":["node_id"],"ref.status_changed":["node_id"],"idea.proposed":["node_id"],"request.filed":["node_id"]};function te(f){let a=[],e=f.journal;if(e===void 0)return a;if(!Array.isArray(e))return a.push({path:"journal",rule:"journal-shape",message:"journal must be an array of events when present.",severity:"error"}),a;if(e.length===0)return a;let r=o=>typeof o=="string"?o:void 0,$=Array.isArray(f.nodes)?f.nodes:[],v=Array.isArray(f.edges)?f.edges:[],p=new Map;for(let o of $){let d=r(o?.id);d!==void 0&&p.set(d,o.status)}let y=new Set;for(let o of v){let d=r(o?.id);d!==void 0&&y.add(d)}let _=[];e.forEach((o,d)=>{let w=`journal[${d}]`;if(typeof o!="object"||o===null||Array.isArray(o)){a.push({path:w,rule:"journal-event-shape",message:`journal[${d}]: each event must be a JSON object.`,severity:"error"});return}let S=o,j=[];if(r(S.id)===void 0&&j.push("id"),r(S.ts)===void 0&&j.push("ts"),r(S.type)===void 0&&j.push("type"),j.length>0){a.push({path:w,rule:"journal-event-envelope",message:`journal[${d}]: event is missing required envelope field(s): ${j.join(", ")}.`,severity:"error"});return}_.push({ev:S,index:d})});let E=new Set(p.keys()),h=new Set(y);for(let{ev:o}of _)if(o.type==="node.created"){let d=r(o.node_id);d&&E.add(d)}else if(o.type==="edge.added"){let d=r(o.edge_id);d&&h.add(d)}let R=de(_.map(o=>o.ev)),I=new Set,b=new Map;for(let o of R)if(o.type==="node.created"){let d=r(o.node_id);d&&I.add(d)}else if(o.type==="node.status_changed"){let d=r(o.node_id);if(d&&o.platform===void 0){let w=r(o.to);w!==void 0&&b.set(d,w)}}for(let{ev:o,index:d}of _){let w=ce[o.type];if(w)for(let S of w){let j=r(o[S]);j!==void 0&&!E.has(j)&&a.push({path:`journal[${d}].${S}`,rule:"journal-dangling-node-ref",message:`journal[${d}] (${o.type}): references node "${j}" that never existed in the snapshot or journal.`,severity:"error"})}if(o.type==="edge.removed"){let S=r(o.edge_id);S!==void 0&&!h.has(S)&&a.push({path:`journal[${d}].edge_id`,rule:"journal-dangling-edge-ref",message:`journal[${d}] (edge.removed): references edge "${S}" that never existed in the snapshot or journal.`,severity:"error"})}}for(let[o,d]of p){I.has(o)||a.push({path:"journal",rule:"journal-missing-node-created",message:`Node "${o}" is present in the snapshot but has no node.created event in the journal.`,severity:"error"});let w=b.get(o);w!==void 0&&w!==d&&a.push({path:"journal",rule:"journal-status-mismatch",message:`Node "${o}": journal's last node.status_changed.to "${w}" disagrees with snapshot status "${String(d)}".`,severity:"error"})}return a}var le=["journey","system"];function ne(f){return le.includes(f)}var pe=["beta","monitoring","deprecated"],se=["compact","large"],ie=2*1024*1024;function fe(f){let a=f.indexOf(",");if(a===-1)return 0;let e=f.slice(a+1),r=e.endsWith("==")?2:e.endsWith("=")?1:0;return Math.floor(e.length*3/4)-r}var ue={composes:[["flow","view"],["flow","flow"],["view","flow"],["view","view"]],calls:[["view","api-endpoint"],["flow","api-endpoint"],["api-endpoint","api-endpoint"]],displays:[["view","data-model"]],queries:[["api-endpoint","data-model"]],covers:[["acceptance","view"],["acceptance","flow"]]};function oe(f){return typeof f=="string"&&!Number.isNaN(Date.parse(f))}function re(f){let a=[],e=(s,u,n)=>a.push({path:s,rule:u,message:n,severity:"error"}),r=(s,u,n)=>a.push({path:s,rule:u,message:n,severity:"warning"}),$=()=>{let s=a.filter(n=>n.severity==="error"),u=a.filter(n=>n.severity==="warning");return{valid:s.length===0,findings:a,errors:s,warnings:u}};if(typeof f!="object"||f===null||Array.isArray(f))return e("","bundle-shape","Bundle must be an object with project, nodes, and edges."),$();let v=f,p=v.project,y=v.nodes,_=v.edges;if(!p||!y||!_)return e("","bundle-shape","Missing top-level keys (project, nodes, edges)."),$();if(!Array.isArray(y))return e("nodes","bundle-shape","nodes must be an array."),$();if(!Array.isArray(_))return e("edges","bundle-shape","edges must be an array."),$();let E=y,h=_,R=new Map,I=new Set;p.id||e("project.id","project-id-required","project.id is missing"),(typeof p.title!="string"||!p.title.trim())&&e("project.title","title-non-empty","project.title is missing or empty"),p.created_at||e("project.created_at","timestamp-required","project.created_at is missing"),p.updated_at||e("project.updated_at","timestamp-required","project.updated_at is missing"),p.created_at&&!oe(p.created_at)&&r("project.created_at","iso-timestamp","project.created_at is not a valid ISO 8601 timestamp"),p.updated_at&&!oe(p.updated_at)&&r("project.updated_at","iso-timestamp","project.updated_at is not a valid ISO 8601 timestamp");let b=p.metadata;b&&b.view_card_variant!==void 0&&!se.includes(b.view_card_variant)&&e("project.metadata.view_card_variant","view-card-variant",`project.metadata.view_card_variant must be one of ${se.join(", ")} (import rejects other values)`),E.forEach((s,u)=>{let n=`nodes[${u}]`,t=s.id;I.has(t)&&e(`${n}.id`,"duplicate-node-id",`Duplicate node ID: ${t}`),I.add(t),R.set(t,s),(typeof s.title!="string"||!s.title.trim())&&e(`${n}.title`,"title-non-empty",`Node ${t}: title is missing or empty`);let c=s.species,i=Z[c];i&&!(typeof t=="string"&&t.startsWith(i))&&e(`${n}.id`,"species-prefix",`Node ${t}: species "${c}" should have prefix "${i}"`),G.includes(c)||e(`${n}.species`,"valid-species",`Node ${t}: invalid species "${c}"`),s.project_id!==p.id&&e(`${n}.project_id`,"project-id-match",`Node ${t}: project_id "${s.project_id}" does not match project.id "${p.id}"`),C.includes(s.status)||e(`${n}.status`,"valid-status",`Node ${t}: invalid status "${s.status}"`);let m=s.platforms;if(!m||m.length===0)e(`${n}.platforms`,"platforms-non-empty",`Node ${t}: platforms array is empty or missing`);else for(let g of m)q.includes(g)||e(`${n}.platforms`,"valid-platform",`Node ${t}: invalid platform "${g}"`);let l=s.metadata||{};l.stage!==void 0&&!pe.includes(l.stage)&&e(`${n}.metadata.stage`,"valid-stage",`Node ${t}: invalid metadata.stage "${l.stage}"`);let x=l.platformNotes;if(x)for(let g of Object.keys(x))q.includes(g)||e(`${n}.metadata.platformNotes`,"valid-platform",`Node ${t}: platformNotes has invalid platform "${g}"`);let N=l.platformStatuses;if(N)for(let[g,A]of Object.entries(N))q.includes(g)?Array.isArray(m)&&!m.includes(g)&&r(`${n}.metadata.platformStatuses`,"platform-statuses-subset",`Node ${t}: platformStatuses covers platform "${g}" not in node.platforms`):e(`${n}.metadata.platformStatuses`,"valid-platform",`Node ${t}: platformStatuses has invalid platform "${g}"`),C.includes(A)||e(`${n}.metadata.platformStatuses`,"valid-status",`Node ${t}: platformStatuses has invalid status "${A}" for platform "${g}"`);let D=l.platformScreenshots;if(D){for(let[g,A]of Object.entries(D))if(typeof A=="string"&&A.startsWith("data:")){let O=fe(A);O>ie&&r(`${n}.metadata.platformScreenshots`,"screenshot-data-uri-size",`Node ${t}: platformScreenshots.${g} is a ${(O/(1024*1024)).toFixed(1)}MB data URI, above the ${ie/(1024*1024)}MB guidance (docs/spec/bundle-format.md \xA7 Asset Values) \u2014 consider a relative path or hosted URL instead`)}}let T=l.gherkin,M=l.values;if(c==="acceptance"?((typeof T!="string"||!T.trim())&&r(`${n}.metadata.gherkin`,"acceptance-gherkin-missing",`Acceptance ${t}: metadata.gherkin is missing or empty (one Given/When/Then scenario expected)`),(!Array.isArray(M)||M.length===0)&&r(`${n}.metadata.values`,"acceptance-values-missing",`Acceptance ${t}: metadata.values is missing or empty (assign 1-3 value elements)`)):(T!==void 0&&r(`${n}.metadata.gherkin`,"gherkin-species",`Node ${t}: metadata.gherkin is only meaningful on acceptance nodes`),M!==void 0&&r(`${n}.metadata.values`,"values-species",`Node ${t}: metadata.values is only meaningful on acceptance nodes`)),Array.isArray(M))for(let g of M)Q.includes(g)||e(`${n}.metadata.values`,"valid-value",`Node ${t}: invalid value element "${g}"`);let V=l.refs;if(Array.isArray(V)){let g=new Set;V.forEach((A,O)=>{let J=A??{},U=`${n}.metadata.refs[${O}]`,k=J.id;typeof k!="string"||!k.trim()?e(`${U}.id`,"ref-id-required",`Node ${t}: ref is missing an id`):g.has(k)?e(`${U}.id`,"duplicate-ref-id",`Node ${t}: duplicate ref id "${k}"`):g.add(k),(typeof J.url!="string"||!J.url.trim())&&e(`${U}.url`,"ref-url-required",`Node ${t}: ref "${k}" is missing a url`),J.status_mapped!==void 0&&!C.includes(J.status_mapped)&&e(`${U}.status_mapped`,"valid-status",`Node ${t}: ref "${k}" has invalid status_mapped "${J.status_mapped}"`)})}if(c==="flow"){let g=l.playlist;!s.metadata||!g||!g.entries?e(`${n}.metadata.playlist`,"flow-playlist-required",`Flow ${t}: missing metadata.playlist.entries`):g.entries.length===0&&r(`${n}.metadata.playlist`,"flow-playlist-empty",`Flow ${t}: playlist is empty`)}});let o=p.root_node_id;o&&!I.has(o)&&e("project.root_node_id","root-node-exists",`project.root_node_id "${o}" does not reference an existing node`);let d=b?.maps;if(Array.isArray(d)){let s=new Set;d.forEach((u,n)=>{if(typeof u!="object"||u===null||Array.isArray(u))return;let t=u,c=`project.metadata.maps[${n}]`,i=typeof t.id=="string"?t.id:void 0;i!==void 0&&(s.has(i)&&r(`${c}.id`,"map-duplicate-id",`Duplicate map id "${i}"`),s.add(i),ne(i)&&r(`${c}.id`,"map-shadows-built-in",`Map id "${i}" shadows a built-in map and will be ignored`));let m=t.root_node_id;typeof m=="string"&&!I.has(m)&&r(`${c}.root_node_id`,"map-unknown-root",`Map "${i??n}" anchors on "${m}" which does not reference an existing node`),Array.isArray(t.species)&&t.species.forEach((l,x)=>{typeof l=="string"&&!G.includes(l)&&r(`${c}.species[${x}]`,"map-unknown-species",`Map "${i??n}" filters on unknown species "${l}"`)}),Array.isArray(t.edge_types)&&t.edge_types.forEach((l,x)=>{typeof l=="string"&&!K.includes(l)&&r(`${c}.edge_types[${x}]`,"map-unknown-edge-type",`Map "${i??n}" filters on unknown edge type "${l}"`)})})}let w=new Set,S=new Set,j=new Set;h.forEach((s,u)=>{let n=`edges[${u}]`,t=s.id,c=s.source_id,i=s.target_id,m=s.edge_type;w.has(t)&&e(`${n}.id`,"duplicate-edge-id",`Duplicate edge ID: ${t}`),w.add(t);let l=`e-${c}-${i}`;t!==l&&r(`${n}.id`,"edge-id-convention",`Edge ${t}: id does not match convention "${l}" (stale after a rename?)`);let x=`${c}->${i}:${m}`;S.has(x)&&r(`${n}`,"duplicate-edge-relationship",`Duplicate edge relationship: ${x}`),S.add(x),s.project_id!==p.id&&e(`${n}.project_id`,"project-id-match",`Edge ${t}: project_id does not match project.id`),I.has(c)||e(`${n}.source_id`,"dangling-edge",`Edge ${t}: source_id "${c}" not found in nodes`),I.has(i)||e(`${n}.target_id`,"dangling-edge",`Edge ${t}: target_id "${i}" not found in nodes`),K.includes(m)||e(`${n}.edge_type`,"valid-edge-type",`Edge ${t}: invalid edge_type "${m}"`);let N=R.get(c),D=R.get(i),T=ue[m];N&&D&&T&&(T.some(([V,g])=>V===N.species&&g===D.species)||e(`${n}.edge_type`,"edge-semantics",`Edge ${t}: "${m}" not valid from ${N.species} to ${D.species}`)),m==="composes"&&j.add(`${c}->${i}`)});let F=(s,u,n,t=0)=>{if(t>50)return e(n,"playlist-depth",`Flow ${u}: playlist nesting too deep (possible cycle)`),[];let c=[];for(let i of s)if(i.type==="view")I.has(i.view_id)||e(n,"playlist-ref-exists",`Flow ${u}: playlist references non-existent view "${i.view_id}"`),c.push(i.view_id);else if(i.type==="flow")I.has(i.flow_id)||e(n,"playlist-ref-exists",`Flow ${u}: playlist references non-existent flow "${i.flow_id}"`),i.flow_id===u&&e(n,"playlist-self-cycle",`Flow ${u}: playlist contains itself (direct cycle)`),c.push(i.flow_id);else if(i.type==="condition")i.if_true&&c.push(...F(i.if_true,u,n,t+1)),i.if_false&&c.push(...F(i.if_false,u,n,t+1));else if(i.type==="junction"&&i.cases)for(let m of i.cases)c.push(...F(m.entries||[],u,n,t+1));return c};E.forEach((s,u)=>{let n=s.metadata;if(s.species==="flow"&&n?.playlist?.entries){let t=`nodes[${u}].metadata.playlist`,c=s.id,i=F(n.playlist.entries,c,t);for(let m of i)j.has(`${c}->${m}`)||e(t,"playlist-composes-coherence",`Flow ${c}: playlist references "${m}" but no composes edge exists`)}});let W=new Map,H=new Map;E.forEach((s,u)=>{let n=s.metadata;if(s.species==="flow"&&n?.playlist?.entries){let t=s.id,c=[],i=m=>{for(let l of m)if(l.type==="flow"&&c.push(l.flow_id),l.type==="condition"&&(l.if_true&&i(l.if_true),l.if_false&&i(l.if_false)),l.type==="junction"&&l.cases)for(let x of l.cases)i(x.entries||[])};i(n.playlist.entries),W.set(t,c),H.set(t,`nodes[${u}].metadata.playlist`)}});let Y=new Set,z=new Set,X=s=>{if(z.has(s))return!0;if(Y.has(s))return!1;Y.add(s),z.add(s);for(let u of W.get(s)||[])if(X(u))return e(H.get(s)??"","flow-cycle",`Cycle detected: flow "${s}" -> "${u}"`),!0;return z.delete(s),!1};for(let s of W.keys())X(s);for(let s of te(v))a.push(s);return $()}var ae="journal.jsonl";function L(f,a){return f.filter(e=>e?.species===a).length}function ge(){let f=process.argv[2];f||(console.error("Usage: node validate-bundle.js <path-to-bundle.json>"),process.exit(1)),(0,P.existsSync)(f)||(console.error(`File not found: ${f}`),process.exit(1));let a;try{a=JSON.parse((0,P.readFileSync)(f,"utf8"))}catch(h){console.error(`FATAL: Cannot parse JSON \u2014 ${h.message}`),process.exit(1)}let e=a;(typeof e!="object"||e===null||!e.project||!e.nodes||!e.edges)&&(console.error("FATAL: Missing top-level keys (project, nodes, edges)."),process.exit(1));let r=[],$=!1;if(e.journal===void 0){let h=(0,B.join)((0,B.dirname)(f),ae);if((0,P.existsSync)(h)){let{events:R,findings:I}=ee((0,P.readFileSync)(h,"utf8"));r=I,$=!0,e.journal=R}}let v=Array.isArray(e.nodes)?e.nodes:[],p=Array.isArray(e.edges)?e.edges:[],y=Array.isArray(e.journal)?e.journal:[],_=re(a);console.log(`
|
|
14
|
+
Arkaik Bundle Validation`),console.log(` =======================
|
|
15
|
+
`),console.log(` Nodes: ${v.length} (${L(v,"view")} views, ${L(v,"flow")} flows, ${L(v,"data-model")} data-models, ${L(v,"api-endpoint")} api-endpoints, ${L(v,"acceptance")} acceptances)`),console.log(` Edges: ${p.length}`),$?console.log(` Journal: ${y.length} event(s) from ${ae} sidecar`):y.length>0&&console.log(` Journal: ${y.length} embedded event(s)`),console.log(""),_.warnings.length>0&&(console.log(` Warnings: ${_.warnings.length}`),_.warnings.forEach(h=>console.log(` WARN: ${h.message}`)),console.log(""));let E=[...r.map(h=>h.message),..._.errors.map(h=>h.message)];E.length===0&&(console.log(` Result: VALID
|
|
16
|
+
`),process.exit(0)),console.log(` Errors: ${E.length}`),E.forEach(h=>console.log(` ERROR: ${h}`)),console.log(`
|
|
17
|
+
Result: INVALID
|
|
18
|
+
`),process.exit(1)}ge();
|