arkaik 0.1.0 → 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/io.js CHANGED
@@ -6,33 +6,6 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/lib/bundle-io.ts
8
8
  import { existsSync, readFileSync } from "node:fs";
9
- function readBundle(filePath) {
10
- if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
11
- let parsed;
12
- try {
13
- parsed = JSON.parse(readFileSync(filePath, "utf8"));
14
- } catch (e) {
15
- throw new Error(`Cannot parse JSON \u2014 ${e.message}`);
16
- }
17
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
18
- throw new Error("Bundle must be a JSON object.");
19
- }
20
- return parsed;
21
- }
22
- function nodesByIdOf(bundle) {
23
- const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
24
- const map2 = /* @__PURE__ */ new Map();
25
- for (const n of nodes) {
26
- if (n !== null && typeof n === "object" && typeof n.id === "string") {
27
- map2.set(n.id, n);
28
- }
29
- }
30
- return map2;
31
- }
32
-
33
- // src/lib/journal-io.ts
34
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
35
- import { dirname, join } from "node:path";
36
9
 
37
10
  // ../../node_modules/zod/v4/classic/external.js
38
11
  var external_exports = {};
@@ -14549,19 +14522,58 @@ function date4(params) {
14549
14522
  config(en_default());
14550
14523
 
14551
14524
  // ../schema/src/ids.ts
14552
- var SPECIES_IDS = ["flow", "view", "data-model", "api-endpoint", "acceptance"];
14525
+ var SPECIES_IDS = ["flow", "view", "data-model", "api-endpoint", "acceptance", "decision"];
14553
14526
  var STATUS_IDS = [
14554
14527
  "idea",
14528
+ "discovery",
14555
14529
  "backlog",
14556
- "prioritized",
14557
14530
  "development",
14558
14531
  "releasing",
14559
14532
  "live",
14560
- "archived",
14561
- "blocked"
14533
+ "archived"
14562
14534
  ];
14563
14535
  var PLATFORM_IDS = ["web", "ios", "android"];
14564
- var EDGE_TYPE_IDS = ["composes", "calls", "displays", "queries", "covers"];
14536
+ var EDGE_TYPE_IDS = ["composes", "calls", "displays", "queries", "covers", "supersedes", "generates", "impacts"];
14537
+ var VALID_EDGE_SEMANTICS = {
14538
+ composes: [
14539
+ ["flow", "view"],
14540
+ ["flow", "flow"],
14541
+ ["view", "flow"],
14542
+ ["view", "view"]
14543
+ ],
14544
+ // `calls` names the initiator, which is why it runs in both directions
14545
+ // between a view and an endpoint: view → api is the outbound/write
14546
+ // affordance, api → view the inbound/read one a server opens itself (a
14547
+ // webhook, an SSE channel, a push). Both project onto the View card
14548
+ // (docs/graph-model.md § Edge Types). Consumers that walk `calls` must
14549
+ // therefore not assume it points down into the system layer — see
14550
+ // `buildProductUsageIndex` in ./products.ts, which restricts hops by
14551
+ // target species for exactly this reason.
14552
+ calls: [
14553
+ ["view", "api-endpoint"],
14554
+ ["flow", "api-endpoint"],
14555
+ ["api-endpoint", "api-endpoint"],
14556
+ ["api-endpoint", "view"]
14557
+ ],
14558
+ displays: [["view", "data-model"]],
14559
+ queries: [["api-endpoint", "data-model"]],
14560
+ covers: [
14561
+ ["acceptance", "view"],
14562
+ ["acceptance", "flow"]
14563
+ ],
14564
+ // Decision edges (cycle 2). `generates` and `impacts` are deliberately
14565
+ // disjoint: an acceptance is *generated* by a decision, never merely
14566
+ // impacted, so `impacts` does not admit an acceptance target
14567
+ // (docs/superpowers/specs/2026-08-03-decisions-species-design.md §3).
14568
+ supersedes: [["decision", "decision"]],
14569
+ generates: [["decision", "acceptance"]],
14570
+ impacts: [
14571
+ ["decision", "flow"],
14572
+ ["decision", "view"],
14573
+ ["decision", "data-model"],
14574
+ ["decision", "api-endpoint"]
14575
+ ]
14576
+ };
14565
14577
  var VALUE_TIER_IDS = ["functional", "emotional", "life-changing", "social-impact"];
14566
14578
  var VALUE_IDS = [
14567
14579
  // functional (14)
@@ -14600,6 +14612,106 @@ var VALUE_IDS = [
14600
14612
  "self-transcendence"
14601
14613
  ];
14602
14614
 
14615
+ // ../schema/src/legacy-status.ts
14616
+ var LEGACY_STATUS_IDS = ["prioritized", "blocked"];
14617
+ var LEGACY_STATUS_ALIASES = {
14618
+ prioritized: "backlog",
14619
+ blocked: "development"
14620
+ };
14621
+ var STATUS_VOCABULARY_VERSION = 3;
14622
+ var BLOCKED_BY_MIGRATION_NOTE = "migrated from legacy blocked status";
14623
+ function normalizeStatus(value) {
14624
+ if (STATUS_IDS.includes(value)) return value;
14625
+ return LEGACY_STATUS_ALIASES[value];
14626
+ }
14627
+ var LEGACY_ALIAS_REMAP = { ...LEGACY_STATUS_ALIASES };
14628
+ var LEGACY_VOCABULARY_REMAP = {
14629
+ backlog: "idea",
14630
+ ...LEGACY_STATUS_ALIASES
14631
+ };
14632
+ function migrateNode(node, remap) {
14633
+ let changed = false;
14634
+ let wasBlocked = node.status === "blocked";
14635
+ const mappedStatus = remap[node.status];
14636
+ const status = mappedStatus ?? node.status;
14637
+ if (mappedStatus !== void 0) changed = true;
14638
+ let metadata = node.metadata;
14639
+ const platformStatuses = node.metadata?.platformStatuses;
14640
+ if (platformStatuses !== void 0) {
14641
+ let anyMapped = false;
14642
+ const next = {};
14643
+ for (const [platform, value] of Object.entries(platformStatuses)) {
14644
+ if (value === "blocked") wasBlocked = true;
14645
+ const mapped = remap[value];
14646
+ next[platform] = mapped ?? value;
14647
+ if (mapped !== void 0) anyMapped = true;
14648
+ }
14649
+ if (anyMapped) {
14650
+ metadata = { ...metadata, platformStatuses: next };
14651
+ changed = true;
14652
+ }
14653
+ }
14654
+ const refs = node.metadata?.refs;
14655
+ if (Array.isArray(refs)) {
14656
+ let anyMapped = false;
14657
+ const nextRefs = refs.map((ref) => {
14658
+ if (ref.status_mapped === void 0) return ref;
14659
+ const mapped = remap[ref.status_mapped];
14660
+ if (mapped === void 0) return ref;
14661
+ anyMapped = true;
14662
+ return { ...ref, status_mapped: mapped };
14663
+ });
14664
+ if (anyMapped) {
14665
+ metadata = { ...metadata, refs: nextRefs };
14666
+ changed = true;
14667
+ }
14668
+ }
14669
+ if (wasBlocked && !metadata?.blocked_by) {
14670
+ metadata = { ...metadata, blocked_by: BLOCKED_BY_MIGRATION_NOTE };
14671
+ changed = true;
14672
+ }
14673
+ if (!changed) return node;
14674
+ return { ...node, status, ...metadata !== void 0 ? { metadata } : {} };
14675
+ }
14676
+ function migrateStatusVocabulary(bundle) {
14677
+ const current = typeof bundle.schema_version === "number" && bundle.schema_version >= STATUS_VOCABULARY_VERSION;
14678
+ const remap = current ? LEGACY_ALIAS_REMAP : LEGACY_VOCABULARY_REMAP;
14679
+ const nodes = bundle.nodes.map((node) => migrateNode(node, remap));
14680
+ if (current) {
14681
+ const untouched = nodes.every((node, i) => node === bundle.nodes[i]);
14682
+ return untouched ? bundle : { ...bundle, nodes };
14683
+ }
14684
+ return { ...bundle, schema_version: STATUS_VOCABULARY_VERSION, nodes };
14685
+ }
14686
+
14687
+ // ../schema/src/decision.ts
14688
+ var DECISION_STATUS_IDS = [
14689
+ "proposed",
14690
+ "approved",
14691
+ "enacted",
14692
+ "rejected",
14693
+ "deprecated",
14694
+ "superseded"
14695
+ ];
14696
+ function lifecycleStatusForDecision(decisionStatus) {
14697
+ switch (decisionStatus) {
14698
+ case "proposed":
14699
+ return "discovery";
14700
+ case "approved":
14701
+ return "backlog";
14702
+ case "enacted":
14703
+ return "live";
14704
+ case "rejected":
14705
+ case "deprecated":
14706
+ case "superseded":
14707
+ return "archived";
14708
+ }
14709
+ }
14710
+ function decisionStatusOf(node) {
14711
+ const raw = node.metadata?.decision_status;
14712
+ return DECISION_STATUS_IDS.includes(raw) ? raw : "proposed";
14713
+ }
14714
+
14603
14715
  // ../schema/src/enums.ts
14604
14716
  var SpeciesSchema = external_exports.enum(SPECIES_IDS).meta({
14605
14717
  id: "Species",
@@ -14609,6 +14721,10 @@ var StatusSchema = external_exports.enum(STATUS_IDS).meta({
14609
14721
  id: "Status",
14610
14722
  description: "Lifecycle status of a node."
14611
14723
  });
14724
+ var AnyStatusSchema = external_exports.enum([...STATUS_IDS, ...LEGACY_STATUS_IDS]).meta({
14725
+ id: "AnyStatus",
14726
+ description: "Lifecycle status as stored: the current vocabulary, or a legacy id (prioritized, blocked) accepted from pre-v3 bundles and migrated on load."
14727
+ });
14612
14728
  var PlatformSchema = external_exports.enum(PLATFORM_IDS).meta({
14613
14729
  id: "Platform",
14614
14730
  description: "Target platform."
@@ -14625,6 +14741,10 @@ var ValueSchema = external_exports.enum(VALUE_IDS).meta({
14625
14741
  id: "Value",
14626
14742
  description: "A Bain B2C Elements-of-Value element served by an acceptance (spec \xA73.2)."
14627
14743
  });
14744
+ var DecisionStatusSchema = external_exports.enum(DECISION_STATUS_IDS).meta({
14745
+ id: "DecisionStatus",
14746
+ description: "Decision nodes only: proposed \u2192 approved (agreed, not yet reality) \u2192 enacted (in effect); terminal: rejected, deprecated, superseded. Not a lifecycle status \u2014 the node's status field is kept in sync (proposed\u2192discovery, approved\u2192backlog, enacted\u2192live, terminals\u2192archived)."
14747
+ });
14628
14748
 
14629
14749
  // ../schema/src/id-gen.ts
14630
14750
  var SPECIES_PREFIXES = {
@@ -14632,7 +14752,8 @@ var SPECIES_PREFIXES = {
14632
14752
  view: "V-",
14633
14753
  "data-model": "DM-",
14634
14754
  "api-endpoint": "API-",
14635
- acceptance: "AC-"
14755
+ acceptance: "AC-",
14756
+ decision: "DEC-"
14636
14757
  };
14637
14758
 
14638
14759
  // ../schema/src/playlist.ts
@@ -14730,6 +14851,7 @@ function parseJournalLines(text) {
14730
14851
  var NODE_REF_FIELDS = {
14731
14852
  "node.updated": ["node_id"],
14732
14853
  "node.status_changed": ["node_id"],
14854
+ "decision.status_changed": ["node_id"],
14733
14855
  "node.deleted": ["node_id"],
14734
14856
  "edge.added": ["source_id", "target_id"],
14735
14857
  "ref.added": ["node_id"],
@@ -14738,6 +14860,12 @@ var NODE_REF_FIELDS = {
14738
14860
  "idea.proposed": ["node_id"],
14739
14861
  "request.filed": ["node_id"]
14740
14862
  };
14863
+ function statusesAgree(last, snapshot) {
14864
+ if (last === snapshot) return true;
14865
+ if (typeof snapshot !== "string") return false;
14866
+ if (last === "backlog" && snapshot === "idea") return true;
14867
+ return (normalizeStatus(last) ?? last) === (normalizeStatus(snapshot) ?? snapshot);
14868
+ }
14741
14869
  function crossCheckJournal(bundle) {
14742
14870
  const findings = [];
14743
14871
  const journalRaw = bundle.journal;
@@ -14756,9 +14884,15 @@ function crossCheckJournal(bundle) {
14756
14884
  const nodesRaw = Array.isArray(bundle.nodes) ? bundle.nodes : [];
14757
14885
  const edgesRaw = Array.isArray(bundle.edges) ? bundle.edges : [];
14758
14886
  const snapshotNodeStatus = /* @__PURE__ */ new Map();
14887
+ const snapshotDecisionStatus = /* @__PURE__ */ new Map();
14759
14888
  for (const n of nodesRaw) {
14760
14889
  const id = str(n?.id);
14761
- if (id !== void 0) snapshotNodeStatus.set(id, n.status);
14890
+ if (id !== void 0) {
14891
+ snapshotNodeStatus.set(id, n.status);
14892
+ const metadata = n.metadata;
14893
+ const decisionStatus = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata.decision_status : void 0;
14894
+ snapshotDecisionStatus.set(id, decisionStatus ?? "proposed");
14895
+ }
14762
14896
  }
14763
14897
  const snapshotEdgeIds = /* @__PURE__ */ new Set();
14764
14898
  for (const e of edgesRaw) {
@@ -14807,6 +14941,7 @@ function crossCheckJournal(bundle) {
14807
14941
  const ordered = orderEvents(valid.map((v) => v.ev));
14808
14942
  const created = /* @__PURE__ */ new Set();
14809
14943
  const lastProjectStatus = /* @__PURE__ */ new Map();
14944
+ const lastDecisionStatus = /* @__PURE__ */ new Map();
14810
14945
  for (const ev of ordered) {
14811
14946
  if (ev.type === "node.created") {
14812
14947
  const nid = str(ev.node_id);
@@ -14817,6 +14952,12 @@ function crossCheckJournal(bundle) {
14817
14952
  const to = str(ev.to);
14818
14953
  if (to !== void 0) lastProjectStatus.set(nid, to);
14819
14954
  }
14955
+ } else if (ev.type === "decision.status_changed") {
14956
+ const nid = str(ev.node_id);
14957
+ if (nid) {
14958
+ const to = str(ev.to);
14959
+ if (to !== void 0) lastDecisionStatus.set(nid, to);
14960
+ }
14820
14961
  }
14821
14962
  }
14822
14963
  for (const { ev, index } of valid) {
@@ -14834,6 +14975,19 @@ function crossCheckJournal(bundle) {
14834
14975
  }
14835
14976
  }
14836
14977
  }
14978
+ if (ev.type === "deliverable.shipped" && Array.isArray(ev.node_ids)) {
14979
+ ev.node_ids.forEach((raw, i) => {
14980
+ const ref = str(raw);
14981
+ if (ref !== void 0 && !everNodes.has(ref)) {
14982
+ findings.push({
14983
+ path: `journal[${index}].node_ids[${i}]`,
14984
+ rule: "journal-dangling-node-ref",
14985
+ message: `journal[${index}] (${ev.type}): references node "${ref}" that never existed in the snapshot or journal.`,
14986
+ severity: "error"
14987
+ });
14988
+ }
14989
+ });
14990
+ }
14837
14991
  if (ev.type === "edge.removed") {
14838
14992
  const ref = str(ev.edge_id);
14839
14993
  if (ref !== void 0 && !everEdges.has(ref)) {
@@ -14856,7 +15010,7 @@ function crossCheckJournal(bundle) {
14856
15010
  });
14857
15011
  }
14858
15012
  const last = lastProjectStatus.get(nodeId);
14859
- if (last !== void 0 && last !== status) {
15013
+ if (last !== void 0 && !statusesAgree(last, status)) {
14860
15014
  findings.push({
14861
15015
  path: "journal",
14862
15016
  rule: "journal-status-mismatch",
@@ -14865,6 +15019,18 @@ function crossCheckJournal(bundle) {
14865
15019
  });
14866
15020
  }
14867
15021
  }
15022
+ for (const [nodeId, last] of lastDecisionStatus) {
15023
+ if (!snapshotDecisionStatus.has(nodeId)) continue;
15024
+ const current = snapshotDecisionStatus.get(nodeId) ?? "proposed";
15025
+ if (last !== current) {
15026
+ findings.push({
15027
+ path: "journal",
15028
+ rule: "journal-decision-status-mismatch",
15029
+ message: `Node "${nodeId}": journal's last decision.status_changed.to "${last}" disagrees with snapshot decision_status "${String(current)}".`,
15030
+ severity: "error"
15031
+ });
15032
+ }
15033
+ }
14868
15034
  return findings;
14869
15035
  }
14870
15036
 
@@ -14895,10 +15061,19 @@ var NodeStatusChangedEventSchema = external_exports.object({
14895
15061
  ...envelope,
14896
15062
  type: external_exports.literal("node.status_changed"),
14897
15063
  node_id: external_exports.string(),
14898
- from: StatusSchema,
14899
- to: StatusSchema,
15064
+ // History is never rewritten (docs/spec/journal.md): pre-v3 events keep
15065
+ // their legacy status ids, so strict per-type validation must accept them.
15066
+ from: AnyStatusSchema,
15067
+ to: AnyStatusSchema,
14900
15068
  platform: PlatformSchema.optional()
14901
15069
  }).catchall(external_exports.unknown());
15070
+ var DecisionStatusChangedEventSchema = external_exports.object({
15071
+ ...envelope,
15072
+ type: external_exports.literal("decision.status_changed"),
15073
+ node_id: external_exports.string(),
15074
+ from: DecisionStatusSchema,
15075
+ to: DecisionStatusSchema
15076
+ }).catchall(external_exports.unknown());
14902
15077
  var NodeDeletedEventSchema = external_exports.object({ ...envelope, type: external_exports.literal("node.deleted"), node_id: external_exports.string() }).catchall(external_exports.unknown());
14903
15078
  var EdgeAddedEventSchema = external_exports.object({
14904
15079
  ...envelope,
@@ -14916,6 +15091,16 @@ var ReleaseTaggedEventSchema = external_exports.object({
14916
15091
  notes: external_exports.string().optional(),
14917
15092
  platform: PlatformSchema.optional()
14918
15093
  }).catchall(external_exports.unknown());
15094
+ var DeliverableShippedEventSchema = external_exports.object({
15095
+ ...envelope,
15096
+ type: external_exports.literal("deliverable.shipped"),
15097
+ deliverable_id: external_exports.string(),
15098
+ title: external_exports.string(),
15099
+ summary: external_exports.string().optional(),
15100
+ url: external_exports.string().optional(),
15101
+ node_ids: external_exports.array(external_exports.string()).optional(),
15102
+ platform: PlatformSchema.optional()
15103
+ }).catchall(external_exports.unknown());
14919
15104
  var IdeaProposedEventSchema = external_exports.object({
14920
15105
  ...envelope,
14921
15106
  type: external_exports.literal("idea.proposed"),
@@ -14953,10 +15138,12 @@ var KnownJournalEventSchema = external_exports.union([
14953
15138
  NodeCreatedEventSchema,
14954
15139
  NodeUpdatedEventSchema,
14955
15140
  NodeStatusChangedEventSchema,
15141
+ DecisionStatusChangedEventSchema,
14956
15142
  NodeDeletedEventSchema,
14957
15143
  EdgeAddedEventSchema,
14958
15144
  EdgeRemovedEventSchema,
14959
15145
  ReleaseTaggedEventSchema,
15146
+ DeliverableShippedEventSchema,
14960
15147
  IdeaProposedEventSchema,
14961
15148
  RequestFiledEventSchema,
14962
15149
  RefAddedEventSchema,
@@ -14967,7 +15154,8 @@ var KnownJournalEventSchema = external_exports.union([
14967
15154
  // ../schema/src/bundle.ts
14968
15155
  var PlatformStatusMapSchema = external_exports.partialRecord(
14969
15156
  PlatformSchema,
14970
- StatusSchema
15157
+ // legacy-tolerant: migrateStatusVocabulary normalizes on load
15158
+ AnyStatusSchema
14971
15159
  ).meta({ id: "PlatformStatusMap", description: "Per-platform status overrides for view nodes." });
14972
15160
  var PlatformNotesMapSchema = external_exports.partialRecord(
14973
15161
  PlatformSchema,
@@ -14990,7 +15178,8 @@ var RefSchema = external_exports.object({
14990
15178
  external_status: external_exports.string().optional().meta({
14991
15179
  description: 'Mirrored external state, verbatim (e.g. "open", "merged", "In Progress").'
14992
15180
  }),
14993
- status_mapped: StatusSchema.optional().meta({
15181
+ // legacy-tolerant: migrateStatusVocabulary normalizes on load
15182
+ status_mapped: AnyStatusSchema.optional().meta({
14994
15183
  description: "Optional mapping of external_status into the arkaik lifecycle. Advisory display data \u2014 never mutates node.status."
14995
15184
  }),
14996
15185
  platform: PlatformSchema.optional().meta({ description: "Optional scoping to one platform variant." }),
@@ -14998,6 +15187,9 @@ var RefSchema = external_exports.object({
14998
15187
  }).meta({ id: "Ref", description: "A typed external reference on a node (docs/spec/bundle-format.md \xA7 References)." });
14999
15188
  var NodeMetadataSchema = external_exports.object({
15000
15189
  stage: external_exports.string().optional(),
15190
+ blocked_by: external_exports.string().optional().meta({
15191
+ description: "Non-empty = blocked at the current status. A node id (rendered as a link) or free text naming the dependency."
15192
+ }),
15001
15193
  playlist: FlowPlaylistSchema.optional(),
15002
15194
  platformNotes: PlatformNotesMapSchema.optional(),
15003
15195
  platformStatuses: PlatformStatusMapSchema.optional(),
@@ -15008,6 +15200,21 @@ var NodeMetadataSchema = external_exports.object({
15008
15200
  }),
15009
15201
  values: external_exports.array(ValueSchema).optional().meta({
15010
15202
  description: "Acceptance nodes only: 1..n Bain value elements served (the Why)."
15203
+ }),
15204
+ product: external_exports.string().optional().meta({
15205
+ description: "Product membership (docs/spec/bundle-format.md \xA7 Products); flow, view, and acceptance only."
15206
+ }),
15207
+ decision_status: DecisionStatusSchema.optional().meta({
15208
+ description: "Decision nodes only: proposed | approved | enacted | rejected | deprecated | superseded. The node's lifecycle status is kept in sync (spec \xA72)."
15209
+ }),
15210
+ context: external_exports.string().optional().meta({
15211
+ description: "Decision nodes only: Context \u2014 the Why (markdown)."
15212
+ }),
15213
+ consequences: external_exports.string().optional().meta({
15214
+ description: "Decision nodes only: Consequences \u2014 the How (markdown)."
15215
+ }),
15216
+ decided_at: external_exports.string().optional().meta({
15217
+ description: "Decision nodes only: ISO 8601 date the decision was made."
15011
15218
  })
15012
15219
  }).catchall(external_exports.unknown()).meta({ id: "NodeMetadata", description: "Optional metadata for a node." });
15013
15220
  var NodeSchema = external_exports.object({
@@ -15016,7 +15223,8 @@ var NodeSchema = external_exports.object({
15016
15223
  species: SpeciesSchema,
15017
15224
  title: external_exports.string().meta({ description: "Human-readable node title." }),
15018
15225
  description: external_exports.string().optional().meta({ description: "Optional description of the node's purpose." }),
15019
- status: StatusSchema,
15226
+ // legacy-tolerant: migrateStatusVocabulary normalizes on load
15227
+ status: AnyStatusSchema,
15020
15228
  platforms: external_exports.array(PlatformSchema).meta({ description: "One or more target platforms." }),
15021
15229
  metadata: NodeMetadataSchema.optional()
15022
15230
  }).meta({ id: "Node" });
@@ -15028,6 +15236,18 @@ var EdgeSchema = external_exports.object({
15028
15236
  edge_type: EdgeTypeSchema,
15029
15237
  metadata: external_exports.record(external_exports.string(), external_exports.unknown()).optional().meta({ description: "Optional edge metadata." })
15030
15238
  }).meta({ id: "Edge" });
15239
+ var MapDisplayOptionsSchema = external_exports.object({
15240
+ images: external_exports.boolean().optional().meta({ description: "Screenshot (or cover) art on view cards." }),
15241
+ flow_platforms: external_exports.string().optional().meta({
15242
+ description: "A flow card's platform delivery: rings (default) | bars."
15243
+ }),
15244
+ view_platforms: external_exports.string().optional().meta({
15245
+ description: "A view card's platform availability: chips (default) | rows."
15246
+ }),
15247
+ minimap_color: external_exports.string().optional().meta({
15248
+ description: "What a minimap node's fill encodes: status (default) | species."
15249
+ })
15250
+ }).catchall(external_exports.unknown()).meta({ id: "MapDisplayOptions", description: "How a map draws its cards (docs/spec/maps.md \xA7 Display Options)." });
15031
15251
  var MapDefinitionSchema = external_exports.object({
15032
15252
  id: external_exports.string().meta({
15033
15253
  description: "Kebab-case, unique within the project; built-in ids (journey, system) are reserved."
@@ -15046,13 +15266,32 @@ var MapDefinitionSchema = external_exports.object({
15046
15266
  root_node_id: external_exports.string().optional().meta({
15047
15267
  description: "Scope anchor: the subgraph is the undirected neighborhood reachable from this node."
15048
15268
  }),
15269
+ product: external_exports.string().optional().meta({ description: "Product scope; absent = every product." }),
15049
15270
  depth: external_exports.number().optional().meta({ description: "Traversal bound from the root; absent = unbounded." }),
15050
- layout: external_exports.object({ direction: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Renderer layout hints (e.g. direction: DOWN | RIGHT)." })
15271
+ layout: external_exports.object({ direction: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Renderer layout hints (e.g. direction: DOWN | RIGHT)." }),
15272
+ display: MapDisplayOptionsSchema.optional().meta({
15273
+ description: "Card rendering; the human twin is project.metadata.map_display[id]."
15274
+ })
15051
15275
  }).catchall(external_exports.unknown()).meta({ id: "MapDefinition", description: "A stored map definition (docs/spec/maps.md \xA7 MapDefinition)." });
15276
+ var ProductDefinitionSchema = external_exports.object({
15277
+ id: external_exports.string().meta({ description: "Kebab-case, unique within the project." }),
15278
+ title: external_exports.string().meta({ description: "Display title." }),
15279
+ description: external_exports.string().optional().meta({ description: "What this product is." }),
15280
+ platforms: external_exports.array(PlatformSchema).meta({
15281
+ description: "The platforms this product can ship on; empty means availability is not tracked."
15282
+ }),
15283
+ root_node_id: external_exports.string().optional().meta({ description: "This product's journey anchor." })
15284
+ }).catchall(external_exports.unknown()).meta({ id: "ProductDefinition", description: "A product definition (docs/spec/bundle-format.md \xA7 Products)." });
15052
15285
  var ProjectMetadataSchema = external_exports.object({
15053
15286
  view_card_variant: external_exports.enum(["compact", "large"]).optional(),
15054
15287
  maps: external_exports.array(MapDefinitionSchema).optional().meta({
15055
15288
  description: "Stored map definitions (docs/spec/maps.md \xA7 Storage) \u2014 additive; unknown fields preserved."
15289
+ }),
15290
+ map_display: external_exports.record(external_exports.string(), MapDisplayOptionsSchema).optional().meta({
15291
+ description: "Per-map display overrides keyed by map id (docs/spec/maps.md \xA7 Display Options) \u2014 the only path open to the built-in maps."
15292
+ }),
15293
+ products: external_exports.array(ProductDefinitionSchema).optional().meta({
15294
+ description: "Product definitions (docs/spec/bundle-format.md \xA7 Products) \u2014 additive; unknown fields preserved."
15056
15295
  })
15057
15296
  }).catchall(external_exports.unknown()).meta({ id: "ProjectMetadata", description: "Optional project-level UI settings." });
15058
15297
  var ProjectSchema = external_exports.object({
@@ -15089,11 +15328,31 @@ var ProjectBundleSchema = external_exports.object({
15089
15328
  });
15090
15329
 
15091
15330
  // ../schema/src/maps.ts
15331
+ var MAP_FLOW_PLATFORMS_MODES = ["rings", "bars"];
15332
+ var MAP_VIEW_PLATFORMS_MODES = ["chips", "rows"];
15333
+ var MAP_MINIMAP_COLOR_MODES = ["status", "species"];
15092
15334
  var BUILT_IN_MAP_IDS = ["journey", "system"];
15093
15335
  function isBuiltInMapId(id) {
15094
15336
  return BUILT_IN_MAP_IDS.includes(id);
15095
15337
  }
15096
15338
 
15339
+ // ../schema/src/products.ts
15340
+ var PRODUCT_MEMBERSHIP_SPECIES = ["flow", "view", "acceptance"];
15341
+ function resolveProducts(project) {
15342
+ const stored = project?.metadata?.products;
15343
+ if (!Array.isArray(stored)) return [];
15344
+ const seen = /* @__PURE__ */ new Set();
15345
+ const products = [];
15346
+ for (const entry of stored) {
15347
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
15348
+ const candidate = entry;
15349
+ if (typeof candidate.id !== "string" || candidate.id.trim() === "" || seen.has(candidate.id)) continue;
15350
+ seen.add(candidate.id);
15351
+ products.push(candidate);
15352
+ }
15353
+ return products;
15354
+ }
15355
+
15097
15356
  // ../schema/src/validate.ts
15098
15357
  var VALID_STAGES = ["beta", "monitoring", "deprecated"];
15099
15358
  var VALID_VIEW_CARD_VARIANTS = ["compact", "large"];
@@ -15105,25 +15364,6 @@ function estimateDataUriBytes(dataUri) {
15105
15364
  const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
15106
15365
  return Math.floor(payload.length * 3 / 4) - padding;
15107
15366
  }
15108
- var VALID_EDGE_SEMANTICS = {
15109
- composes: [
15110
- ["flow", "view"],
15111
- ["flow", "flow"],
15112
- ["view", "flow"],
15113
- ["view", "view"]
15114
- ],
15115
- calls: [
15116
- ["view", "api-endpoint"],
15117
- ["flow", "api-endpoint"],
15118
- ["api-endpoint", "api-endpoint"]
15119
- ],
15120
- displays: [["view", "data-model"]],
15121
- queries: [["api-endpoint", "data-model"]],
15122
- covers: [
15123
- ["acceptance", "view"],
15124
- ["acceptance", "flow"]
15125
- ]
15126
- };
15127
15367
  function isIsoDate(value) {
15128
15368
  return typeof value === "string" && !Number.isNaN(Date.parse(value));
15129
15369
  }
@@ -15219,7 +15459,9 @@ function validateBundle(input) {
15219
15459
  }
15220
15460
  const platforms = node.platforms;
15221
15461
  if (!platforms || platforms.length === 0) {
15222
- error51(`${base}.platforms`, "platforms-non-empty", `Node ${nodeId}: platforms array is empty or missing`);
15462
+ if (species !== "decision") {
15463
+ error51(`${base}.platforms`, "platforms-non-empty", `Node ${nodeId}: platforms array is empty or missing`);
15464
+ }
15223
15465
  } else {
15224
15466
  for (const p of platforms) {
15225
15467
  if (!PLATFORM_IDS.includes(p)) {
@@ -15341,6 +15583,25 @@ function validateBundle(input) {
15341
15583
  }
15342
15584
  });
15343
15585
  }
15586
+ const decisionStatus = md.decision_status;
15587
+ if (decisionStatus !== void 0 && species !== "decision") {
15588
+ warn(
15589
+ `${base}.metadata.decision_status`,
15590
+ "decision-status-wrong-species",
15591
+ `decision_status is meaningful on decision nodes only; "${nodeId}" is a ${species}.`
15592
+ );
15593
+ }
15594
+ if (species === "decision") {
15595
+ const effective = decisionStatusOf({ metadata: node.metadata });
15596
+ const expected = lifecycleStatusForDecision(effective);
15597
+ if (node.status !== expected) {
15598
+ warn(
15599
+ `${base}.status`,
15600
+ "decision-lifecycle-mismatch",
15601
+ `Decision "${nodeId}" is ${effective}, whose lifecycle status should be "${expected}", but status is "${node.status}" (spec \xA72).`
15602
+ );
15603
+ }
15604
+ }
15344
15605
  if (species === "flow") {
15345
15606
  const playlist = md.playlist;
15346
15607
  if (!node.metadata || !playlist || !playlist.entries) {
@@ -15358,6 +15619,38 @@ function validateBundle(input) {
15358
15619
  `project.root_node_id "${rootNodeId}" does not reference an existing node`
15359
15620
  );
15360
15621
  }
15622
+ const checkMapDisplay = (display, path, subject) => {
15623
+ if (typeof display !== "object" || display === null || Array.isArray(display)) return;
15624
+ const options = display;
15625
+ const modes = [
15626
+ ["flow_platforms", MAP_FLOW_PLATFORMS_MODES],
15627
+ ["view_platforms", MAP_VIEW_PLATFORMS_MODES],
15628
+ ["minimap_color", MAP_MINIMAP_COLOR_MODES]
15629
+ ];
15630
+ for (const [key, allowed] of modes) {
15631
+ const value = options[key];
15632
+ if (value !== void 0 && (typeof value !== "string" || !allowed.includes(value))) {
15633
+ warn(
15634
+ `${path}.${key}`,
15635
+ "map-unknown-display",
15636
+ `${subject} sets ${key} to "${String(value)}"; expected one of ${allowed.join(", ")} (renderers fall back to the default)`
15637
+ );
15638
+ }
15639
+ }
15640
+ if (options.images !== void 0 && typeof options.images !== "boolean") {
15641
+ warn(
15642
+ `${path}.images`,
15643
+ "map-unknown-display",
15644
+ `${subject} sets images to a non-boolean value (renderers fall back to the default)`
15645
+ );
15646
+ }
15647
+ };
15648
+ const mapDisplayOverrides = projectMetadata?.map_display;
15649
+ if (typeof mapDisplayOverrides === "object" && mapDisplayOverrides !== null && !Array.isArray(mapDisplayOverrides)) {
15650
+ for (const [mapId, display] of Object.entries(mapDisplayOverrides)) {
15651
+ checkMapDisplay(display, `project.metadata.map_display.${mapId}`, `Map "${mapId}"`);
15652
+ }
15653
+ }
15361
15654
  const storedMaps = projectMetadata?.maps;
15362
15655
  if (Array.isArray(storedMaps)) {
15363
15656
  const seenMapIds = /* @__PURE__ */ new Set();
@@ -15405,8 +15698,122 @@ function validateBundle(input) {
15405
15698
  }
15406
15699
  });
15407
15700
  }
15701
+ checkMapDisplay(map2.display, `${path}.display`, `Map "${mapId ?? index}"`);
15408
15702
  });
15409
15703
  }
15704
+ const storedProducts = projectMetadata?.products;
15705
+ const declaredProductIds = /* @__PURE__ */ new Set();
15706
+ if (Array.isArray(storedProducts)) {
15707
+ const seenProductIds = /* @__PURE__ */ new Set();
15708
+ storedProducts.forEach((definition, index) => {
15709
+ if (typeof definition !== "object" || definition === null || Array.isArray(definition)) return;
15710
+ const product = definition;
15711
+ const path = `project.metadata.products[${index}]`;
15712
+ const productId = typeof product.id === "string" ? product.id : void 0;
15713
+ if (productId === void 0) return;
15714
+ if (seenProductIds.has(productId)) {
15715
+ warn(`${path}.id`, "product-duplicate-id", `Duplicate product id "${productId}" \u2014 the first wins`);
15716
+ } else {
15717
+ seenProductIds.add(productId);
15718
+ if (productId.trim() !== "") declaredProductIds.add(productId);
15719
+ }
15720
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(productId)) {
15721
+ warn(`${path}.id`, "product-invalid-id", `Product id "${productId}" is not kebab-case`);
15722
+ }
15723
+ });
15724
+ }
15725
+ const hasProducts = declaredProductIds.size > 0;
15726
+ const anchorsByAcceptance = /* @__PURE__ */ new Map();
15727
+ for (const edge of edges) {
15728
+ if (edge.edge_type !== "covers") continue;
15729
+ const source = typeof edge.source_id === "string" ? edge.source_id : void 0;
15730
+ const target = typeof edge.target_id === "string" ? edge.target_id : void 0;
15731
+ if (source === void 0 || target === void 0) continue;
15732
+ const list = anchorsByAcceptance.get(source) ?? [];
15733
+ list.push(target);
15734
+ anchorsByAcceptance.set(source, list);
15735
+ }
15736
+ const menuByProduct = /* @__PURE__ */ new Map();
15737
+ if (hasProducts) {
15738
+ for (const definition of resolveProducts({ metadata: projectMetadata })) {
15739
+ menuByProduct.set(
15740
+ definition.id,
15741
+ new Set(Array.isArray(definition.platforms) ? definition.platforms : [])
15742
+ );
15743
+ }
15744
+ }
15745
+ const productByNodeId = /* @__PURE__ */ new Map();
15746
+ const indexByNodeId = /* @__PURE__ */ new Map();
15747
+ nodes.forEach((node, index) => {
15748
+ const nodeId = typeof node.id === "string" ? node.id : `#${index}`;
15749
+ const species = node.species;
15750
+ const base = `nodes[${index}]`;
15751
+ indexByNodeId.set(nodeId, index);
15752
+ const metadata = node.metadata ?? {};
15753
+ const membership = typeof metadata.product === "string" ? metadata.product : void 0;
15754
+ const storesMembership = species !== void 0 && PRODUCT_MEMBERSHIP_SPECIES.includes(species);
15755
+ if (membership !== void 0 && !storesMembership) {
15756
+ const detail = SPECIES_IDS.includes(species) ? `${species} membership is derived from consumers and must not be stored` : "metadata.product is only meaningful on flow, view, and acceptance nodes";
15757
+ warn(`${base}.metadata.product`, "product-membership-wrong-species", `Node ${nodeId}: ${detail}`);
15758
+ return;
15759
+ }
15760
+ if (!storesMembership) return;
15761
+ if (membership === void 0) {
15762
+ if (!hasProducts) return;
15763
+ if (species === "acceptance") {
15764
+ if (!anchorsByAcceptance.has(nodeId)) {
15765
+ warn(
15766
+ `${base}.metadata.product`,
15767
+ "acceptance-product-unassigned",
15768
+ `Acceptance ${nodeId} covers nothing and names no product \u2014 it will show only under "All products"`
15769
+ );
15770
+ }
15771
+ } else {
15772
+ warn(
15773
+ `${base}.metadata.product`,
15774
+ "unassigned-membership",
15775
+ `Node ${nodeId}: no product membership \u2014 it will show only under "All products"`
15776
+ );
15777
+ }
15778
+ return;
15779
+ }
15780
+ productByNodeId.set(nodeId, membership);
15781
+ if (!declaredProductIds.has(membership)) {
15782
+ warn(
15783
+ `${base}.metadata.product`,
15784
+ "product-unknown-reference",
15785
+ `Node ${nodeId}: product "${membership}" is not declared on the project`
15786
+ );
15787
+ return;
15788
+ }
15789
+ const menu = menuByProduct.get(membership);
15790
+ const nodePlatforms = Array.isArray(node.platforms) ? node.platforms : [];
15791
+ if (menu) {
15792
+ for (const platform of nodePlatforms) {
15793
+ if (typeof platform === "string" && !menu.has(platform)) {
15794
+ warn(
15795
+ `${base}.platforms`,
15796
+ "product-platform-not-in-menu",
15797
+ `Node ${nodeId}: platform "${platform}" is not in product "${membership}"'s menu`
15798
+ );
15799
+ }
15800
+ }
15801
+ }
15802
+ });
15803
+ if (hasProducts) {
15804
+ for (const [acceptanceId, anchors] of anchorsByAcceptance) {
15805
+ const acceptanceIndex = indexByNodeId.get(acceptanceId);
15806
+ if (acceptanceIndex === void 0) continue;
15807
+ const spanned = new Set(anchors.map((id) => productByNodeId.get(id)).filter((id) => Boolean(id)));
15808
+ if (spanned.size > 1) {
15809
+ warn(
15810
+ `nodes[${acceptanceIndex}].metadata.product`,
15811
+ "acceptance-covers-span-products",
15812
+ `Acceptance ${acceptanceId} covers anchors in ${[...spanned].sort().join(" and ")} \u2014 statuses may conflate products`
15813
+ );
15814
+ }
15815
+ }
15816
+ }
15410
15817
  const edgeIds = /* @__PURE__ */ new Set();
15411
15818
  const edgeSignatures = /* @__PURE__ */ new Set();
15412
15819
  const composesSet = /* @__PURE__ */ new Set();
@@ -15563,7 +15970,36 @@ function validateBundle(input) {
15563
15970
  var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
15564
15971
  var ENCODING_LEN = ENCODING.length;
15565
15972
 
15973
+ // src/lib/bundle-io.ts
15974
+ function readBundle(filePath) {
15975
+ if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
15976
+ let parsed;
15977
+ try {
15978
+ parsed = JSON.parse(readFileSync(filePath, "utf8"));
15979
+ } catch (e) {
15980
+ throw new Error(`Cannot parse JSON \u2014 ${e.message}`);
15981
+ }
15982
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
15983
+ throw new Error("Bundle must be a JSON object.");
15984
+ }
15985
+ const record2 = parsed;
15986
+ if (!Array.isArray(record2.nodes)) return record2;
15987
+ return migrateStatusVocabulary(record2);
15988
+ }
15989
+ function nodesByIdOf(bundle) {
15990
+ const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
15991
+ const map2 = /* @__PURE__ */ new Map();
15992
+ for (const n of nodes) {
15993
+ if (n !== null && typeof n === "object" && typeof n.id === "string") {
15994
+ map2.set(n.id, n);
15995
+ }
15996
+ }
15997
+ return map2;
15998
+ }
15999
+
15566
16000
  // src/lib/journal-io.ts
16001
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
16002
+ import { dirname, join } from "node:path";
15567
16003
  var JOURNAL_SIDECAR = "journal.jsonl";
15568
16004
  function journalPathFor(bundlePath) {
15569
16005
  return join(dirname(bundlePath), JOURNAL_SIDECAR);