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,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();