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/index.js CHANGED
@@ -783,10 +783,10 @@ function mergeDefs(...defs) {
783
783
  function cloneDef(schema) {
784
784
  return mergeDefs(schema._zod.def);
785
785
  }
786
- function getElementAtPath(obj, path) {
787
- if (!path)
786
+ function getElementAtPath(obj, path6) {
787
+ if (!path6)
788
788
  return obj;
789
- return path.reduce((acc, key) => acc?.[key], obj);
789
+ return path6.reduce((acc, key) => acc?.[key], obj);
790
790
  }
791
791
  function promiseAllObject(promisesObj) {
792
792
  const keys = Object.keys(promisesObj);
@@ -1195,11 +1195,11 @@ function explicitlyAborted(x, startIndex = 0) {
1195
1195
  }
1196
1196
  return false;
1197
1197
  }
1198
- function prefixIssues(path, issues) {
1198
+ function prefixIssues(path6, issues) {
1199
1199
  return issues.map((iss) => {
1200
1200
  var _a3;
1201
1201
  (_a3 = iss).path ?? (_a3.path = []);
1202
- iss.path.unshift(path);
1202
+ iss.path.unshift(path6);
1203
1203
  return iss;
1204
1204
  });
1205
1205
  }
@@ -1346,16 +1346,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
1346
1346
  }
1347
1347
  function formatError(error51, mapper = (issue2) => issue2.message) {
1348
1348
  const fieldErrors = { _errors: [] };
1349
- const processError = (error52, path = []) => {
1349
+ const processError = (error52, path6 = []) => {
1350
1350
  for (const issue2 of error52.issues) {
1351
1351
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1352
- issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
1352
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
1353
1353
  } else if (issue2.code === "invalid_key") {
1354
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1354
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
1355
1355
  } else if (issue2.code === "invalid_element") {
1356
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1356
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
1357
1357
  } else {
1358
- const fullpath = [...path, ...issue2.path];
1358
+ const fullpath = [...path6, ...issue2.path];
1359
1359
  if (fullpath.length === 0) {
1360
1360
  fieldErrors._errors.push(mapper(issue2));
1361
1361
  } else {
@@ -1382,17 +1382,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
1382
1382
  }
1383
1383
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
1384
1384
  const result = { errors: [] };
1385
- const processError = (error52, path = []) => {
1385
+ const processError = (error52, path6 = []) => {
1386
1386
  var _a3, _b;
1387
1387
  for (const issue2 of error52.issues) {
1388
1388
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1389
- issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
1389
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
1390
1390
  } else if (issue2.code === "invalid_key") {
1391
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1391
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
1392
1392
  } else if (issue2.code === "invalid_element") {
1393
- processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1393
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
1394
1394
  } else {
1395
- const fullpath = [...path, ...issue2.path];
1395
+ const fullpath = [...path6, ...issue2.path];
1396
1396
  if (fullpath.length === 0) {
1397
1397
  result.errors.push(mapper(issue2));
1398
1398
  continue;
@@ -1424,8 +1424,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
1424
1424
  }
1425
1425
  function toDotPath(_path) {
1426
1426
  const segs = [];
1427
- const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1428
- for (const seg of path) {
1427
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1428
+ for (const seg of path6) {
1429
1429
  if (typeof seg === "number")
1430
1430
  segs.push(`[${seg}]`);
1431
1431
  else if (typeof seg === "symbol")
@@ -4761,8 +4761,8 @@ var error3 = () => {
4761
4761
  const sizing = getSizing(issue2.origin);
4762
4762
  if (sizing) {
4763
4763
  const maxValue = Number(issue2.maximum);
4764
- const unit = getBelarusianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
4765
- return `\u0417\u0430\u043D\u0430\u0434\u0442\u0430 \u0432\u044F\u043B\u0456\u043A\u0456: \u0447\u0430\u043A\u0430\u043B\u0430\u0441\u044F, \u0448\u0442\u043E ${issue2.origin ?? "\u0437\u043D\u0430\u0447\u044D\u043D\u043D\u0435"} \u043F\u0430\u0432\u0456\u043D\u043D\u0430 ${sizing.verb} ${adj}${issue2.maximum.toString()} ${unit}`;
4764
+ const unit2 = getBelarusianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
4765
+ return `\u0417\u0430\u043D\u0430\u0434\u0442\u0430 \u0432\u044F\u043B\u0456\u043A\u0456: \u0447\u0430\u043A\u0430\u043B\u0430\u0441\u044F, \u0448\u0442\u043E ${issue2.origin ?? "\u0437\u043D\u0430\u0447\u044D\u043D\u043D\u0435"} \u043F\u0430\u0432\u0456\u043D\u043D\u0430 ${sizing.verb} ${adj}${issue2.maximum.toString()} ${unit2}`;
4766
4766
  }
4767
4767
  return `\u0417\u0430\u043D\u0430\u0434\u0442\u0430 \u0432\u044F\u043B\u0456\u043A\u0456: \u0447\u0430\u043A\u0430\u043B\u0430\u0441\u044F, \u0448\u0442\u043E ${issue2.origin ?? "\u0437\u043D\u0430\u0447\u044D\u043D\u043D\u0435"} \u043F\u0430\u0432\u0456\u043D\u043D\u0430 \u0431\u044B\u0446\u044C ${adj}${issue2.maximum.toString()}`;
4768
4768
  }
@@ -4771,8 +4771,8 @@ var error3 = () => {
4771
4771
  const sizing = getSizing(issue2.origin);
4772
4772
  if (sizing) {
4773
4773
  const minValue = Number(issue2.minimum);
4774
- const unit = getBelarusianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
4775
- return `\u0417\u0430\u043D\u0430\u0434\u0442\u0430 \u043C\u0430\u043B\u044B: \u0447\u0430\u043A\u0430\u043B\u0430\u0441\u044F, \u0448\u0442\u043E ${issue2.origin} \u043F\u0430\u0432\u0456\u043D\u043D\u0430 ${sizing.verb} ${adj}${issue2.minimum.toString()} ${unit}`;
4774
+ const unit2 = getBelarusianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
4775
+ return `\u0417\u0430\u043D\u0430\u0434\u0442\u0430 \u043C\u0430\u043B\u044B: \u0447\u0430\u043A\u0430\u043B\u0430\u0441\u044F, \u0448\u0442\u043E ${issue2.origin} \u043F\u0430\u0432\u0456\u043D\u043D\u0430 ${sizing.verb} ${adj}${issue2.minimum.toString()} ${unit2}`;
4776
4776
  }
4777
4777
  return `\u0417\u0430\u043D\u0430\u0434\u0442\u0430 \u043C\u0430\u043B\u044B: \u0447\u0430\u043A\u0430\u043B\u0430\u0441\u044F, \u0448\u0442\u043E ${issue2.origin} \u043F\u0430\u0432\u0456\u043D\u043D\u0430 \u0431\u044B\u0446\u044C ${adj}${issue2.minimum.toString()}`;
4778
4778
  }
@@ -6831,8 +6831,8 @@ var error20 = () => {
6831
6831
  const sizing = getSizing(issue2.origin);
6832
6832
  if (sizing) {
6833
6833
  const maxValue = Number(issue2.maximum);
6834
- const unit = getArmenianPlural(maxValue, sizing.unit.one, sizing.unit.many);
6835
- return `\u0549\u0561\u0583\u0561\u0566\u0561\u0576\u0581 \u0574\u0565\u056E \u0561\u0580\u056A\u0565\u0584\u2024 \u057D\u057A\u0561\u057D\u057E\u0578\u0582\u0574 \u0567, \u0578\u0580 ${withDefiniteArticle(issue2.origin ?? "\u0561\u0580\u056A\u0565\u0584")} \u056F\u0578\u0582\u0576\u0565\u0576\u0561 ${adj}${issue2.maximum.toString()} ${unit}`;
6834
+ const unit2 = getArmenianPlural(maxValue, sizing.unit.one, sizing.unit.many);
6835
+ return `\u0549\u0561\u0583\u0561\u0566\u0561\u0576\u0581 \u0574\u0565\u056E \u0561\u0580\u056A\u0565\u0584\u2024 \u057D\u057A\u0561\u057D\u057E\u0578\u0582\u0574 \u0567, \u0578\u0580 ${withDefiniteArticle(issue2.origin ?? "\u0561\u0580\u056A\u0565\u0584")} \u056F\u0578\u0582\u0576\u0565\u0576\u0561 ${adj}${issue2.maximum.toString()} ${unit2}`;
6836
6836
  }
6837
6837
  return `\u0549\u0561\u0583\u0561\u0566\u0561\u0576\u0581 \u0574\u0565\u056E \u0561\u0580\u056A\u0565\u0584\u2024 \u057D\u057A\u0561\u057D\u057E\u0578\u0582\u0574 \u0567, \u0578\u0580 ${withDefiniteArticle(issue2.origin ?? "\u0561\u0580\u056A\u0565\u0584")} \u056C\u056B\u0576\u056B ${adj}${issue2.maximum.toString()}`;
6838
6838
  }
@@ -6841,8 +6841,8 @@ var error20 = () => {
6841
6841
  const sizing = getSizing(issue2.origin);
6842
6842
  if (sizing) {
6843
6843
  const minValue = Number(issue2.minimum);
6844
- const unit = getArmenianPlural(minValue, sizing.unit.one, sizing.unit.many);
6845
- return `\u0549\u0561\u0583\u0561\u0566\u0561\u0576\u0581 \u0583\u0578\u0584\u0580 \u0561\u0580\u056A\u0565\u0584\u2024 \u057D\u057A\u0561\u057D\u057E\u0578\u0582\u0574 \u0567, \u0578\u0580 ${withDefiniteArticle(issue2.origin)} \u056F\u0578\u0582\u0576\u0565\u0576\u0561 ${adj}${issue2.minimum.toString()} ${unit}`;
6844
+ const unit2 = getArmenianPlural(minValue, sizing.unit.one, sizing.unit.many);
6845
+ return `\u0549\u0561\u0583\u0561\u0566\u0561\u0576\u0581 \u0583\u0578\u0584\u0580 \u0561\u0580\u056A\u0565\u0584\u2024 \u057D\u057A\u0561\u057D\u057E\u0578\u0582\u0574 \u0567, \u0578\u0580 ${withDefiniteArticle(issue2.origin)} \u056F\u0578\u0582\u0576\u0565\u0576\u0561 ${adj}${issue2.minimum.toString()} ${unit2}`;
6846
6846
  }
6847
6847
  return `\u0549\u0561\u0583\u0561\u0566\u0561\u0576\u0581 \u0583\u0578\u0584\u0580 \u0561\u0580\u056A\u0565\u0584\u2024 \u057D\u057A\u0561\u057D\u057E\u0578\u0582\u0574 \u0567, \u0578\u0580 ${withDefiniteArticle(issue2.origin)} \u056C\u056B\u0576\u056B ${adj}${issue2.minimum.toString()}`;
6848
6848
  }
@@ -7605,18 +7605,18 @@ var error27 = () => {
7605
7605
  const adj = issue2.inclusive ? "\uC774\uD558" : "\uBBF8\uB9CC";
7606
7606
  const suffix = adj === "\uBBF8\uB9CC" ? "\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" : "\uC5EC\uC57C \uD569\uB2C8\uB2E4";
7607
7607
  const sizing = getSizing(issue2.origin);
7608
- const unit = sizing?.unit ?? "\uC694\uC18C";
7608
+ const unit2 = sizing?.unit ?? "\uC694\uC18C";
7609
7609
  if (sizing)
7610
- return `${issue2.origin ?? "\uAC12"}\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${issue2.maximum.toString()}${unit} ${adj}${suffix}`;
7610
+ return `${issue2.origin ?? "\uAC12"}\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${issue2.maximum.toString()}${unit2} ${adj}${suffix}`;
7611
7611
  return `${issue2.origin ?? "\uAC12"}\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${issue2.maximum.toString()} ${adj}${suffix}`;
7612
7612
  }
7613
7613
  case "too_small": {
7614
7614
  const adj = issue2.inclusive ? "\uC774\uC0C1" : "\uCD08\uACFC";
7615
7615
  const suffix = adj === "\uC774\uC0C1" ? "\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" : "\uC5EC\uC57C \uD569\uB2C8\uB2E4";
7616
7616
  const sizing = getSizing(issue2.origin);
7617
- const unit = sizing?.unit ?? "\uC694\uC18C";
7617
+ const unit2 = sizing?.unit ?? "\uC694\uC18C";
7618
7618
  if (sizing) {
7619
- return `${issue2.origin ?? "\uAC12"}\uC774 \uB108\uBB34 \uC791\uC2B5\uB2C8\uB2E4: ${issue2.minimum.toString()}${unit} ${adj}${suffix}`;
7619
+ return `${issue2.origin ?? "\uAC12"}\uC774 \uB108\uBB34 \uC791\uC2B5\uB2C8\uB2E4: ${issue2.minimum.toString()}${unit2} ${adj}${suffix}`;
7620
7620
  }
7621
7621
  return `${issue2.origin ?? "\uAC12"}\uC774 \uB108\uBB34 \uC791\uC2B5\uB2C8\uB2E4: ${issue2.minimum.toString()} ${adj}${suffix}`;
7622
7622
  }
@@ -8969,8 +8969,8 @@ var error38 = () => {
8969
8969
  const sizing = getSizing(issue2.origin);
8970
8970
  if (sizing) {
8971
8971
  const maxValue = Number(issue2.maximum);
8972
- const unit = getRussianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
8973
- return `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C, \u0447\u0442\u043E ${issue2.origin ?? "\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"} \u0431\u0443\u0434\u0435\u0442 \u0438\u043C\u0435\u0442\u044C ${adj}${issue2.maximum.toString()} ${unit}`;
8972
+ const unit2 = getRussianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
8973
+ return `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C, \u0447\u0442\u043E ${issue2.origin ?? "\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"} \u0431\u0443\u0434\u0435\u0442 \u0438\u043C\u0435\u0442\u044C ${adj}${issue2.maximum.toString()} ${unit2}`;
8974
8974
  }
8975
8975
  return `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C, \u0447\u0442\u043E ${issue2.origin ?? "\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"} \u0431\u0443\u0434\u0435\u0442 ${adj}${issue2.maximum.toString()}`;
8976
8976
  }
@@ -8979,8 +8979,8 @@ var error38 = () => {
8979
8979
  const sizing = getSizing(issue2.origin);
8980
8980
  if (sizing) {
8981
8981
  const minValue = Number(issue2.minimum);
8982
- const unit = getRussianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
8983
- return `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C, \u0447\u0442\u043E ${issue2.origin} \u0431\u0443\u0434\u0435\u0442 \u0438\u043C\u0435\u0442\u044C ${adj}${issue2.minimum.toString()} ${unit}`;
8982
+ const unit2 = getRussianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
8983
+ return `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C, \u0447\u0442\u043E ${issue2.origin} \u0431\u0443\u0434\u0435\u0442 \u0438\u043C\u0435\u0442\u044C ${adj}${issue2.minimum.toString()} ${unit2}`;
8984
8984
  }
8985
8985
  return `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u043E\u0441\u044C, \u0447\u0442\u043E ${issue2.origin} \u0431\u0443\u0434\u0435\u0442 ${adj}${issue2.minimum.toString()}`;
8986
8986
  }
@@ -14117,13 +14117,13 @@ function resolveRef(ref, ctx) {
14117
14117
  if (!ref.startsWith("#")) {
14118
14118
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14119
14119
  }
14120
- const path = ref.slice(1).split("/").filter(Boolean);
14121
- if (path.length === 0) {
14120
+ const path6 = ref.slice(1).split("/").filter(Boolean);
14121
+ if (path6.length === 0) {
14122
14122
  return ctx.rootSchema;
14123
14123
  }
14124
14124
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14125
- if (path[0] === defsKey) {
14126
- const key = path[1];
14125
+ if (path6[0] === defsKey) {
14126
+ const key = path6[1];
14127
14127
  if (!key || !ctx.defs[key]) {
14128
14128
  throw new Error(`Reference not found: ${ref}`);
14129
14129
  }
@@ -14532,19 +14532,58 @@ function date4(params) {
14532
14532
  config(en_default());
14533
14533
 
14534
14534
  // ../schema/src/ids.ts
14535
- var SPECIES_IDS = ["flow", "view", "data-model", "api-endpoint", "acceptance"];
14535
+ var SPECIES_IDS = ["flow", "view", "data-model", "api-endpoint", "acceptance", "decision"];
14536
14536
  var STATUS_IDS = [
14537
14537
  "idea",
14538
+ "discovery",
14538
14539
  "backlog",
14539
- "prioritized",
14540
14540
  "development",
14541
14541
  "releasing",
14542
14542
  "live",
14543
- "archived",
14544
- "blocked"
14543
+ "archived"
14545
14544
  ];
14546
14545
  var PLATFORM_IDS = ["web", "ios", "android"];
14547
- var EDGE_TYPE_IDS = ["composes", "calls", "displays", "queries", "covers"];
14546
+ var EDGE_TYPE_IDS = ["composes", "calls", "displays", "queries", "covers", "supersedes", "generates", "impacts"];
14547
+ var VALID_EDGE_SEMANTICS = {
14548
+ composes: [
14549
+ ["flow", "view"],
14550
+ ["flow", "flow"],
14551
+ ["view", "flow"],
14552
+ ["view", "view"]
14553
+ ],
14554
+ // `calls` names the initiator, which is why it runs in both directions
14555
+ // between a view and an endpoint: view → api is the outbound/write
14556
+ // affordance, api → view the inbound/read one a server opens itself (a
14557
+ // webhook, an SSE channel, a push). Both project onto the View card
14558
+ // (docs/graph-model.md § Edge Types). Consumers that walk `calls` must
14559
+ // therefore not assume it points down into the system layer — see
14560
+ // `buildProductUsageIndex` in ./products.ts, which restricts hops by
14561
+ // target species for exactly this reason.
14562
+ calls: [
14563
+ ["view", "api-endpoint"],
14564
+ ["flow", "api-endpoint"],
14565
+ ["api-endpoint", "api-endpoint"],
14566
+ ["api-endpoint", "view"]
14567
+ ],
14568
+ displays: [["view", "data-model"]],
14569
+ queries: [["api-endpoint", "data-model"]],
14570
+ covers: [
14571
+ ["acceptance", "view"],
14572
+ ["acceptance", "flow"]
14573
+ ],
14574
+ // Decision edges (cycle 2). `generates` and `impacts` are deliberately
14575
+ // disjoint: an acceptance is *generated* by a decision, never merely
14576
+ // impacted, so `impacts` does not admit an acceptance target
14577
+ // (docs/superpowers/specs/2026-08-03-decisions-species-design.md §3).
14578
+ supersedes: [["decision", "decision"]],
14579
+ generates: [["decision", "acceptance"]],
14580
+ impacts: [
14581
+ ["decision", "flow"],
14582
+ ["decision", "view"],
14583
+ ["decision", "data-model"],
14584
+ ["decision", "api-endpoint"]
14585
+ ]
14586
+ };
14548
14587
  var VALUE_TIER_IDS = ["functional", "emotional", "life-changing", "social-impact"];
14549
14588
  var VALUE_IDS = [
14550
14589
  // functional (14)
@@ -14583,6 +14622,106 @@ var VALUE_IDS = [
14583
14622
  "self-transcendence"
14584
14623
  ];
14585
14624
 
14625
+ // ../schema/src/legacy-status.ts
14626
+ var LEGACY_STATUS_IDS = ["prioritized", "blocked"];
14627
+ var LEGACY_STATUS_ALIASES = {
14628
+ prioritized: "backlog",
14629
+ blocked: "development"
14630
+ };
14631
+ var STATUS_VOCABULARY_VERSION = 3;
14632
+ var BLOCKED_BY_MIGRATION_NOTE = "migrated from legacy blocked status";
14633
+ function normalizeStatus(value) {
14634
+ if (STATUS_IDS.includes(value)) return value;
14635
+ return LEGACY_STATUS_ALIASES[value];
14636
+ }
14637
+ var LEGACY_ALIAS_REMAP = { ...LEGACY_STATUS_ALIASES };
14638
+ var LEGACY_VOCABULARY_REMAP = {
14639
+ backlog: "idea",
14640
+ ...LEGACY_STATUS_ALIASES
14641
+ };
14642
+ function migrateNode(node, remap) {
14643
+ let changed = false;
14644
+ let wasBlocked = node.status === "blocked";
14645
+ const mappedStatus = remap[node.status];
14646
+ const status = mappedStatus ?? node.status;
14647
+ if (mappedStatus !== void 0) changed = true;
14648
+ let metadata = node.metadata;
14649
+ const platformStatuses = node.metadata?.platformStatuses;
14650
+ if (platformStatuses !== void 0) {
14651
+ let anyMapped = false;
14652
+ const next = {};
14653
+ for (const [platform, value] of Object.entries(platformStatuses)) {
14654
+ if (value === "blocked") wasBlocked = true;
14655
+ const mapped = remap[value];
14656
+ next[platform] = mapped ?? value;
14657
+ if (mapped !== void 0) anyMapped = true;
14658
+ }
14659
+ if (anyMapped) {
14660
+ metadata = { ...metadata, platformStatuses: next };
14661
+ changed = true;
14662
+ }
14663
+ }
14664
+ const refs = node.metadata?.refs;
14665
+ if (Array.isArray(refs)) {
14666
+ let anyMapped = false;
14667
+ const nextRefs = refs.map((ref) => {
14668
+ if (ref.status_mapped === void 0) return ref;
14669
+ const mapped = remap[ref.status_mapped];
14670
+ if (mapped === void 0) return ref;
14671
+ anyMapped = true;
14672
+ return { ...ref, status_mapped: mapped };
14673
+ });
14674
+ if (anyMapped) {
14675
+ metadata = { ...metadata, refs: nextRefs };
14676
+ changed = true;
14677
+ }
14678
+ }
14679
+ if (wasBlocked && !metadata?.blocked_by) {
14680
+ metadata = { ...metadata, blocked_by: BLOCKED_BY_MIGRATION_NOTE };
14681
+ changed = true;
14682
+ }
14683
+ if (!changed) return node;
14684
+ return { ...node, status, ...metadata !== void 0 ? { metadata } : {} };
14685
+ }
14686
+ function migrateStatusVocabulary(bundle) {
14687
+ const current = typeof bundle.schema_version === "number" && bundle.schema_version >= STATUS_VOCABULARY_VERSION;
14688
+ const remap = current ? LEGACY_ALIAS_REMAP : LEGACY_VOCABULARY_REMAP;
14689
+ const nodes = bundle.nodes.map((node) => migrateNode(node, remap));
14690
+ if (current) {
14691
+ const untouched = nodes.every((node, i) => node === bundle.nodes[i]);
14692
+ return untouched ? bundle : { ...bundle, nodes };
14693
+ }
14694
+ return { ...bundle, schema_version: STATUS_VOCABULARY_VERSION, nodes };
14695
+ }
14696
+
14697
+ // ../schema/src/decision.ts
14698
+ var DECISION_STATUS_IDS = [
14699
+ "proposed",
14700
+ "approved",
14701
+ "enacted",
14702
+ "rejected",
14703
+ "deprecated",
14704
+ "superseded"
14705
+ ];
14706
+ function lifecycleStatusForDecision(decisionStatus) {
14707
+ switch (decisionStatus) {
14708
+ case "proposed":
14709
+ return "discovery";
14710
+ case "approved":
14711
+ return "backlog";
14712
+ case "enacted":
14713
+ return "live";
14714
+ case "rejected":
14715
+ case "deprecated":
14716
+ case "superseded":
14717
+ return "archived";
14718
+ }
14719
+ }
14720
+ function decisionStatusOf(node) {
14721
+ const raw = node.metadata?.decision_status;
14722
+ return DECISION_STATUS_IDS.includes(raw) ? raw : "proposed";
14723
+ }
14724
+
14586
14725
  // ../schema/src/enums.ts
14587
14726
  var SpeciesSchema = external_exports.enum(SPECIES_IDS).meta({
14588
14727
  id: "Species",
@@ -14592,6 +14731,10 @@ var StatusSchema = external_exports.enum(STATUS_IDS).meta({
14592
14731
  id: "Status",
14593
14732
  description: "Lifecycle status of a node."
14594
14733
  });
14734
+ var AnyStatusSchema = external_exports.enum([...STATUS_IDS, ...LEGACY_STATUS_IDS]).meta({
14735
+ id: "AnyStatus",
14736
+ description: "Lifecycle status as stored: the current vocabulary, or a legacy id (prioritized, blocked) accepted from pre-v3 bundles and migrated on load."
14737
+ });
14595
14738
  var PlatformSchema = external_exports.enum(PLATFORM_IDS).meta({
14596
14739
  id: "Platform",
14597
14740
  description: "Target platform."
@@ -14608,6 +14751,10 @@ var ValueSchema = external_exports.enum(VALUE_IDS).meta({
14608
14751
  id: "Value",
14609
14752
  description: "A Bain B2C Elements-of-Value element served by an acceptance (spec \xA73.2)."
14610
14753
  });
14754
+ var DecisionStatusSchema = external_exports.enum(DECISION_STATUS_IDS).meta({
14755
+ id: "DecisionStatus",
14756
+ 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)."
14757
+ });
14611
14758
 
14612
14759
  // ../schema/src/id-gen.ts
14613
14760
  var SPECIES_PREFIXES = {
@@ -14615,8 +14762,12 @@ var SPECIES_PREFIXES = {
14615
14762
  view: "V-",
14616
14763
  "data-model": "DM-",
14617
14764
  "api-endpoint": "API-",
14618
- acceptance: "AC-"
14765
+ acceptance: "AC-",
14766
+ decision: "DEC-"
14619
14767
  };
14768
+ function edgeId(sourceId, targetId) {
14769
+ return `e-${sourceId}-${targetId}`;
14770
+ }
14620
14771
 
14621
14772
  // ../schema/src/playlist.ts
14622
14773
  var JunctionCaseSchema = external_exports.lazy(
@@ -14713,6 +14864,7 @@ function parseJournalLines(text) {
14713
14864
  var NODE_REF_FIELDS = {
14714
14865
  "node.updated": ["node_id"],
14715
14866
  "node.status_changed": ["node_id"],
14867
+ "decision.status_changed": ["node_id"],
14716
14868
  "node.deleted": ["node_id"],
14717
14869
  "edge.added": ["source_id", "target_id"],
14718
14870
  "ref.added": ["node_id"],
@@ -14721,6 +14873,12 @@ var NODE_REF_FIELDS = {
14721
14873
  "idea.proposed": ["node_id"],
14722
14874
  "request.filed": ["node_id"]
14723
14875
  };
14876
+ function statusesAgree(last, snapshot) {
14877
+ if (last === snapshot) return true;
14878
+ if (typeof snapshot !== "string") return false;
14879
+ if (last === "backlog" && snapshot === "idea") return true;
14880
+ return (normalizeStatus(last) ?? last) === (normalizeStatus(snapshot) ?? snapshot);
14881
+ }
14724
14882
  function crossCheckJournal(bundle) {
14725
14883
  const findings = [];
14726
14884
  const journalRaw = bundle.journal;
@@ -14739,9 +14897,15 @@ function crossCheckJournal(bundle) {
14739
14897
  const nodesRaw = Array.isArray(bundle.nodes) ? bundle.nodes : [];
14740
14898
  const edgesRaw = Array.isArray(bundle.edges) ? bundle.edges : [];
14741
14899
  const snapshotNodeStatus = /* @__PURE__ */ new Map();
14900
+ const snapshotDecisionStatus = /* @__PURE__ */ new Map();
14742
14901
  for (const n of nodesRaw) {
14743
14902
  const id = str2(n?.id);
14744
- if (id !== void 0) snapshotNodeStatus.set(id, n.status);
14903
+ if (id !== void 0) {
14904
+ snapshotNodeStatus.set(id, n.status);
14905
+ const metadata = n.metadata;
14906
+ const decisionStatus = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata.decision_status : void 0;
14907
+ snapshotDecisionStatus.set(id, decisionStatus ?? "proposed");
14908
+ }
14745
14909
  }
14746
14910
  const snapshotEdgeIds = /* @__PURE__ */ new Set();
14747
14911
  for (const e of edgesRaw) {
@@ -14750,10 +14914,10 @@ function crossCheckJournal(bundle) {
14750
14914
  }
14751
14915
  const valid = [];
14752
14916
  journalRaw.forEach((raw, index) => {
14753
- const path = `journal[${index}]`;
14917
+ const path6 = `journal[${index}]`;
14754
14918
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
14755
14919
  findings.push({
14756
- path,
14920
+ path: path6,
14757
14921
  rule: "journal-event-shape",
14758
14922
  message: `journal[${index}]: each event must be a JSON object.`,
14759
14923
  severity: "error"
@@ -14767,7 +14931,7 @@ function crossCheckJournal(bundle) {
14767
14931
  if (str2(ev.type) === void 0) missing.push("type");
14768
14932
  if (missing.length > 0) {
14769
14933
  findings.push({
14770
- path,
14934
+ path: path6,
14771
14935
  rule: "journal-event-envelope",
14772
14936
  message: `journal[${index}]: event is missing required envelope field(s): ${missing.join(", ")}.`,
14773
14937
  severity: "error"
@@ -14790,6 +14954,7 @@ function crossCheckJournal(bundle) {
14790
14954
  const ordered = orderEvents(valid.map((v) => v.ev));
14791
14955
  const created = /* @__PURE__ */ new Set();
14792
14956
  const lastProjectStatus = /* @__PURE__ */ new Map();
14957
+ const lastDecisionStatus = /* @__PURE__ */ new Map();
14793
14958
  for (const ev of ordered) {
14794
14959
  if (ev.type === "node.created") {
14795
14960
  const nid = str2(ev.node_id);
@@ -14800,6 +14965,12 @@ function crossCheckJournal(bundle) {
14800
14965
  const to = str2(ev.to);
14801
14966
  if (to !== void 0) lastProjectStatus.set(nid, to);
14802
14967
  }
14968
+ } else if (ev.type === "decision.status_changed") {
14969
+ const nid = str2(ev.node_id);
14970
+ if (nid) {
14971
+ const to = str2(ev.to);
14972
+ if (to !== void 0) lastDecisionStatus.set(nid, to);
14973
+ }
14803
14974
  }
14804
14975
  }
14805
14976
  for (const { ev, index } of valid) {
@@ -14817,6 +14988,19 @@ function crossCheckJournal(bundle) {
14817
14988
  }
14818
14989
  }
14819
14990
  }
14991
+ if (ev.type === "deliverable.shipped" && Array.isArray(ev.node_ids)) {
14992
+ ev.node_ids.forEach((raw, i) => {
14993
+ const ref = str2(raw);
14994
+ if (ref !== void 0 && !everNodes.has(ref)) {
14995
+ findings.push({
14996
+ path: `journal[${index}].node_ids[${i}]`,
14997
+ rule: "journal-dangling-node-ref",
14998
+ message: `journal[${index}] (${ev.type}): references node "${ref}" that never existed in the snapshot or journal.`,
14999
+ severity: "error"
15000
+ });
15001
+ }
15002
+ });
15003
+ }
14820
15004
  if (ev.type === "edge.removed") {
14821
15005
  const ref = str2(ev.edge_id);
14822
15006
  if (ref !== void 0 && !everEdges.has(ref)) {
@@ -14839,7 +15023,7 @@ function crossCheckJournal(bundle) {
14839
15023
  });
14840
15024
  }
14841
15025
  const last = lastProjectStatus.get(nodeId);
14842
- if (last !== void 0 && last !== status) {
15026
+ if (last !== void 0 && !statusesAgree(last, status)) {
14843
15027
  findings.push({
14844
15028
  path: "journal",
14845
15029
  rule: "journal-status-mismatch",
@@ -14848,6 +15032,18 @@ function crossCheckJournal(bundle) {
14848
15032
  });
14849
15033
  }
14850
15034
  }
15035
+ for (const [nodeId, last] of lastDecisionStatus) {
15036
+ if (!snapshotDecisionStatus.has(nodeId)) continue;
15037
+ const current = snapshotDecisionStatus.get(nodeId) ?? "proposed";
15038
+ if (last !== current) {
15039
+ findings.push({
15040
+ path: "journal",
15041
+ rule: "journal-decision-status-mismatch",
15042
+ message: `Node "${nodeId}": journal's last decision.status_changed.to "${last}" disagrees with snapshot decision_status "${String(current)}".`,
15043
+ severity: "error"
15044
+ });
15045
+ }
15046
+ }
14851
15047
  return findings;
14852
15048
  }
14853
15049
 
@@ -14878,10 +15074,19 @@ var NodeStatusChangedEventSchema = external_exports.object({
14878
15074
  ...envelope,
14879
15075
  type: external_exports.literal("node.status_changed"),
14880
15076
  node_id: external_exports.string(),
14881
- from: StatusSchema,
14882
- to: StatusSchema,
15077
+ // History is never rewritten (docs/spec/journal.md): pre-v3 events keep
15078
+ // their legacy status ids, so strict per-type validation must accept them.
15079
+ from: AnyStatusSchema,
15080
+ to: AnyStatusSchema,
14883
15081
  platform: PlatformSchema.optional()
14884
15082
  }).catchall(external_exports.unknown());
15083
+ var DecisionStatusChangedEventSchema = external_exports.object({
15084
+ ...envelope,
15085
+ type: external_exports.literal("decision.status_changed"),
15086
+ node_id: external_exports.string(),
15087
+ from: DecisionStatusSchema,
15088
+ to: DecisionStatusSchema
15089
+ }).catchall(external_exports.unknown());
14885
15090
  var NodeDeletedEventSchema = external_exports.object({ ...envelope, type: external_exports.literal("node.deleted"), node_id: external_exports.string() }).catchall(external_exports.unknown());
14886
15091
  var EdgeAddedEventSchema = external_exports.object({
14887
15092
  ...envelope,
@@ -14899,6 +15104,16 @@ var ReleaseTaggedEventSchema = external_exports.object({
14899
15104
  notes: external_exports.string().optional(),
14900
15105
  platform: PlatformSchema.optional()
14901
15106
  }).catchall(external_exports.unknown());
15107
+ var DeliverableShippedEventSchema = external_exports.object({
15108
+ ...envelope,
15109
+ type: external_exports.literal("deliverable.shipped"),
15110
+ deliverable_id: external_exports.string(),
15111
+ title: external_exports.string(),
15112
+ summary: external_exports.string().optional(),
15113
+ url: external_exports.string().optional(),
15114
+ node_ids: external_exports.array(external_exports.string()).optional(),
15115
+ platform: PlatformSchema.optional()
15116
+ }).catchall(external_exports.unknown());
14902
15117
  var IdeaProposedEventSchema = external_exports.object({
14903
15118
  ...envelope,
14904
15119
  type: external_exports.literal("idea.proposed"),
@@ -14936,10 +15151,12 @@ var JOURNAL_EVENT_SCHEMAS = {
14936
15151
  "node.created": NodeCreatedEventSchema,
14937
15152
  "node.updated": NodeUpdatedEventSchema,
14938
15153
  "node.status_changed": NodeStatusChangedEventSchema,
15154
+ "decision.status_changed": DecisionStatusChangedEventSchema,
14939
15155
  "node.deleted": NodeDeletedEventSchema,
14940
15156
  "edge.added": EdgeAddedEventSchema,
14941
15157
  "edge.removed": EdgeRemovedEventSchema,
14942
15158
  "release.tagged": ReleaseTaggedEventSchema,
15159
+ "deliverable.shipped": DeliverableShippedEventSchema,
14943
15160
  "idea.proposed": IdeaProposedEventSchema,
14944
15161
  "request.filed": RequestFiledEventSchema,
14945
15162
  "ref.added": RefAddedEventSchema,
@@ -14950,10 +15167,12 @@ var KnownJournalEventSchema = external_exports.union([
14950
15167
  NodeCreatedEventSchema,
14951
15168
  NodeUpdatedEventSchema,
14952
15169
  NodeStatusChangedEventSchema,
15170
+ DecisionStatusChangedEventSchema,
14953
15171
  NodeDeletedEventSchema,
14954
15172
  EdgeAddedEventSchema,
14955
15173
  EdgeRemovedEventSchema,
14956
15174
  ReleaseTaggedEventSchema,
15175
+ DeliverableShippedEventSchema,
14957
15176
  IdeaProposedEventSchema,
14958
15177
  RequestFiledEventSchema,
14959
15178
  RefAddedEventSchema,
@@ -14964,7 +15183,8 @@ var KnownJournalEventSchema = external_exports.union([
14964
15183
  // ../schema/src/bundle.ts
14965
15184
  var PlatformStatusMapSchema = external_exports.partialRecord(
14966
15185
  PlatformSchema,
14967
- StatusSchema
15186
+ // legacy-tolerant: migrateStatusVocabulary normalizes on load
15187
+ AnyStatusSchema
14968
15188
  ).meta({ id: "PlatformStatusMap", description: "Per-platform status overrides for view nodes." });
14969
15189
  var PlatformNotesMapSchema = external_exports.partialRecord(
14970
15190
  PlatformSchema,
@@ -14987,7 +15207,8 @@ var RefSchema = external_exports.object({
14987
15207
  external_status: external_exports.string().optional().meta({
14988
15208
  description: 'Mirrored external state, verbatim (e.g. "open", "merged", "In Progress").'
14989
15209
  }),
14990
- status_mapped: StatusSchema.optional().meta({
15210
+ // legacy-tolerant: migrateStatusVocabulary normalizes on load
15211
+ status_mapped: AnyStatusSchema.optional().meta({
14991
15212
  description: "Optional mapping of external_status into the arkaik lifecycle. Advisory display data \u2014 never mutates node.status."
14992
15213
  }),
14993
15214
  platform: PlatformSchema.optional().meta({ description: "Optional scoping to one platform variant." }),
@@ -14995,6 +15216,9 @@ var RefSchema = external_exports.object({
14995
15216
  }).meta({ id: "Ref", description: "A typed external reference on a node (docs/spec/bundle-format.md \xA7 References)." });
14996
15217
  var NodeMetadataSchema = external_exports.object({
14997
15218
  stage: external_exports.string().optional(),
15219
+ blocked_by: external_exports.string().optional().meta({
15220
+ description: "Non-empty = blocked at the current status. A node id (rendered as a link) or free text naming the dependency."
15221
+ }),
14998
15222
  playlist: FlowPlaylistSchema.optional(),
14999
15223
  platformNotes: PlatformNotesMapSchema.optional(),
15000
15224
  platformStatuses: PlatformStatusMapSchema.optional(),
@@ -15005,6 +15229,21 @@ var NodeMetadataSchema = external_exports.object({
15005
15229
  }),
15006
15230
  values: external_exports.array(ValueSchema).optional().meta({
15007
15231
  description: "Acceptance nodes only: 1..n Bain value elements served (the Why)."
15232
+ }),
15233
+ product: external_exports.string().optional().meta({
15234
+ description: "Product membership (docs/spec/bundle-format.md \xA7 Products); flow, view, and acceptance only."
15235
+ }),
15236
+ decision_status: DecisionStatusSchema.optional().meta({
15237
+ description: "Decision nodes only: proposed | approved | enacted | rejected | deprecated | superseded. The node's lifecycle status is kept in sync (spec \xA72)."
15238
+ }),
15239
+ context: external_exports.string().optional().meta({
15240
+ description: "Decision nodes only: Context \u2014 the Why (markdown)."
15241
+ }),
15242
+ consequences: external_exports.string().optional().meta({
15243
+ description: "Decision nodes only: Consequences \u2014 the How (markdown)."
15244
+ }),
15245
+ decided_at: external_exports.string().optional().meta({
15246
+ description: "Decision nodes only: ISO 8601 date the decision was made."
15008
15247
  })
15009
15248
  }).catchall(external_exports.unknown()).meta({ id: "NodeMetadata", description: "Optional metadata for a node." });
15010
15249
  var NodeSchema = external_exports.object({
@@ -15013,7 +15252,8 @@ var NodeSchema = external_exports.object({
15013
15252
  species: SpeciesSchema,
15014
15253
  title: external_exports.string().meta({ description: "Human-readable node title." }),
15015
15254
  description: external_exports.string().optional().meta({ description: "Optional description of the node's purpose." }),
15016
- status: StatusSchema,
15255
+ // legacy-tolerant: migrateStatusVocabulary normalizes on load
15256
+ status: AnyStatusSchema,
15017
15257
  platforms: external_exports.array(PlatformSchema).meta({ description: "One or more target platforms." }),
15018
15258
  metadata: NodeMetadataSchema.optional()
15019
15259
  }).meta({ id: "Node" });
@@ -15025,6 +15265,18 @@ var EdgeSchema = external_exports.object({
15025
15265
  edge_type: EdgeTypeSchema,
15026
15266
  metadata: external_exports.record(external_exports.string(), external_exports.unknown()).optional().meta({ description: "Optional edge metadata." })
15027
15267
  }).meta({ id: "Edge" });
15268
+ var MapDisplayOptionsSchema = external_exports.object({
15269
+ images: external_exports.boolean().optional().meta({ description: "Screenshot (or cover) art on view cards." }),
15270
+ flow_platforms: external_exports.string().optional().meta({
15271
+ description: "A flow card's platform delivery: rings (default) | bars."
15272
+ }),
15273
+ view_platforms: external_exports.string().optional().meta({
15274
+ description: "A view card's platform availability: chips (default) | rows."
15275
+ }),
15276
+ minimap_color: external_exports.string().optional().meta({
15277
+ description: "What a minimap node's fill encodes: status (default) | species."
15278
+ })
15279
+ }).catchall(external_exports.unknown()).meta({ id: "MapDisplayOptions", description: "How a map draws its cards (docs/spec/maps.md \xA7 Display Options)." });
15028
15280
  var MapDefinitionSchema = external_exports.object({
15029
15281
  id: external_exports.string().meta({
15030
15282
  description: "Kebab-case, unique within the project; built-in ids (journey, system) are reserved."
@@ -15043,13 +15295,32 @@ var MapDefinitionSchema = external_exports.object({
15043
15295
  root_node_id: external_exports.string().optional().meta({
15044
15296
  description: "Scope anchor: the subgraph is the undirected neighborhood reachable from this node."
15045
15297
  }),
15298
+ product: external_exports.string().optional().meta({ description: "Product scope; absent = every product." }),
15046
15299
  depth: external_exports.number().optional().meta({ description: "Traversal bound from the root; absent = unbounded." }),
15047
- layout: external_exports.object({ direction: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Renderer layout hints (e.g. direction: DOWN | RIGHT)." })
15300
+ layout: external_exports.object({ direction: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Renderer layout hints (e.g. direction: DOWN | RIGHT)." }),
15301
+ display: MapDisplayOptionsSchema.optional().meta({
15302
+ description: "Card rendering; the human twin is project.metadata.map_display[id]."
15303
+ })
15048
15304
  }).catchall(external_exports.unknown()).meta({ id: "MapDefinition", description: "A stored map definition (docs/spec/maps.md \xA7 MapDefinition)." });
15305
+ var ProductDefinitionSchema = external_exports.object({
15306
+ id: external_exports.string().meta({ description: "Kebab-case, unique within the project." }),
15307
+ title: external_exports.string().meta({ description: "Display title." }),
15308
+ description: external_exports.string().optional().meta({ description: "What this product is." }),
15309
+ platforms: external_exports.array(PlatformSchema).meta({
15310
+ description: "The platforms this product can ship on; empty means availability is not tracked."
15311
+ }),
15312
+ root_node_id: external_exports.string().optional().meta({ description: "This product's journey anchor." })
15313
+ }).catchall(external_exports.unknown()).meta({ id: "ProductDefinition", description: "A product definition (docs/spec/bundle-format.md \xA7 Products)." });
15049
15314
  var ProjectMetadataSchema = external_exports.object({
15050
15315
  view_card_variant: external_exports.enum(["compact", "large"]).optional(),
15051
15316
  maps: external_exports.array(MapDefinitionSchema).optional().meta({
15052
15317
  description: "Stored map definitions (docs/spec/maps.md \xA7 Storage) \u2014 additive; unknown fields preserved."
15318
+ }),
15319
+ map_display: external_exports.record(external_exports.string(), MapDisplayOptionsSchema).optional().meta({
15320
+ 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."
15321
+ }),
15322
+ products: external_exports.array(ProductDefinitionSchema).optional().meta({
15323
+ description: "Product definitions (docs/spec/bundle-format.md \xA7 Products) \u2014 additive; unknown fields preserved."
15053
15324
  })
15054
15325
  }).catchall(external_exports.unknown()).meta({ id: "ProjectMetadata", description: "Optional project-level UI settings." });
15055
15326
  var ProjectSchema = external_exports.object({
@@ -15086,11 +15357,31 @@ var ProjectBundleSchema = external_exports.object({
15086
15357
  });
15087
15358
 
15088
15359
  // ../schema/src/maps.ts
15360
+ var MAP_FLOW_PLATFORMS_MODES = ["rings", "bars"];
15361
+ var MAP_VIEW_PLATFORMS_MODES = ["chips", "rows"];
15362
+ var MAP_MINIMAP_COLOR_MODES = ["status", "species"];
15089
15363
  var BUILT_IN_MAP_IDS = ["journey", "system"];
15090
15364
  function isBuiltInMapId(id) {
15091
15365
  return BUILT_IN_MAP_IDS.includes(id);
15092
15366
  }
15093
15367
 
15368
+ // ../schema/src/products.ts
15369
+ var PRODUCT_MEMBERSHIP_SPECIES = ["flow", "view", "acceptance"];
15370
+ function resolveProducts(project) {
15371
+ const stored = project?.metadata?.products;
15372
+ if (!Array.isArray(stored)) return [];
15373
+ const seen = /* @__PURE__ */ new Set();
15374
+ const products = [];
15375
+ for (const entry of stored) {
15376
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
15377
+ const candidate = entry;
15378
+ if (typeof candidate.id !== "string" || candidate.id.trim() === "" || seen.has(candidate.id)) continue;
15379
+ seen.add(candidate.id);
15380
+ products.push(candidate);
15381
+ }
15382
+ return products;
15383
+ }
15384
+
15094
15385
  // ../schema/src/validate.ts
15095
15386
  var VALID_STAGES = ["beta", "monitoring", "deprecated"];
15096
15387
  var VALID_VIEW_CARD_VARIANTS = ["compact", "large"];
@@ -15102,32 +15393,13 @@ function estimateDataUriBytes(dataUri) {
15102
15393
  const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
15103
15394
  return Math.floor(payload.length * 3 / 4) - padding;
15104
15395
  }
15105
- var VALID_EDGE_SEMANTICS = {
15106
- composes: [
15107
- ["flow", "view"],
15108
- ["flow", "flow"],
15109
- ["view", "flow"],
15110
- ["view", "view"]
15111
- ],
15112
- calls: [
15113
- ["view", "api-endpoint"],
15114
- ["flow", "api-endpoint"],
15115
- ["api-endpoint", "api-endpoint"]
15116
- ],
15117
- displays: [["view", "data-model"]],
15118
- queries: [["api-endpoint", "data-model"]],
15119
- covers: [
15120
- ["acceptance", "view"],
15121
- ["acceptance", "flow"]
15122
- ]
15123
- };
15124
15396
  function isIsoDate(value) {
15125
15397
  return typeof value === "string" && !Number.isNaN(Date.parse(value));
15126
15398
  }
15127
15399
  function validateBundle(input) {
15128
15400
  const findings = [];
15129
- const error51 = (path, rule, message) => findings.push({ path, rule, message, severity: "error" });
15130
- const warn = (path, rule, message) => findings.push({ path, rule, message, severity: "warning" });
15401
+ const error51 = (path6, rule, message) => findings.push({ path: path6, rule, message, severity: "error" });
15402
+ const warn = (path6, rule, message) => findings.push({ path: path6, rule, message, severity: "warning" });
15131
15403
  const result = () => {
15132
15404
  const errors = findings.filter((f) => f.severity === "error");
15133
15405
  const warnings = findings.filter((f) => f.severity === "warning");
@@ -15216,7 +15488,9 @@ function validateBundle(input) {
15216
15488
  }
15217
15489
  const platforms = node.platforms;
15218
15490
  if (!platforms || platforms.length === 0) {
15219
- error51(`${base}.platforms`, "platforms-non-empty", `Node ${nodeId}: platforms array is empty or missing`);
15491
+ if (species !== "decision") {
15492
+ error51(`${base}.platforms`, "platforms-non-empty", `Node ${nodeId}: platforms array is empty or missing`);
15493
+ }
15220
15494
  } else {
15221
15495
  for (const p of platforms) {
15222
15496
  if (!PLATFORM_IDS.includes(p)) {
@@ -15338,6 +15612,25 @@ function validateBundle(input) {
15338
15612
  }
15339
15613
  });
15340
15614
  }
15615
+ const decisionStatus = md.decision_status;
15616
+ if (decisionStatus !== void 0 && species !== "decision") {
15617
+ warn(
15618
+ `${base}.metadata.decision_status`,
15619
+ "decision-status-wrong-species",
15620
+ `decision_status is meaningful on decision nodes only; "${nodeId}" is a ${species}.`
15621
+ );
15622
+ }
15623
+ if (species === "decision") {
15624
+ const effective = decisionStatusOf({ metadata: node.metadata });
15625
+ const expected = lifecycleStatusForDecision(effective);
15626
+ if (node.status !== expected) {
15627
+ warn(
15628
+ `${base}.status`,
15629
+ "decision-lifecycle-mismatch",
15630
+ `Decision "${nodeId}" is ${effective}, whose lifecycle status should be "${expected}", but status is "${node.status}" (spec \xA72).`
15631
+ );
15632
+ }
15633
+ }
15341
15634
  if (species === "flow") {
15342
15635
  const playlist = md.playlist;
15343
15636
  if (!node.metadata || !playlist || !playlist.entries) {
@@ -15355,27 +15648,59 @@ function validateBundle(input) {
15355
15648
  `project.root_node_id "${rootNodeId}" does not reference an existing node`
15356
15649
  );
15357
15650
  }
15651
+ const checkMapDisplay = (display, path6, subject) => {
15652
+ if (typeof display !== "object" || display === null || Array.isArray(display)) return;
15653
+ const options = display;
15654
+ const modes = [
15655
+ ["flow_platforms", MAP_FLOW_PLATFORMS_MODES],
15656
+ ["view_platforms", MAP_VIEW_PLATFORMS_MODES],
15657
+ ["minimap_color", MAP_MINIMAP_COLOR_MODES]
15658
+ ];
15659
+ for (const [key, allowed] of modes) {
15660
+ const value = options[key];
15661
+ if (value !== void 0 && (typeof value !== "string" || !allowed.includes(value))) {
15662
+ warn(
15663
+ `${path6}.${key}`,
15664
+ "map-unknown-display",
15665
+ `${subject} sets ${key} to "${String(value)}"; expected one of ${allowed.join(", ")} (renderers fall back to the default)`
15666
+ );
15667
+ }
15668
+ }
15669
+ if (options.images !== void 0 && typeof options.images !== "boolean") {
15670
+ warn(
15671
+ `${path6}.images`,
15672
+ "map-unknown-display",
15673
+ `${subject} sets images to a non-boolean value (renderers fall back to the default)`
15674
+ );
15675
+ }
15676
+ };
15677
+ const mapDisplayOverrides = projectMetadata?.map_display;
15678
+ if (typeof mapDisplayOverrides === "object" && mapDisplayOverrides !== null && !Array.isArray(mapDisplayOverrides)) {
15679
+ for (const [mapId, display] of Object.entries(mapDisplayOverrides)) {
15680
+ checkMapDisplay(display, `project.metadata.map_display.${mapId}`, `Map "${mapId}"`);
15681
+ }
15682
+ }
15358
15683
  const storedMaps = projectMetadata?.maps;
15359
15684
  if (Array.isArray(storedMaps)) {
15360
15685
  const seenMapIds = /* @__PURE__ */ new Set();
15361
15686
  storedMaps.forEach((definition, index) => {
15362
15687
  if (typeof definition !== "object" || definition === null || Array.isArray(definition)) return;
15363
15688
  const map2 = definition;
15364
- const path = `project.metadata.maps[${index}]`;
15689
+ const path6 = `project.metadata.maps[${index}]`;
15365
15690
  const mapId = typeof map2.id === "string" ? map2.id : void 0;
15366
15691
  if (mapId !== void 0) {
15367
15692
  if (seenMapIds.has(mapId)) {
15368
- warn(`${path}.id`, "map-duplicate-id", `Duplicate map id "${mapId}"`);
15693
+ warn(`${path6}.id`, "map-duplicate-id", `Duplicate map id "${mapId}"`);
15369
15694
  }
15370
15695
  seenMapIds.add(mapId);
15371
15696
  if (isBuiltInMapId(mapId)) {
15372
- warn(`${path}.id`, "map-shadows-built-in", `Map id "${mapId}" shadows a built-in map and will be ignored`);
15697
+ warn(`${path6}.id`, "map-shadows-built-in", `Map id "${mapId}" shadows a built-in map and will be ignored`);
15373
15698
  }
15374
15699
  }
15375
15700
  const mapRoot = map2.root_node_id;
15376
15701
  if (typeof mapRoot === "string" && !nodeIds.has(mapRoot)) {
15377
15702
  warn(
15378
- `${path}.root_node_id`,
15703
+ `${path6}.root_node_id`,
15379
15704
  "map-unknown-root",
15380
15705
  `Map "${mapId ?? index}" anchors on "${mapRoot}" which does not reference an existing node`
15381
15706
  );
@@ -15384,7 +15709,7 @@ function validateBundle(input) {
15384
15709
  map2.species.forEach((value, valueIndex) => {
15385
15710
  if (typeof value === "string" && !SPECIES_IDS.includes(value)) {
15386
15711
  warn(
15387
- `${path}.species[${valueIndex}]`,
15712
+ `${path6}.species[${valueIndex}]`,
15388
15713
  "map-unknown-species",
15389
15714
  `Map "${mapId ?? index}" filters on unknown species "${value}"`
15390
15715
  );
@@ -15395,15 +15720,129 @@ function validateBundle(input) {
15395
15720
  map2.edge_types.forEach((value, valueIndex) => {
15396
15721
  if (typeof value === "string" && !EDGE_TYPE_IDS.includes(value)) {
15397
15722
  warn(
15398
- `${path}.edge_types[${valueIndex}]`,
15723
+ `${path6}.edge_types[${valueIndex}]`,
15399
15724
  "map-unknown-edge-type",
15400
15725
  `Map "${mapId ?? index}" filters on unknown edge type "${value}"`
15401
15726
  );
15402
15727
  }
15403
15728
  });
15404
15729
  }
15730
+ checkMapDisplay(map2.display, `${path6}.display`, `Map "${mapId ?? index}"`);
15731
+ });
15732
+ }
15733
+ const storedProducts = projectMetadata?.products;
15734
+ const declaredProductIds = /* @__PURE__ */ new Set();
15735
+ if (Array.isArray(storedProducts)) {
15736
+ const seenProductIds = /* @__PURE__ */ new Set();
15737
+ storedProducts.forEach((definition, index) => {
15738
+ if (typeof definition !== "object" || definition === null || Array.isArray(definition)) return;
15739
+ const product = definition;
15740
+ const path6 = `project.metadata.products[${index}]`;
15741
+ const productId = typeof product.id === "string" ? product.id : void 0;
15742
+ if (productId === void 0) return;
15743
+ if (seenProductIds.has(productId)) {
15744
+ warn(`${path6}.id`, "product-duplicate-id", `Duplicate product id "${productId}" \u2014 the first wins`);
15745
+ } else {
15746
+ seenProductIds.add(productId);
15747
+ if (productId.trim() !== "") declaredProductIds.add(productId);
15748
+ }
15749
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(productId)) {
15750
+ warn(`${path6}.id`, "product-invalid-id", `Product id "${productId}" is not kebab-case`);
15751
+ }
15405
15752
  });
15406
15753
  }
15754
+ const hasProducts = declaredProductIds.size > 0;
15755
+ const anchorsByAcceptance = /* @__PURE__ */ new Map();
15756
+ for (const edge of edges) {
15757
+ if (edge.edge_type !== "covers") continue;
15758
+ const source = typeof edge.source_id === "string" ? edge.source_id : void 0;
15759
+ const target = typeof edge.target_id === "string" ? edge.target_id : void 0;
15760
+ if (source === void 0 || target === void 0) continue;
15761
+ const list = anchorsByAcceptance.get(source) ?? [];
15762
+ list.push(target);
15763
+ anchorsByAcceptance.set(source, list);
15764
+ }
15765
+ const menuByProduct = /* @__PURE__ */ new Map();
15766
+ if (hasProducts) {
15767
+ for (const definition of resolveProducts({ metadata: projectMetadata })) {
15768
+ menuByProduct.set(
15769
+ definition.id,
15770
+ new Set(Array.isArray(definition.platforms) ? definition.platforms : [])
15771
+ );
15772
+ }
15773
+ }
15774
+ const productByNodeId = /* @__PURE__ */ new Map();
15775
+ const indexByNodeId = /* @__PURE__ */ new Map();
15776
+ nodes.forEach((node, index) => {
15777
+ const nodeId = typeof node.id === "string" ? node.id : `#${index}`;
15778
+ const species = node.species;
15779
+ const base = `nodes[${index}]`;
15780
+ indexByNodeId.set(nodeId, index);
15781
+ const metadata = node.metadata ?? {};
15782
+ const membership = typeof metadata.product === "string" ? metadata.product : void 0;
15783
+ const storesMembership = species !== void 0 && PRODUCT_MEMBERSHIP_SPECIES.includes(species);
15784
+ if (membership !== void 0 && !storesMembership) {
15785
+ 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";
15786
+ warn(`${base}.metadata.product`, "product-membership-wrong-species", `Node ${nodeId}: ${detail}`);
15787
+ return;
15788
+ }
15789
+ if (!storesMembership) return;
15790
+ if (membership === void 0) {
15791
+ if (!hasProducts) return;
15792
+ if (species === "acceptance") {
15793
+ if (!anchorsByAcceptance.has(nodeId)) {
15794
+ warn(
15795
+ `${base}.metadata.product`,
15796
+ "acceptance-product-unassigned",
15797
+ `Acceptance ${nodeId} covers nothing and names no product \u2014 it will show only under "All products"`
15798
+ );
15799
+ }
15800
+ } else {
15801
+ warn(
15802
+ `${base}.metadata.product`,
15803
+ "unassigned-membership",
15804
+ `Node ${nodeId}: no product membership \u2014 it will show only under "All products"`
15805
+ );
15806
+ }
15807
+ return;
15808
+ }
15809
+ productByNodeId.set(nodeId, membership);
15810
+ if (!declaredProductIds.has(membership)) {
15811
+ warn(
15812
+ `${base}.metadata.product`,
15813
+ "product-unknown-reference",
15814
+ `Node ${nodeId}: product "${membership}" is not declared on the project`
15815
+ );
15816
+ return;
15817
+ }
15818
+ const menu = menuByProduct.get(membership);
15819
+ const nodePlatforms = Array.isArray(node.platforms) ? node.platforms : [];
15820
+ if (menu) {
15821
+ for (const platform of nodePlatforms) {
15822
+ if (typeof platform === "string" && !menu.has(platform)) {
15823
+ warn(
15824
+ `${base}.platforms`,
15825
+ "product-platform-not-in-menu",
15826
+ `Node ${nodeId}: platform "${platform}" is not in product "${membership}"'s menu`
15827
+ );
15828
+ }
15829
+ }
15830
+ }
15831
+ });
15832
+ if (hasProducts) {
15833
+ for (const [acceptanceId, anchors] of anchorsByAcceptance) {
15834
+ const acceptanceIndex = indexByNodeId.get(acceptanceId);
15835
+ if (acceptanceIndex === void 0) continue;
15836
+ const spanned = new Set(anchors.map((id) => productByNodeId.get(id)).filter((id) => Boolean(id)));
15837
+ if (spanned.size > 1) {
15838
+ warn(
15839
+ `nodes[${acceptanceIndex}].metadata.product`,
15840
+ "acceptance-covers-span-products",
15841
+ `Acceptance ${acceptanceId} covers anchors in ${[...spanned].sort().join(" and ")} \u2014 statuses may conflate products`
15842
+ );
15843
+ }
15844
+ }
15845
+ }
15407
15846
  const edgeIds = /* @__PURE__ */ new Set();
15408
15847
  const edgeSignatures = /* @__PURE__ */ new Set();
15409
15848
  const composesSet = /* @__PURE__ */ new Set();
@@ -15459,33 +15898,33 @@ function validateBundle(input) {
15459
15898
  composesSet.add(`${sourceId}->${targetId}`);
15460
15899
  }
15461
15900
  });
15462
- const collectPlaylistRefs = (entries, flowId, path, depth = 0) => {
15901
+ const collectPlaylistRefs = (entries, flowId, path6, depth = 0) => {
15463
15902
  if (depth > 50) {
15464
- error51(path, "playlist-depth", `Flow ${flowId}: playlist nesting too deep (possible cycle)`);
15903
+ error51(path6, "playlist-depth", `Flow ${flowId}: playlist nesting too deep (possible cycle)`);
15465
15904
  return [];
15466
15905
  }
15467
15906
  const refs = [];
15468
15907
  for (const entry of entries) {
15469
15908
  if (entry.type === "view") {
15470
15909
  if (!nodeIds.has(entry.view_id)) {
15471
- error51(path, "playlist-ref-exists", `Flow ${flowId}: playlist references non-existent view "${entry.view_id}"`);
15910
+ error51(path6, "playlist-ref-exists", `Flow ${flowId}: playlist references non-existent view "${entry.view_id}"`);
15472
15911
  }
15473
15912
  refs.push(entry.view_id);
15474
15913
  } else if (entry.type === "flow") {
15475
15914
  if (!nodeIds.has(entry.flow_id)) {
15476
- error51(path, "playlist-ref-exists", `Flow ${flowId}: playlist references non-existent flow "${entry.flow_id}"`);
15915
+ error51(path6, "playlist-ref-exists", `Flow ${flowId}: playlist references non-existent flow "${entry.flow_id}"`);
15477
15916
  }
15478
15917
  if (entry.flow_id === flowId) {
15479
- error51(path, "playlist-self-cycle", `Flow ${flowId}: playlist contains itself (direct cycle)`);
15918
+ error51(path6, "playlist-self-cycle", `Flow ${flowId}: playlist contains itself (direct cycle)`);
15480
15919
  }
15481
15920
  refs.push(entry.flow_id);
15482
15921
  } else if (entry.type === "condition") {
15483
- if (entry.if_true) refs.push(...collectPlaylistRefs(entry.if_true, flowId, path, depth + 1));
15484
- if (entry.if_false) refs.push(...collectPlaylistRefs(entry.if_false, flowId, path, depth + 1));
15922
+ if (entry.if_true) refs.push(...collectPlaylistRefs(entry.if_true, flowId, path6, depth + 1));
15923
+ if (entry.if_false) refs.push(...collectPlaylistRefs(entry.if_false, flowId, path6, depth + 1));
15485
15924
  } else if (entry.type === "junction") {
15486
15925
  if (entry.cases) {
15487
15926
  for (const c of entry.cases) {
15488
- refs.push(...collectPlaylistRefs(c.entries || [], flowId, path, depth + 1));
15927
+ refs.push(...collectPlaylistRefs(c.entries || [], flowId, path6, depth + 1));
15489
15928
  }
15490
15929
  }
15491
15930
  }
@@ -15495,13 +15934,13 @@ function validateBundle(input) {
15495
15934
  nodes.forEach((node, i) => {
15496
15935
  const md = node.metadata;
15497
15936
  if (node.species === "flow" && md?.playlist?.entries) {
15498
- const path = `nodes[${i}].metadata.playlist`;
15937
+ const path6 = `nodes[${i}].metadata.playlist`;
15499
15938
  const nodeId = node.id;
15500
- const refs = collectPlaylistRefs(md.playlist.entries, nodeId, path);
15939
+ const refs = collectPlaylistRefs(md.playlist.entries, nodeId, path6);
15501
15940
  for (const ref of refs) {
15502
15941
  if (!composesSet.has(`${nodeId}->${ref}`)) {
15503
15942
  error51(
15504
- path,
15943
+ path6,
15505
15944
  "playlist-composes-coherence",
15506
15945
  `Flow ${nodeId}: playlist references "${ref}" but no composes edge exists`
15507
15946
  );
@@ -15695,6 +16134,9 @@ function eventAffectsPlatform(ev, platform, nodesById) {
15695
16134
  const edge = ev;
15696
16135
  return onPlatform(edge.source_id) || onPlatform(edge.target_id);
15697
16136
  }
16137
+ if (ev.type === "deliverable.shipped" && Array.isArray(ev.node_ids)) {
16138
+ return ev.node_ids.some(onPlatform);
16139
+ }
15698
16140
  return false;
15699
16141
  }
15700
16142
  function computeChangelog(events, toVersion, options = {}) {
@@ -15708,7 +16150,7 @@ function computeChangelog(events, toVersion, options = {}) {
15708
16150
  }
15709
16151
  }
15710
16152
  if (toIndex === -1) {
15711
- return { fromVersion: null, toVersion, events: [] };
16153
+ return { fromVersion: null, toVersion, events: [], deliverables: [] };
15712
16154
  }
15713
16155
  const platform = asString(ordered[toIndex].platform);
15714
16156
  let fromIndex = -1;
@@ -15737,9 +16179,65 @@ function computeChangelog(events, toVersion, options = {}) {
15737
16179
  fromVersion: fromIndex >= 0 ? asString(ordered[fromIndex].version) ?? null : null,
15738
16180
  toVersion,
15739
16181
  ...platform ? { platform } : {},
15740
- events: slice
16182
+ events: slice,
16183
+ deliverables: computeDeliverables(ordered).filter((d) => d.releaseVersion === toVersion)
15741
16184
  };
15742
16185
  }
16186
+ function computeDeliverables(events) {
16187
+ const ordered = orderEvents(events);
16188
+ const firstIndex = /* @__PURE__ */ new Map();
16189
+ const latest = /* @__PURE__ */ new Map();
16190
+ ordered.forEach((ev, i) => {
16191
+ if (ev.type !== "deliverable.shipped") return;
16192
+ const id = asString(ev.deliverable_id);
16193
+ if (id === void 0) return;
16194
+ if (!firstIndex.has(id)) firstIndex.set(id, i);
16195
+ latest.set(id, ev);
16196
+ });
16197
+ if (firstIndex.size === 0) return [];
16198
+ const markerLatest = /* @__PURE__ */ new Map();
16199
+ ordered.forEach((ev, i) => {
16200
+ if (ev.type !== "release.tagged") return;
16201
+ const version2 = asString(ev.version);
16202
+ if (version2 !== void 0) markerLatest.set(version2, i);
16203
+ });
16204
+ const windows = [];
16205
+ for (const [version2, toIndex] of markerLatest) {
16206
+ let fromIndex = -1;
16207
+ for (let i = toIndex - 1; i >= 0; i -= 1) {
16208
+ if (ordered[i].type === "release.tagged") {
16209
+ fromIndex = i;
16210
+ break;
16211
+ }
16212
+ }
16213
+ windows.push({ version: version2, from: fromIndex, to: toIndex });
16214
+ }
16215
+ const releaseOf = (index) => {
16216
+ for (const w of windows) {
16217
+ if (index > w.from && index < w.to) return w.version;
16218
+ }
16219
+ return null;
16220
+ };
16221
+ const out = [];
16222
+ for (const [id, anchorIndex] of firstIndex) {
16223
+ const ev = latest.get(id);
16224
+ if (ev === void 0) continue;
16225
+ const summary = asString(ev.summary);
16226
+ const url2 = asString(ev.url);
16227
+ const platform = asString(ev.platform);
16228
+ out.push({
16229
+ deliverable_id: id,
16230
+ title: asString(ev.title) ?? id,
16231
+ ...summary !== void 0 ? { summary } : {},
16232
+ ...url2 !== void 0 ? { url: url2 } : {},
16233
+ node_ids: Array.isArray(ev.node_ids) ? ev.node_ids.filter((n) => typeof n === "string") : [],
16234
+ ...platform !== void 0 ? { platform } : {},
16235
+ ts: asString(ordered[anchorIndex].ts) ?? "",
16236
+ releaseVersion: releaseOf(anchorIndex)
16237
+ });
16238
+ }
16239
+ return out;
16240
+ }
15743
16241
 
15744
16242
  // ../schema/src/emit.ts
15745
16243
  var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -15831,6 +16329,16 @@ function computeRefPromotions(bundle) {
15831
16329
  continue;
15832
16330
  }
15833
16331
  if (target === null) continue;
16332
+ const to = normalizeStatus(target);
16333
+ if (to === void 0) {
16334
+ skipped.push({
16335
+ node_id: node.id,
16336
+ ref_id: ref.id,
16337
+ reason: "no-mapping",
16338
+ detail: `unknown status: ${target}`
16339
+ });
16340
+ continue;
16341
+ }
15834
16342
  if (node.status === "archived") {
15835
16343
  skipped.push({ node_id: node.id, ref_id: ref.id, reason: "archived" });
15836
16344
  continue;
@@ -15845,8 +16353,8 @@ function computeRefPromotions(bundle) {
15845
16353
  continue;
15846
16354
  }
15847
16355
  const from = currentStatus(node, ref.platform);
15848
- if (from === target) {
15849
- skipped.push({ node_id: node.id, ref_id: ref.id, reason: "already-there", detail: target });
16356
+ if (from === to) {
16357
+ skipped.push({ node_id: node.id, ref_id: ref.id, reason: "already-there", detail: to });
15850
16358
  continue;
15851
16359
  }
15852
16360
  promotions.push({
@@ -15854,7 +16362,7 @@ function computeRefPromotions(bundle) {
15854
16362
  ref_id: ref.id,
15855
16363
  ...ref.platform ? { platform: ref.platform } : {},
15856
16364
  from,
15857
- to: target,
16365
+ to,
15858
16366
  external_status: ref.external_status
15859
16367
  });
15860
16368
  }
@@ -15876,7 +16384,8 @@ var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
15876
16384
  var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
15877
16385
  var DEFAULT_SKILLS_DIR = ".claude/skills/arkaik";
15878
16386
  var ASSET_DIR = join(dirname(fileURLToPath(import.meta.url)), "assets", "skill");
15879
- var USAGE = `arkaik init [--product <name>] [--bundle <path>] [--journal <path>] [--skills-dir <path>] [--update]
16387
+ var BOOTSTRAP_ASSET_DIR = join(dirname(fileURLToPath(import.meta.url)), "assets", "bootstrap-skill");
16388
+ var USAGE = `arkaik init [--product <name>] [--bundle <path>] [--journal <path>] [--skills-dir <path>] [--update] [--bootstrap] [--remove-bootstrap]
15880
16389
 
15881
16390
  Scaffold docs/arkaik/ (bundle.json, journal.jsonl, assets/) in the current
15882
16391
  directory, configure .gitattributes for the journal's union merge, and
@@ -15902,14 +16411,22 @@ Options:
15902
16411
  (and skip references/values.md). Applies when the
15903
16412
  skill is installed or upgraded; a same-version
15904
16413
  \`--update\` run is a no-op.
16414
+ --bootstrap Also install the one-time \`arkaik-bootstrap\` skill
16415
+ beside the arkaik skill. Not installed by default: it
16416
+ is a large skill for a one-time job (bootstrapping
16417
+ the map from repo history). Install-if-absent with a
16418
+ plain init; version-gated upgrade with --update.
16419
+ --remove-bootstrap Remove the installed \`arkaik-bootstrap\` skill and do
16420
+ nothing else \u2014 no scaffolding. Prints a notice and
16421
+ exits 0 when it isn't installed.
15905
16422
  -h, --help Show this help.`;
15906
16423
  function fail(message) {
15907
16424
  console.error(message);
15908
16425
  process.exit(1);
15909
16426
  }
15910
16427
  function parseArgs(args) {
15911
- const opts = { update: false, noValues: false };
15912
- const nextValue = (i, flag) => {
16428
+ const opts = { update: false, noValues: false, bootstrap: false, removeBootstrap: false };
16429
+ const nextValue2 = (i, flag) => {
15913
16430
  const value = args[i];
15914
16431
  if (value === void 0) fail(`Missing value for ${flag}
15915
16432
 
@@ -15925,20 +16442,29 @@ ${USAGE}`);
15925
16442
  opts.update = true;
15926
16443
  } else if (arg === "--no-values") {
15927
16444
  opts.noValues = true;
16445
+ } else if (arg === "--bootstrap") {
16446
+ opts.bootstrap = true;
16447
+ } else if (arg === "--remove-bootstrap") {
16448
+ opts.removeBootstrap = true;
15928
16449
  } else if (arg === "--product") {
15929
- opts.product = nextValue(++i, arg);
16450
+ opts.product = nextValue2(++i, arg);
15930
16451
  } else if (arg === "--bundle") {
15931
- opts.bundle = nextValue(++i, arg);
16452
+ opts.bundle = nextValue2(++i, arg);
15932
16453
  } else if (arg === "--journal") {
15933
- opts.journal = nextValue(++i, arg);
16454
+ opts.journal = nextValue2(++i, arg);
15934
16455
  } else if (arg === "--skills-dir") {
15935
- opts.skillsDir = nextValue(++i, arg);
16456
+ opts.skillsDir = nextValue2(++i, arg);
15936
16457
  } else {
15937
16458
  fail(`Unknown option: ${arg}
15938
16459
 
15939
16460
  ${USAGE}`);
15940
16461
  }
15941
16462
  }
16463
+ if (opts.bootstrap && opts.removeBootstrap) {
16464
+ fail(`--bootstrap and --remove-bootstrap are contradictory; pass one or the other.
16465
+
16466
+ ${USAGE}`);
16467
+ }
15942
16468
  return opts;
15943
16469
  }
15944
16470
  function defaultProductName() {
@@ -15950,14 +16476,14 @@ function kebabCase(input) {
15950
16476
  const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15951
16477
  return slug || "project";
15952
16478
  }
15953
- function writeIfAbsent(path, content, label) {
15954
- if (existsSync(path)) {
15955
- console.log(`Skipping ${label} (already exists): ${path}`);
16479
+ function writeIfAbsent(path6, content, label) {
16480
+ if (existsSync(path6)) {
16481
+ console.log(`Skipping ${label} (already exists): ${path6}`);
15956
16482
  return;
15957
16483
  }
15958
- mkdirSync(dirname(path), { recursive: true });
15959
- writeFileSync(path, content);
15960
- console.log(`Created ${path}`);
16484
+ mkdirSync(dirname(path6), { recursive: true });
16485
+ writeFileSync(path6, content);
16486
+ console.log(`Created ${path6}`);
15961
16487
  }
15962
16488
  function scaffoldBundle(bundlePath, productName) {
15963
16489
  if (existsSync(bundlePath)) {
@@ -16068,6 +16594,49 @@ function updateSkill(skillsDirPath, vars, noValues) {
16068
16594
  const version2 = renderAndWriteSkill(skillsDirPath, vars, noValues);
16069
16595
  console.log(`Upgraded skill v${installedVersion ?? "unknown"} -> v${version2}.`);
16070
16596
  }
16597
+ function renderAndWriteBootstrapSkill(bootstrapDirPath, vars) {
16598
+ const rawSkill = readFileSync(join(BOOTSTRAP_ASSET_DIR, "skill.md"), "utf8");
16599
+ mkdirSync(join(bootstrapDirPath, "references"), { recursive: true });
16600
+ writeFileSync(join(bootstrapDirPath, "SKILL.md"), renderTemplate(rawSkill, vars));
16601
+ copyFileSync(join(BOOTSTRAP_ASSET_DIR, "references", "fragments.md"), join(bootstrapDirPath, "references", "fragments.md"));
16602
+ copyFileSync(join(BOOTSTRAP_ASSET_DIR, "references", "waves.md"), join(bootstrapDirPath, "references", "waves.md"));
16603
+ return extractVersion(rawSkill) ?? "unknown";
16604
+ }
16605
+ function installBootstrapSkill(bootstrapDirPath, vars) {
16606
+ const skillPath = join(bootstrapDirPath, "SKILL.md");
16607
+ if (existsSync(skillPath)) {
16608
+ console.log(
16609
+ `Skipping bootstrap skill install (already exists): ${skillPath}. Use \`arkaik init --update --bootstrap\` to upgrade.`
16610
+ );
16611
+ return;
16612
+ }
16613
+ const version2 = renderAndWriteBootstrapSkill(bootstrapDirPath, vars);
16614
+ console.log(`Installed bootstrap skill v${version2} -> ${skillPath}`);
16615
+ }
16616
+ function updateBootstrapSkill(bootstrapDirPath, vars) {
16617
+ const packagedVersion = extractVersion(readFileSync(join(BOOTSTRAP_ASSET_DIR, "skill.md"), "utf8"));
16618
+ const skillPath = join(bootstrapDirPath, "SKILL.md");
16619
+ if (!existsSync(skillPath)) {
16620
+ const version3 = renderAndWriteBootstrapSkill(bootstrapDirPath, vars);
16621
+ console.log(`No existing bootstrap skill found at ${skillPath}; installed v${version3}.`);
16622
+ return;
16623
+ }
16624
+ const installedVersion = extractVersion(readFileSync(skillPath, "utf8"));
16625
+ if (installedVersion !== void 0 && packagedVersion !== void 0 && compareVersions(packagedVersion, installedVersion) <= 0) {
16626
+ console.log(`Bootstrap skill already up to date (v${installedVersion}).`);
16627
+ return;
16628
+ }
16629
+ const version2 = renderAndWriteBootstrapSkill(bootstrapDirPath, vars);
16630
+ console.log(`Upgraded bootstrap skill v${installedVersion ?? "unknown"} -> v${version2}.`);
16631
+ }
16632
+ function removeBootstrapSkill(bootstrapDirPath) {
16633
+ if (!existsSync(bootstrapDirPath)) {
16634
+ console.log(`No bootstrap skill installed at ${bootstrapDirPath}; nothing to remove.`);
16635
+ return;
16636
+ }
16637
+ rmSync(bootstrapDirPath, { recursive: true, force: true });
16638
+ console.log(`Removed bootstrap skill: ${bootstrapDirPath}`);
16639
+ }
16071
16640
  function runInit(args) {
16072
16641
  const opts = parseArgs(args);
16073
16642
  const productName = opts.product ?? defaultProductName();
@@ -16078,6 +16647,11 @@ function runInit(args) {
16078
16647
  const bundlePath = resolve(cwd, bundleRelPath);
16079
16648
  const journalPath = resolve(cwd, journalRelPath);
16080
16649
  const skillsDirPath = resolve(cwd, skillsDirRelPath);
16650
+ const bootstrapSkillDirPath = join(dirname(skillsDirPath), "arkaik-bootstrap");
16651
+ if (opts.removeBootstrap) {
16652
+ removeBootstrapSkill(bootstrapSkillDirPath);
16653
+ return;
16654
+ }
16081
16655
  const vars = {
16082
16656
  PRODUCT_NAME: productName,
16083
16657
  PROJECT_ID: kebabCase(productName),
@@ -16086,6 +16660,7 @@ function runInit(args) {
16086
16660
  };
16087
16661
  if (opts.update) {
16088
16662
  updateSkill(skillsDirPath, vars, opts.noValues);
16663
+ if (opts.bootstrap) updateBootstrapSkill(bootstrapSkillDirPath, vars);
16089
16664
  return;
16090
16665
  }
16091
16666
  scaffoldBundle(bundlePath, productName);
@@ -16093,6 +16668,7 @@ function runInit(args) {
16093
16668
  writeIfAbsent(join(dirname(bundlePath), "assets", ".gitkeep"), "", "assets dir");
16094
16669
  ensureGitAttributes(journalRelPath);
16095
16670
  installSkill(skillsDirPath, vars, opts.noValues);
16671
+ if (opts.bootstrap) installBootstrapSkill(bootstrapSkillDirPath, vars);
16096
16672
  }
16097
16673
 
16098
16674
  // src/commands/validate.ts
@@ -16111,7 +16687,9 @@ function readBundle(filePath) {
16111
16687
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16112
16688
  throw new Error("Bundle must be a JSON object.");
16113
16689
  }
16114
- return parsed;
16690
+ const record2 = parsed;
16691
+ if (!Array.isArray(record2.nodes)) return record2;
16692
+ return migrateStatusVocabulary(record2);
16115
16693
  }
16116
16694
  function nodesByIdOf(bundle) {
16117
16695
  const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
@@ -16358,6 +16936,10 @@ function renderEventLine(event, nodesById) {
16358
16936
  const platform = str(event.platform);
16359
16937
  return `Released ${version2}${platform ? ` [${platform}]` : ""}`;
16360
16938
  }
16939
+ case "deliverable.shipped": {
16940
+ const url2 = str(event.url);
16941
+ return `Shipped: ${str(event.title) ?? str(event.deliverable_id) ?? "?"}${url2 ? ` (${url2})` : ""}`;
16942
+ }
16361
16943
  case "idea.proposed":
16362
16944
  return `Idea: ${str(event.title) ?? "Untitled"}`;
16363
16945
  case "request.filed": {
@@ -16590,10 +17172,19 @@ ${USAGE4}`);
16590
17172
  ` : ` Initial changes:
16591
17173
  `
16592
17174
  );
16593
- if (changelog.events.length === 0) {
16594
- console.log(" (no changes in this release)");
17175
+ const draftDeliverables = changelog.platform ? changelog.deliverables.filter((d) => d.platform === void 0 || d.platform === changelog.platform) : changelog.deliverables;
17176
+ if (draftDeliverables.length > 0) {
17177
+ console.log(" Deliverables:");
17178
+ draftDeliverables.forEach((d) => {
17179
+ console.log(` - ${d.title}${d.summary ? ` \u2014 ${d.summary}` : ""}${d.url ? ` (${d.url})` : ""}`);
17180
+ });
17181
+ console.log("");
17182
+ }
17183
+ const draftEvents = draftDeliverables.length > 0 ? changelog.events.filter((ev) => ev.type !== "deliverable.shipped") : changelog.events;
17184
+ if (draftEvents.length === 0) {
17185
+ console.log(draftDeliverables.length > 0 ? " (no other events in this release)" : " (no changes in this release)");
16595
17186
  } else {
16596
- changelog.events.forEach((ev) => console.log(` - ${renderEventLine(ev, nodesById)}`));
17187
+ draftEvents.forEach((ev) => console.log(` - ${renderEventLine(ev, nodesById)}`));
16597
17188
  }
16598
17189
  console.log("");
16599
17190
  if (compact) {
@@ -16611,56 +17202,180 @@ ${USAGE4}`);
16611
17202
  process.exit(0);
16612
17203
  }
16613
17204
 
16614
- // src/commands/sync.ts
16615
- import { resolve as resolve2 } from "node:path";
16616
- import { writeFileSync as writeFileSync4 } from "node:fs";
17205
+ // src/commands/deliverable.ts
17206
+ var DEFAULT_BUNDLE_PATH3 = "docs/arkaik/bundle.json";
17207
+ var ACTOR2 = "arkaik-cli";
17208
+ var USAGE5 = `arkaik deliverable <title> [options] [path]
16617
17209
 
16618
- // src/lib/providers.ts
16619
- var DEFAULT_HTTP_CLIENT = (url2, init) => fetch(url2, init);
16620
- var GITHUB_ISSUE_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:[/?#].*)?$/;
16621
- var GITHUB_PR_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#].*)?$/;
16622
- function githubHeaders(token) {
16623
- const headers = {
16624
- Accept: "application/vnd.github+json",
16625
- "X-GitHub-Api-Version": "2022-11-28"
16626
- };
16627
- if (token) headers.Authorization = `Bearer ${token}`;
16628
- return headers;
16629
- }
16630
- async function getGithubJson(httpClient, url2, token, refUrl) {
16631
- const res = await httpClient(url2, { headers: githubHeaders(token) });
16632
- if (!res.ok) throw new Error(`GitHub API error ${res.status} for ${refUrl}`);
16633
- const body = await res.json();
16634
- if (typeof body !== "object" || body === null) {
16635
- throw new Error(`GitHub API returned a non-object response for ${refUrl}`);
16636
- }
16637
- return body;
16638
- }
16639
- async function fetchGithubIssueStatus(ref, ctx) {
16640
- const m = GITHUB_ISSUE_URL_RE.exec(ref.url);
16641
- if (!m) throw new Error(`Cannot parse a GitHub issue URL: ${ref.url}`);
16642
- const [, owner, repo, number4] = m;
16643
- const body = await getGithubJson(
16644
- ctx.httpClient,
16645
- `https://api.github.com/repos/${owner}/${repo}/issues/${number4}`,
16646
- ctx.token,
16647
- ref.url
16648
- );
16649
- if (typeof body.state !== "string") throw new Error(`GitHub API response missing "state" for ${ref.url}`);
16650
- return body.state;
17210
+ Record a deliverable: append a validated deliverable.shipped event to the
17211
+ journal.jsonl sidecar. The bundle file is not modified.
17212
+
17213
+ Arguments:
17214
+ title What shipped, in one line. Required.
17215
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH3}).
17216
+
17217
+ Options:
17218
+ --id <id> Stable deliverable id (convention: pr-123). Re-appending
17219
+ with the same id edits the deliverable (latest-wins
17220
+ content, anchored at the first occurrence). Default: a
17221
+ fresh ULID.
17222
+ --summary <s> A short human note.
17223
+ --url <u> The PR (or other reference) URL.
17224
+ --nodes <id,id> Comma-separated ids of graph nodes this touched. Checked
17225
+ against the snapshot.
17226
+ --platform <p> Scope to a platform's release rhythm.
17227
+ -h, --help Show this help.`;
17228
+ function fail5(message) {
17229
+ console.error(message);
17230
+ process.exit(1);
16651
17231
  }
16652
- async function fetchGithubPrStatus(ref, ctx) {
16653
- const m = GITHUB_PR_URL_RE.exec(ref.url);
16654
- if (!m) throw new Error(`Cannot parse a GitHub pull request URL: ${ref.url}`);
16655
- const [, owner, repo, number4] = m;
16656
- const body = await getGithubJson(
16657
- ctx.httpClient,
16658
- `https://api.github.com/repos/${owner}/${repo}/pulls/${number4}`,
16659
- ctx.token,
16660
- ref.url
16661
- );
16662
- if (typeof body.state !== "string") throw new Error(`GitHub API response missing "state" for ${ref.url}`);
16663
- return body.merged === true ? "merged" : body.state;
17232
+ function runDeliverable(args) {
17233
+ let id;
17234
+ let summary;
17235
+ let url2;
17236
+ let nodes;
17237
+ let platform;
17238
+ const positionals = [];
17239
+ for (let i = 0; i < args.length; i++) {
17240
+ const arg = args[i];
17241
+ if (arg === "-h" || arg === "--help") {
17242
+ console.log(USAGE5);
17243
+ process.exit(0);
17244
+ } else if (arg === "--id") {
17245
+ const value = args[++i];
17246
+ if (value === void 0) fail5(`Missing value for --id
17247
+
17248
+ ${USAGE5}`);
17249
+ id = value;
17250
+ } else if (arg === "--summary") {
17251
+ const value = args[++i];
17252
+ if (value === void 0) fail5(`Missing value for --summary
17253
+
17254
+ ${USAGE5}`);
17255
+ summary = value;
17256
+ } else if (arg === "--url") {
17257
+ const value = args[++i];
17258
+ if (value === void 0) fail5(`Missing value for --url
17259
+
17260
+ ${USAGE5}`);
17261
+ url2 = value;
17262
+ } else if (arg === "--nodes") {
17263
+ const value = args[++i];
17264
+ if (value === void 0) fail5(`Missing value for --nodes
17265
+
17266
+ ${USAGE5}`);
17267
+ nodes = value.split(",").map((n) => n.trim()).filter((n) => n !== "");
17268
+ if (nodes.length === 0) nodes = void 0;
17269
+ } else if (arg === "--platform") {
17270
+ const value = args[++i];
17271
+ if (value === void 0) fail5(`Missing value for --platform
17272
+
17273
+ ${USAGE5}`);
17274
+ platform = value;
17275
+ } else if (arg.startsWith("-")) {
17276
+ fail5(`Unknown option: ${arg}
17277
+
17278
+ ${USAGE5}`);
17279
+ } else {
17280
+ positionals.push(arg);
17281
+ }
17282
+ }
17283
+ const title2 = positionals[0];
17284
+ if (title2 === void 0) fail5(`Missing title.
17285
+
17286
+ ${USAGE5}`);
17287
+ const filePath = positionals[1] ?? DEFAULT_BUNDLE_PATH3;
17288
+ let bundle;
17289
+ try {
17290
+ bundle = readBundle(filePath);
17291
+ } catch (e) {
17292
+ fail5(`FATAL: ${e.message}`);
17293
+ }
17294
+ if (nodes !== void 0) {
17295
+ const nodesById = nodesByIdOf(bundle);
17296
+ const unknown2 = nodes.filter((n) => !nodesById.has(n));
17297
+ if (unknown2.length > 0) {
17298
+ fail5(`FATAL: --nodes references unknown node id(s): ${unknown2.join(", ")}`);
17299
+ }
17300
+ }
17301
+ const deliverableId = id ?? ulid3();
17302
+ let event;
17303
+ try {
17304
+ event = makeEvent(
17305
+ "deliverable.shipped",
17306
+ {
17307
+ deliverable_id: deliverableId,
17308
+ title: title2,
17309
+ ...summary !== void 0 ? { summary } : {},
17310
+ ...url2 !== void 0 ? { url: url2 } : {},
17311
+ ...nodes !== void 0 ? { node_ids: nodes } : {},
17312
+ ...platform !== void 0 ? { platform } : {}
17313
+ },
17314
+ { actor: ACTOR2 }
17315
+ );
17316
+ } catch (e) {
17317
+ fail5(`FATAL: could not build deliverable event \u2014 ${e.message}`);
17318
+ }
17319
+ const journalPath = journalPathFor(filePath);
17320
+ appendJournalEvent(journalPath, event);
17321
+ console.log(
17322
+ `
17323
+ Recorded deliverable ${deliverableId} \u2014 ${title2} -> ${journalPath}
17324
+ `
17325
+ );
17326
+ process.exit(0);
17327
+ }
17328
+
17329
+ // src/commands/sync.ts
17330
+ import { resolve as resolve2 } from "node:path";
17331
+ import { writeFileSync as writeFileSync4 } from "node:fs";
17332
+
17333
+ // src/lib/providers.ts
17334
+ var DEFAULT_HTTP_CLIENT = (url2, init) => fetch(url2, init);
17335
+ var GITHUB_ISSUE_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:[/?#].*)?$/;
17336
+ var GITHUB_PR_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#].*)?$/;
17337
+ function githubHeaders(token) {
17338
+ const headers = {
17339
+ Accept: "application/vnd.github+json",
17340
+ "X-GitHub-Api-Version": "2022-11-28"
17341
+ };
17342
+ if (token) headers.Authorization = `Bearer ${token}`;
17343
+ return headers;
17344
+ }
17345
+ async function getGithubJson(httpClient, url2, token, refUrl) {
17346
+ const res = await httpClient(url2, { headers: githubHeaders(token) });
17347
+ if (!res.ok) throw new Error(`GitHub API error ${res.status} for ${refUrl}`);
17348
+ const body = await res.json();
17349
+ if (typeof body !== "object" || body === null) {
17350
+ throw new Error(`GitHub API returned a non-object response for ${refUrl}`);
17351
+ }
17352
+ return body;
17353
+ }
17354
+ async function fetchGithubIssueStatus(ref, ctx) {
17355
+ const m = GITHUB_ISSUE_URL_RE.exec(ref.url);
17356
+ if (!m) throw new Error(`Cannot parse a GitHub issue URL: ${ref.url}`);
17357
+ const [, owner, repo, number4] = m;
17358
+ const body = await getGithubJson(
17359
+ ctx.httpClient,
17360
+ `https://api.github.com/repos/${owner}/${repo}/issues/${number4}`,
17361
+ ctx.token,
17362
+ ref.url
17363
+ );
17364
+ if (typeof body.state !== "string") throw new Error(`GitHub API response missing "state" for ${ref.url}`);
17365
+ return body.state;
17366
+ }
17367
+ async function fetchGithubPrStatus(ref, ctx) {
17368
+ const m = GITHUB_PR_URL_RE.exec(ref.url);
17369
+ if (!m) throw new Error(`Cannot parse a GitHub pull request URL: ${ref.url}`);
17370
+ const [, owner, repo, number4] = m;
17371
+ const body = await getGithubJson(
17372
+ ctx.httpClient,
17373
+ `https://api.github.com/repos/${owner}/${repo}/pulls/${number4}`,
17374
+ ctx.token,
17375
+ ref.url
17376
+ );
17377
+ if (typeof body.state !== "string") throw new Error(`GitHub API response missing "state" for ${ref.url}`);
17378
+ return body.merged === true ? "merged" : body.state;
16664
17379
  }
16665
17380
  async function fetchGithubStatus(ref, ctx) {
16666
17381
  if (ref.type === "github-issue") return fetchGithubIssueStatus(ref, ctx);
@@ -16704,15 +17419,15 @@ async function fetchRefStatus(ref, ctx) {
16704
17419
  }
16705
17420
 
16706
17421
  // src/commands/sync.ts
16707
- var DEFAULT_BUNDLE_PATH3 = "docs/arkaik/bundle.json";
16708
- var ACTOR2 = "arkaik-cli";
17422
+ var DEFAULT_BUNDLE_PATH4 = "docs/arkaik/bundle.json";
17423
+ var ACTOR3 = "arkaik-cli";
16709
17424
  var PROVIDER_LINES = PROVIDERS.map((p) => {
16710
17425
  const state = p.status === "live" ? "live " : "stub ";
16711
17426
  const token = p.tokenEnvVar ? ` (token: ${p.tokenEnvVar})` : "";
16712
17427
  const note = p.status === "stub" ? " \u2014 not yet implemented, refs are reported and skipped" : "";
16713
17428
  return ` ${p.name.padEnd(8)} ${state}\u2014 ${p.refTypes.join(", ")}${token}${note}`;
16714
17429
  }).join("\n");
16715
- var USAGE5 = `arkaik sync [--provider <name>] [--dry-run] [--promote] [path]
17430
+ var USAGE6 = `arkaik sync [--provider <name>] [--dry-run] [--promote] [path]
16716
17431
 
16717
17432
  Mirror external ref status into node metadata.refs. Reads every node's
16718
17433
  metadata.refs, queries each ref's provider for its current external status,
@@ -16728,7 +17443,7 @@ Providers (v1):
16728
17443
  ${PROVIDER_LINES}
16729
17444
 
16730
17445
  Arguments:
16731
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH3}).
17446
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH4}).
16732
17447
 
16733
17448
  Options:
16734
17449
  --provider <name> Only sync refs handled by this provider (${PROVIDERS.map((p) => p.name).join(" | ")}).
@@ -16738,7 +17453,7 @@ Options:
16738
17453
  --dry-run Report what would change without writing the bundle or
16739
17454
  appending to the journal.
16740
17455
  -h, --help Show this help.`;
16741
- function fail5(message) {
17456
+ function fail6(message) {
16742
17457
  console.error(message);
16743
17458
  process.exit(1);
16744
17459
  }
@@ -16759,14 +17474,14 @@ function fatalResult(bundlePath, journalPath, dryRun, message) {
16759
17474
  }
16760
17475
  async function runSync(options = {}) {
16761
17476
  const cwd = options.cwd ?? process.cwd();
16762
- const filePath = resolve2(cwd, options.path ?? DEFAULT_BUNDLE_PATH3);
17477
+ const filePath = resolve2(cwd, options.path ?? DEFAULT_BUNDLE_PATH4);
16763
17478
  const journalPath = journalPathFor(filePath);
16764
17479
  const dryRun = options.dryRun ?? false;
16765
17480
  const promote = options.promote ?? false;
16766
17481
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
16767
17482
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
16768
17483
  const env = options.env ?? process.env;
16769
- const actor = options.actor ?? ACTOR2;
17484
+ const actor = options.actor ?? ACTOR3;
16770
17485
  if (options.provider !== void 0 && !PROVIDERS.some((p) => p.name === options.provider)) {
16771
17486
  return fatalResult(
16772
17487
  filePath,
@@ -16925,7 +17640,7 @@ function runSyncCli(args) {
16925
17640
  for (let i = 0; i < args.length; i++) {
16926
17641
  const arg = args[i];
16927
17642
  if (arg === "-h" || arg === "--help") {
16928
- console.log(USAGE5);
17643
+ console.log(USAGE6);
16929
17644
  process.exit(0);
16930
17645
  } else if (arg === "--dry-run") {
16931
17646
  dryRun = true;
@@ -16933,31 +17648,31 @@ function runSyncCli(args) {
16933
17648
  promote = true;
16934
17649
  } else if (arg === "--provider") {
16935
17650
  const value = args[++i];
16936
- if (value === void 0) fail5(`Missing value for --provider
17651
+ if (value === void 0) fail6(`Missing value for --provider
16937
17652
 
16938
- ${USAGE5}`);
17653
+ ${USAGE6}`);
16939
17654
  provider = value;
16940
17655
  } else if (arg.startsWith("-")) {
16941
- fail5(`Unknown option: ${arg}
17656
+ fail6(`Unknown option: ${arg}
16942
17657
 
16943
- ${USAGE5}`);
17658
+ ${USAGE6}`);
16944
17659
  } else {
16945
17660
  positionals.push(arg);
16946
17661
  }
16947
17662
  }
16948
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH3;
17663
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH4;
16949
17664
  runSync({ path: filePath, provider, dryRun, promote }).then((result) => {
16950
- if (!result.ok) fail5(`FATAL: ${result.fatal}`);
17665
+ if (!result.ok) fail6(`FATAL: ${result.fatal}`);
16951
17666
  report(result);
16952
17667
  process.exit(result.errors.length > 0 ? 1 : 0);
16953
- }).catch((e) => fail5(`FATAL: ${e.message}`));
17668
+ }).catch((e) => fail6(`FATAL: ${e.message}`));
16954
17669
  }
16955
17670
 
16956
17671
  // src/commands/pack.ts
16957
17672
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
16958
17673
  import { dirname as dirname4, extname, resolve as resolve3 } from "node:path";
16959
- var DEFAULT_BUNDLE_PATH4 = "docs/arkaik/bundle.json";
16960
- var USAGE6 = `arkaik pack [--no-journal] [--inline-assets] [--out <path>] [path]
17674
+ var DEFAULT_BUNDLE_PATH5 = "docs/arkaik/bundle.json";
17675
+ var USAGE7 = `arkaik pack [--no-journal] [--inline-assets] [--out <path>] [path]
16961
17676
 
16962
17677
  Produce a single self-contained interchange bundle: fold in the sidecar
16963
17678
  journal (or keep an existing embedded one) and, with --inline-assets, inline
@@ -16965,7 +17680,7 @@ local screenshot files as data: URIs. Written canonically via serializeBundle.
16965
17680
  Unknown top-level keys and unknown fields always round-trip.
16966
17681
 
16967
17682
  Arguments:
16968
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH4}).
17683
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH5}).
16969
17684
 
16970
17685
  Options:
16971
17686
  --no-journal Omit the embedded journal[] (Publik-safe posture \u2014 history
@@ -16982,7 +17697,7 @@ Options:
16982
17697
  implemented.
16983
17698
  --out <path> Write the packed bundle here instead of stdout.
16984
17699
  -h, --help Show this help.`;
16985
- function fail6(message) {
17700
+ function fail7(message) {
16986
17701
  console.error(message);
16987
17702
  process.exit(1);
16988
17703
  }
@@ -17017,7 +17732,7 @@ function fatalResult2(bundlePath, message) {
17017
17732
  }
17018
17733
  function runPack(options = {}) {
17019
17734
  const cwd = options.cwd ?? process.cwd();
17020
- const filePath = resolve3(cwd, options.path ?? DEFAULT_BUNDLE_PATH4);
17735
+ const filePath = resolve3(cwd, options.path ?? DEFAULT_BUNDLE_PATH5);
17021
17736
  const noJournal = options.noJournal ?? false;
17022
17737
  const inlineAssets = options.inlineAssets ?? false;
17023
17738
  let bundle;
@@ -17081,7 +17796,7 @@ function runPackCli(args) {
17081
17796
  for (let i = 0; i < args.length; i++) {
17082
17797
  const arg = args[i];
17083
17798
  if (arg === "-h" || arg === "--help") {
17084
- console.log(USAGE6);
17799
+ console.log(USAGE7);
17085
17800
  process.exit(0);
17086
17801
  } else if (arg === "--no-journal") {
17087
17802
  noJournal = true;
@@ -17089,21 +17804,21 @@ function runPackCli(args) {
17089
17804
  inlineAssets = true;
17090
17805
  } else if (arg === "--out") {
17091
17806
  const value = args[++i];
17092
- if (value === void 0) fail6(`Missing value for --out
17807
+ if (value === void 0) fail7(`Missing value for --out
17093
17808
 
17094
- ${USAGE6}`);
17809
+ ${USAGE7}`);
17095
17810
  out = value;
17096
17811
  } else if (arg.startsWith("-")) {
17097
- fail6(`Unknown option: ${arg}
17812
+ fail7(`Unknown option: ${arg}
17098
17813
 
17099
- ${USAGE6}`);
17814
+ ${USAGE7}`);
17100
17815
  } else {
17101
17816
  positionals.push(arg);
17102
17817
  }
17103
17818
  }
17104
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH4;
17819
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH5;
17105
17820
  const result = runPack({ path: filePath, out, noJournal, inlineAssets });
17106
- if (!result.ok) fail6(`FATAL: ${result.fatal}`);
17821
+ if (!result.ok) fail7(`FATAL: ${result.fatal}`);
17107
17822
  if (result.journalIncluded) {
17108
17823
  console.error(`Journal: embedded ${result.journalEventCount} event(s)`);
17109
17824
  } else if (noJournal) {
@@ -17130,9 +17845,9 @@ import { spawn } from "node:child_process";
17130
17845
  import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
17131
17846
  import { tmpdir } from "node:os";
17132
17847
  import { join as join4, resolve as resolve4 } from "node:path";
17133
- var DEFAULT_BUNDLE_PATH5 = "docs/arkaik/bundle.json";
17848
+ var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
17134
17849
  var OPEN_URL = "https://arkaik.app/projects";
17135
- var USAGE7 = `arkaik open [--out <path>] [--no-open] [path]
17850
+ var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
17136
17851
 
17137
17852
  Validate the bundle (shape + semantic + snapshot<->journal cross-checks, same
17138
17853
  as "arkaik validate"), and only on success pack it and hand off to arkaik.app
@@ -17141,13 +17856,13 @@ ${OPEN_URL} (the project list's "Import JSON" picker). On an invalid bundle,
17141
17856
  findings are printed and nothing is packed, written, or opened.
17142
17857
 
17143
17858
  Arguments:
17144
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH5}).
17859
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
17145
17860
 
17146
17861
  Options:
17147
17862
  --out <path> Write the packed bundle here instead of a temp file.
17148
17863
  --no-open Skip launching the browser; still packs and reports the URL.
17149
17864
  -h, --help Show this help.`;
17150
- function fail7(message) {
17865
+ function fail8(message) {
17151
17866
  console.error(message);
17152
17867
  process.exit(1);
17153
17868
  }
@@ -17163,7 +17878,7 @@ function fatalResult3(bundlePath, message) {
17163
17878
  }
17164
17879
  async function runOpen(options = {}) {
17165
17880
  const cwd = options.cwd ?? process.cwd();
17166
- const filePath = resolve4(cwd, options.path ?? DEFAULT_BUNDLE_PATH5);
17881
+ const filePath = resolve4(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
17167
17882
  const noOpen = options.noOpen ?? false;
17168
17883
  let v;
17169
17884
  try {
@@ -17204,27 +17919,27 @@ function runOpenCli(args) {
17204
17919
  for (let i = 0; i < args.length; i++) {
17205
17920
  const arg = args[i];
17206
17921
  if (arg === "-h" || arg === "--help") {
17207
- console.log(USAGE7);
17922
+ console.log(USAGE8);
17208
17923
  process.exit(0);
17209
17924
  } else if (arg === "--no-open") {
17210
17925
  noOpen = true;
17211
17926
  } else if (arg === "--out") {
17212
17927
  const value = args[++i];
17213
- if (value === void 0) fail7(`Missing value for --out
17928
+ if (value === void 0) fail8(`Missing value for --out
17214
17929
 
17215
- ${USAGE7}`);
17930
+ ${USAGE8}`);
17216
17931
  out = value;
17217
17932
  } else if (arg.startsWith("-")) {
17218
- fail7(`Unknown option: ${arg}
17933
+ fail8(`Unknown option: ${arg}
17219
17934
 
17220
- ${USAGE7}`);
17935
+ ${USAGE8}`);
17221
17936
  } else {
17222
17937
  positionals.push(arg);
17223
17938
  }
17224
17939
  }
17225
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH5;
17940
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
17226
17941
  runOpen({ path: filePath, out, noOpen }).then((result) => {
17227
- if (!result.ok) fail7(`FATAL: ${result.fatal}`);
17942
+ if (!result.ok) fail8(`FATAL: ${result.fatal}`);
17228
17943
  if (result.warningLines.length > 0) {
17229
17944
  console.error(`Warnings: ${result.warningLines.length}`);
17230
17945
  result.warningLines.forEach((w) => console.error(` ${w}`));
@@ -17242,14 +17957,14 @@ ${USAGE7}`);
17242
17957
  console.log(`Import at ${result.url} (drag in ${result.outPath})`);
17243
17958
  }
17244
17959
  process.exit(0);
17245
- }).catch((e) => fail7(`FATAL: ${e.message}`));
17960
+ }).catch((e) => fail8(`FATAL: ${e.message}`));
17246
17961
  }
17247
17962
 
17248
17963
  // src/commands/push.ts
17249
17964
  import { resolve as resolve5 } from "node:path";
17250
- var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
17965
+ var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
17251
17966
  var DEFAULT_API_BASE = "https://arkaik.app";
17252
- var USAGE8 = `arkaik push [--include-journal] [--api <base-url>] [path]
17967
+ var USAGE9 = `arkaik push [--include-journal] [--api <base-url>] [path]
17253
17968
  arkaik push --delete <id> --key <owner_key> [--api <base-url>]
17254
17969
 
17255
17970
  Publish a project bundle to Publik (anonymous, account-less snapshot
@@ -17268,7 +17983,7 @@ a new id. The owner key printed on success is shown exactly once and cannot
17268
17983
  be recovered \u2014 save it if you may need to delete the snapshot later.
17269
17984
 
17270
17985
  Arguments:
17271
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
17986
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
17272
17987
  Ignored with --delete.
17273
17988
 
17274
17989
  Options:
@@ -17282,7 +17997,7 @@ Options:
17282
17997
  --key <owner_key> Owner key for --delete (from the original push's
17283
17998
  output).
17284
17999
  -h, --help Show this help.`;
17285
- function fail8(message) {
18000
+ function fail9(message) {
17286
18001
  console.error(message);
17287
18002
  process.exit(1);
17288
18003
  }
@@ -17299,7 +18014,7 @@ function fatalResult4(bundlePath, message) {
17299
18014
  }
17300
18015
  async function runPush(options = {}) {
17301
18016
  const cwd = options.cwd ?? process.cwd();
17302
- const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
18017
+ const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
17303
18018
  const includeJournal = options.includeJournal ?? false;
17304
18019
  const apiBase = options.apiBase ?? DEFAULT_API_BASE;
17305
18020
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
@@ -17450,7 +18165,7 @@ async function runPushDelete(options) {
17450
18165
  };
17451
18166
  }
17452
18167
  function reportDelete(id, result) {
17453
- if (!result.ok) fail8(`FATAL: ${result.fatal}`);
18168
+ if (!result.ok) fail9(`FATAL: ${result.fatal}`);
17454
18169
  if (result.deleted) {
17455
18170
  console.log(`Deleted ${id}`);
17456
18171
  process.exit(0);
@@ -17467,56 +18182,56 @@ function runPushCli(args) {
17467
18182
  for (let i = 0; i < args.length; i++) {
17468
18183
  const arg = args[i];
17469
18184
  if (arg === "-h" || arg === "--help") {
17470
- console.log(USAGE8);
18185
+ console.log(USAGE9);
17471
18186
  process.exit(0);
17472
18187
  } else if (arg === "--include-journal") {
17473
18188
  includeJournal = true;
17474
18189
  } else if (arg === "--api") {
17475
18190
  const value = args[++i];
17476
- if (value === void 0) fail8(`Missing value for --api
18191
+ if (value === void 0) fail9(`Missing value for --api
17477
18192
 
17478
- ${USAGE8}`);
18193
+ ${USAGE9}`);
17479
18194
  apiBase = value;
17480
18195
  } else if (arg === "--delete") {
17481
18196
  const value = args[++i];
17482
- if (value === void 0) fail8(`Missing value for --delete
18197
+ if (value === void 0) fail9(`Missing value for --delete
17483
18198
 
17484
- ${USAGE8}`);
18199
+ ${USAGE9}`);
17485
18200
  deleteId = value;
17486
18201
  } else if (arg === "--key") {
17487
18202
  const value = args[++i];
17488
- if (value === void 0) fail8(`Missing value for --key
18203
+ if (value === void 0) fail9(`Missing value for --key
17489
18204
 
17490
- ${USAGE8}`);
18205
+ ${USAGE9}`);
17491
18206
  key = value;
17492
18207
  } else if (arg.startsWith("-")) {
17493
- fail8(`Unknown option: ${arg}
18208
+ fail9(`Unknown option: ${arg}
17494
18209
 
17495
- ${USAGE8}`);
18210
+ ${USAGE9}`);
17496
18211
  } else {
17497
18212
  positionals.push(arg);
17498
18213
  }
17499
18214
  }
17500
18215
  if (deleteId !== void 0) {
17501
- if (key === void 0) fail8(`--delete requires --key <owner_key>
18216
+ if (key === void 0) fail9(`--delete requires --key <owner_key>
17502
18217
 
17503
- ${USAGE8}`);
18218
+ ${USAGE9}`);
17504
18219
  if (positionals.length > 0) {
17505
- fail8(`Unexpected argument(s) with --delete: ${positionals.join(" ")}
18220
+ fail9(`Unexpected argument(s) with --delete: ${positionals.join(" ")}
17506
18221
 
17507
- ${USAGE8}`);
18222
+ ${USAGE9}`);
17508
18223
  }
17509
- runPushDelete({ id: deleteId, key, apiBase }).then((result) => reportDelete(deleteId, result)).catch((e) => fail8(`FATAL: ${e.message}`));
18224
+ runPushDelete({ id: deleteId, key, apiBase }).then((result) => reportDelete(deleteId, result)).catch((e) => fail9(`FATAL: ${e.message}`));
17510
18225
  return;
17511
18226
  }
17512
- if (key !== void 0) fail8(`--key is only valid with --delete
18227
+ if (key !== void 0) fail9(`--key is only valid with --delete
17513
18228
 
17514
- ${USAGE8}`);
17515
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
18229
+ ${USAGE9}`);
18230
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
17516
18231
  runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
17517
- if (!result.ok) fail8(`FATAL: ${result.fatal}`);
18232
+ if (!result.ok) fail9(`FATAL: ${result.fatal}`);
17518
18233
  reportPush(result);
17519
- }).catch((e) => fail8(`FATAL: ${e.message}`));
18234
+ }).catch((e) => fail9(`FATAL: ${e.message}`));
17520
18235
  }
17521
18236
 
17522
18237
  // src/commands/link.ts
@@ -17524,7 +18239,7 @@ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync a
17524
18239
  import { dirname as dirname5, join as join5, resolve as resolve6 } from "node:path";
17525
18240
  var LINK_FILE = "docs/arkaik/arkaik.json";
17526
18241
  var DEFAULT_BASE_URL = "https://arkaik.app";
17527
- var USAGE9 = `arkaik link \u2014 point this repo at a hosted Arkaik project
18242
+ var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
17528
18243
 
17529
18244
  Usage:
17530
18245
  arkaik link --project <id> [--remote <url>] [path]
@@ -17552,7 +18267,7 @@ async function runLink(argv, options = {}) {
17552
18267
  const cwd = options.cwd ?? process.cwd();
17553
18268
  const doFetch = options.httpClient ?? ((...args) => fetch(...args));
17554
18269
  if (argv.includes("--help") || argv.includes("-h")) {
17555
- log(USAGE9);
18270
+ log(USAGE10);
17556
18271
  return { ok: true };
17557
18272
  }
17558
18273
  const baseUrl = (flagValue(argv, "--remote") ?? env.ARKAIK_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
@@ -17626,67 +18341,1767 @@ function runLinkCli(argv) {
17626
18341
  });
17627
18342
  }
17628
18343
 
17629
- // src/index.ts
17630
- var USAGE10 = `arkaik \u2014 CLI for Arkaik project bundles
18344
+ // src/commands/restore.ts
18345
+ import { existsSync as existsSync6, linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
18346
+ import { join as join6, resolve as resolve7 } from "node:path";
18347
+ var LINK_FILE2 = "docs/arkaik/arkaik.json";
18348
+ var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
18349
+ var DEFAULT_API_BASE2 = "https://arkaik.app";
18350
+ var USAGE11 = `arkaik restore [options] [path]
17631
18351
 
17632
- Usage:
17633
- arkaik <command> [options]
18352
+ Replace the linked hosted project's bundle AND journal with a local bundle \u2014
18353
+ the landing step for a bootstrapped map. Before sending anything, this
18354
+ exports the CURRENT hosted state (snapshot + journal) to
18355
+ docs/arkaik/.backups/<timestamp>-bundle.json (next to the link file \u2014 not
18356
+ wherever the local bundle happens to live) and refuses to proceed if that
18357
+ backup cannot be written \u2014 the server keeps no pre-image, so this file is the
18358
+ only way back if the restore turns out to be wrong.
17634
18359
 
17635
- Commands:
17636
- init [options] Scaffold docs/arkaik/, install the agent skill (--update to upgrade).
17637
- validate [path] Validate a project bundle (folds in a journal.jsonl sidecar).
17638
- log [--node <id>] [path] Print the journal: project changelog, or one node's timeline.
17639
- release <version> [path] Tag a release (append release.tagged) and draft its notes.
17640
- sync [options] [path] Mirror external ref status (GitHub issues/PRs) into node refs.
17641
- pack [options] [path] Produce a single self-contained interchange bundle (embeds the journal).
17642
- open [options] [path] Validate, then hand off the packed bundle to arkaik.app import.
17643
- push [options] [path] Validate, pack (journal stripped), and publish to Publik.
17644
- --delete <id> --key <owner_key> removes a snapshot.
17645
- link [options] [path] Point this repo at a hosted project so an agent can edit it.
17646
- --list shows the projects your token can reach.
18360
+ Arguments:
18361
+ path Path to the local bundle JSON file
18362
+ (default: ${DEFAULT_BUNDLE_PATH8}). Its journal.jsonl
18363
+ sidecar (or an embedded journal, which wins) is folded
18364
+ in automatically.
17647
18365
 
17648
18366
  Options:
17649
- -h, --help Show this help.
18367
+ --dry-run Ask the server what this restore WOULD do and print
18368
+ the delta. Sends nothing destructive and takes no
18369
+ backup (there is nothing to protect against in this
18370
+ mode).
18371
+ --allow-history-loss Proceed even though the outbound journal has fewer
18372
+ events than the hosted one currently holds. Without
18373
+ this flag, that shrink refuses outright \u2014 it usually
18374
+ means a missing/gitignored journal.jsonl or a bundle
18375
+ from the wrong directory, not an intended history
18376
+ rewrite.
18377
+ --api <base-url> Override the remote from docs/arkaik/arkaik.json
18378
+ (also overridable with $ARKAIK_URL).
18379
+ -h, --help Show this help.
17650
18380
 
17651
- Run "arkaik <command> --help" for command-specific help.`;
17652
- function main(argv) {
17653
- const [command, ...rest] = argv;
17654
- if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
17655
- console.log(USAGE10);
17656
- process.exit(0);
18381
+ Environment:
18382
+ ARKAIK_TOKEN Required. Create one at <origin>/settings/tokens.
18383
+ ARKAIK_URL Optional. Overrides the link file's remote (same as
18384
+ \`arkaik link\`); --api still wins over this.`;
18385
+ function fail10(message) {
18386
+ console.error(message);
18387
+ process.exit(1);
18388
+ }
18389
+ function fatalResult5(dryRun, message) {
18390
+ return { ok: false, fatal: message, dryRun, requestSent: false };
18391
+ }
18392
+ async function safeJson(res) {
18393
+ try {
18394
+ return await res.json();
18395
+ } catch {
18396
+ return {};
17657
18397
  }
17658
- switch (command) {
17659
- case "init":
17660
- runInit(rest);
17661
- return;
17662
- case "validate":
17663
- runValidate(rest);
17664
- return;
17665
- case "log":
17666
- runLog(rest);
17667
- return;
17668
- case "release":
17669
- runRelease(rest);
17670
- return;
17671
- case "sync":
17672
- runSyncCli(rest);
17673
- return;
17674
- case "pack":
17675
- runPackCli(rest);
17676
- return;
17677
- case "open":
17678
- runOpenCli(rest);
17679
- return;
17680
- case "push":
17681
- runPushCli(rest);
17682
- return;
17683
- case "link":
17684
- runLinkCli(rest);
18398
+ }
18399
+ function describeVersionReadFailure(status, baseUrl, projectId) {
18400
+ if (status === 401) return `Unauthorized \u2014 check ARKAIK_TOKEN (create one at ${baseUrl}/settings/tokens).`;
18401
+ if (status === 403) return `Forbidden \u2014 this token lacks the graph:read scope, or does not own project ${projectId}.`;
18402
+ if (status === 404) return `No project "${projectId}" in this account. Run \`arkaik link --list\` to see the ids.`;
18403
+ return `Could not read the project (${status}). Nothing was sent.`;
18404
+ }
18405
+ function backupNoteFor(backupPath) {
18406
+ if (!backupPath) return "";
18407
+ return ` Your pre-restore backup is at ${backupPath}. Undo this restore with: arkaik restore ${backupPath}`;
18408
+ }
18409
+ async function interpretPutResponse(res, ctx) {
18410
+ const base = {
18411
+ ok: true,
18412
+ dryRun: ctx.dryRun,
18413
+ bundlePath: ctx.bundlePath,
18414
+ backupPath: ctx.backupPath,
18415
+ requestSent: true,
18416
+ status: res.status
18417
+ };
18418
+ const backupNote = backupNoteFor(ctx.backupPath);
18419
+ if (res.status === 200) {
18420
+ const body2 = await safeJson(res);
18421
+ return {
18422
+ ...base,
18423
+ version: typeof body2.version === "string" ? body2.version : void 0,
18424
+ delta: body2.delta ?? void 0
18425
+ };
18426
+ }
18427
+ const body = await safeJson(res);
18428
+ switch (res.status) {
18429
+ case 404:
18430
+ return { ...base, errorMessage: `Project not found (or not owned by this token). Nothing was written.${backupNote}` };
18431
+ case 428:
18432
+ return {
18433
+ ...base,
18434
+ errorMessage: `The server rejected this request for a missing If-Match header \u2014 this CLI always sends one, so this points at a bug, not a version conflict. Nothing was written.${backupNote}`
18435
+ };
18436
+ case 400:
18437
+ if (body.error === "invalid_dry_run") {
18438
+ return {
18439
+ ...base,
18440
+ errorMessage: `The server rejected the dry-run indicator this CLI sent \u2014 this points at a bug, not a version conflict. Nothing was written.${backupNote}`
18441
+ };
18442
+ }
18443
+ return {
18444
+ ...base,
18445
+ errorMessage: `The server rejected If-Match as malformed \u2014 this points at a bug, not a version conflict. Nothing was written.${backupNote}`
18446
+ };
18447
+ case 412: {
18448
+ const current = typeof body.current === "string" ? body.current : void 0;
18449
+ return {
18450
+ ...base,
18451
+ conflictCurrent: current,
18452
+ errorMessage: `Conflict \u2014 the hosted project changed since this run read version ${ctx.version} (it is now ${current ?? "unknown"}). Nothing was written.${backupNote} Do not retry with the same version \u2014 that would overwrite state that has since moved. Re-run \`arkaik restore\` to read the current state and decide again.`
18453
+ };
18454
+ }
18455
+ case 403:
18456
+ return {
18457
+ ...base,
18458
+ errorMessage: `This bundle exceeds the hosted tier's entity limit (limit ${body.limit ?? "uncapped"}, actual ${body.actual ?? "unknown"}, tier ${body.tier ?? "unknown"}). Nothing was written.${backupNote}`
18459
+ };
18460
+ case 413:
18461
+ return {
18462
+ ...base,
18463
+ errorMessage: `The bundle is too large for the server to accept (limit ${body.limit ?? "5MB"} bytes). Nothing was written.${backupNote}`
18464
+ };
18465
+ case 422:
18466
+ return {
18467
+ ...base,
18468
+ serverFindings: Array.isArray(body.errors) ? body.errors : void 0,
18469
+ errorMessage: `The server's validator rejected the bundle \u2014 see the findings below. Nothing was written.${backupNote}`
18470
+ };
18471
+ default:
18472
+ return {
18473
+ ...base,
18474
+ errorMessage: `Restore failed (${res.status}): ${typeof body.message === "string" ? body.message : "unknown error"}.${backupNote}`
18475
+ };
18476
+ }
18477
+ }
18478
+ function writeBackupFile(filePath, content) {
18479
+ const tmpPath = `${filePath}.tmp-${process.pid}`;
18480
+ writeFileSync8(tmpPath, content);
18481
+ try {
18482
+ linkSync(tmpPath, filePath);
18483
+ } finally {
18484
+ unlinkSync(tmpPath);
18485
+ }
18486
+ }
18487
+ async function runRestore(options = {}) {
18488
+ const cwd = options.cwd ?? process.cwd();
18489
+ const env = options.env ?? process.env;
18490
+ const dryRun = options.dryRun ?? false;
18491
+ const allowHistoryLoss = options.allowHistoryLoss ?? false;
18492
+ const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
18493
+ const linkPath = join6(cwd, LINK_FILE2);
18494
+ if (!existsSync6(linkPath)) {
18495
+ return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
18496
+ }
18497
+ let link;
18498
+ try {
18499
+ link = JSON.parse(readFileSync8(linkPath, "utf8"));
18500
+ } catch (e) {
18501
+ return fatalResult5(dryRun, `Could not parse ${LINK_FILE2}: ${e.message}`);
18502
+ }
18503
+ const projectId = link.project_id;
18504
+ if (!projectId) return fatalResult5(dryRun, `${LINK_FILE2} has no project_id. Run \`arkaik link --project <id>\`.`);
18505
+ const baseUrl = (options.apiBase ?? env.ARKAIK_URL ?? link.remote ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
18506
+ const encodedProjectId = encodeURIComponent(projectId);
18507
+ const token = env.ARKAIK_TOKEN;
18508
+ if (!token) return fatalResult5(dryRun, `ARKAIK_TOKEN is not set. Create a token at ${baseUrl}/settings/tokens and export it.`);
18509
+ const bundlePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
18510
+ if (!existsSync6(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
18511
+ let localRaw;
18512
+ try {
18513
+ localRaw = JSON.parse(readFileSync8(bundlePath, "utf8"));
18514
+ } catch (e) {
18515
+ return fatalResult5(dryRun, `Could not parse ${bundlePath}: ${e.message}`);
18516
+ }
18517
+ if (typeof localRaw !== "object" || localRaw === null || Array.isArray(localRaw)) {
18518
+ return fatalResult5(dryRun, `${bundlePath} is not a bundle object.`);
18519
+ }
18520
+ const local = localRaw;
18521
+ const journalEvents = loadJournalEvents(local, bundlePath);
18522
+ const outboundBundle = { ...local, journal: journalEvents };
18523
+ const headers = { Authorization: `Bearer ${token}` };
18524
+ let version2;
18525
+ try {
18526
+ const projectRes = await httpClient(`${baseUrl}/api/graph/projects/${encodedProjectId}`, { headers });
18527
+ if (!projectRes.ok) {
18528
+ return fatalResult5(dryRun, describeVersionReadFailure(projectRes.status, baseUrl, projectId));
18529
+ }
18530
+ const body = await safeJson(projectRes);
18531
+ if (typeof body.version !== "string" || body.version.length === 0) {
18532
+ return fatalResult5(dryRun, "The project response had no usable version \u2014 cannot set If-Match safely. Nothing was sent.");
18533
+ }
18534
+ version2 = body.version;
18535
+ } catch (e) {
18536
+ return fatalResult5(dryRun, `Could not read the project: ${e.message}`);
18537
+ }
18538
+ const putHeaders = { ...headers, "Content-Type": "application/json", "If-Match": `"${version2}"` };
18539
+ const putBody = JSON.stringify({ bundle: outboundBundle });
18540
+ if (dryRun) {
18541
+ let res2;
18542
+ try {
18543
+ res2 = await httpClient(`${baseUrl}/api/graph/projects/${encodedProjectId}/bundle?dryRun=1`, {
18544
+ method: "PUT",
18545
+ headers: putHeaders,
18546
+ body: putBody
18547
+ });
18548
+ } catch (e) {
18549
+ return { ok: true, dryRun, bundlePath, requestSent: false, errorMessage: `Network error: ${e.message}` };
18550
+ }
18551
+ return interpretPutResponse(res2, { dryRun, bundlePath, version: version2 });
18552
+ }
18553
+ let exported;
18554
+ try {
18555
+ const exportRes = await httpClient(`${baseUrl}/api/graph/projects/${encodedProjectId}/export`, { headers });
18556
+ if (!exportRes.ok) {
18557
+ return fatalResult5(dryRun, `Could not export the current hosted state (${exportRes.status}). Refusing to restore without a backup. Nothing was sent.`);
18558
+ }
18559
+ const body = await safeJson(exportRes);
18560
+ exported = body.bundle;
18561
+ } catch (e) {
18562
+ return fatalResult5(dryRun, `Could not export the current hosted state: ${e.message}. Refusing to restore without a backup.`);
18563
+ }
18564
+ if (typeof exported !== "object" || exported === null || Array.isArray(exported)) {
18565
+ return fatalResult5(dryRun, "The export response was not a bundle. Refusing to restore without a backup. Nothing was sent.");
18566
+ }
18567
+ const exportedBundle = exported;
18568
+ if (!Array.isArray(exportedBundle.journal) || !Array.isArray(exportedBundle.nodes) || !Array.isArray(exportedBundle.edges)) {
18569
+ return fatalResult5(
18570
+ dryRun,
18571
+ "The exported bundle is missing nodes, edges, or a journal array \u2014 an incomplete backup would not cover what restore is about to destroy. Refusing to restore. Nothing was sent."
18572
+ );
18573
+ }
18574
+ const hostedEventCount = exportedBundle.journal.length;
18575
+ if (journalEvents.length < hostedEventCount && !allowHistoryLoss) {
18576
+ return fatalResult5(
18577
+ dryRun,
18578
+ `This restore would replace ${hostedEventCount} hosted journal events with ${journalEvents.length}. Nothing was sent. If that is intended, re-run with --allow-history-loss; otherwise check that ${journalPathFor(bundlePath)} exists and is current.`
18579
+ );
18580
+ }
18581
+ const backupDir = join6(cwd, "docs", "arkaik", ".backups");
18582
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18583
+ const backupPath = join6(backupDir, `${stamp}-bundle.json`);
18584
+ const backupContent = `${JSON.stringify(exported, null, 2)}
18585
+ `;
18586
+ try {
18587
+ mkdirSync5(backupDir, { recursive: true });
18588
+ writeBackupFile(backupPath, backupContent);
18589
+ JSON.parse(readFileSync8(backupPath, "utf8"));
18590
+ } catch (e) {
18591
+ return fatalResult5(
18592
+ dryRun,
18593
+ `Could not write the pre-restore backup to ${backupPath}: ${e.message}
18594
+ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`
18595
+ );
18596
+ }
18597
+ let res;
18598
+ try {
18599
+ res = await httpClient(`${baseUrl}/api/graph/projects/${encodedProjectId}/bundle`, {
18600
+ method: "PUT",
18601
+ headers: putHeaders,
18602
+ body: putBody
18603
+ });
18604
+ } catch (e) {
18605
+ return {
18606
+ ok: true,
18607
+ dryRun,
18608
+ bundlePath,
18609
+ backupPath,
18610
+ requestSent: false,
18611
+ errorMessage: `Network error: ${e.message}. Nothing was sent.${backupNoteFor(backupPath)}`
18612
+ };
18613
+ }
18614
+ return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2 });
18615
+ }
18616
+ function printDelta(delta) {
18617
+ if (!delta) return;
18618
+ const n = (k) => delta[k] ?? "?";
18619
+ console.log(
18620
+ ` nodes ${n("nodesBefore")} -> ${n("nodesAfter")} (+${n("nodesAdded")} -${n("nodesRemoved")} ~${n("nodesChanged")}${delta.nodesMalformed ? `, ${delta.nodesMalformed} malformed` : ""})`
18621
+ );
18622
+ console.log(
18623
+ ` edges ${n("edgesBefore")} -> ${n("edgesAfter")} (+${n("edgesAdded")} -${n("edgesRemoved")} ~${n("edgesChanged")}${delta.edgesMalformed ? `, ${delta.edgesMalformed} malformed` : ""})`
18624
+ );
18625
+ console.log(
18626
+ ` events ${n("eventsBefore")} -> ${n("eventsAfter")} (+${n("eventsAdded")} -${n("eventsDropped")} ~${n("eventsChanged")}${delta.eventsMalformed ? `, ${delta.eventsMalformed} malformed` : ""})`
18627
+ );
18628
+ }
18629
+ function reportRestore(result) {
18630
+ if (!result.ok) fail10(`FATAL: ${result.fatal}`);
18631
+ if (result.backupPath) {
18632
+ console.log(`Backed up the current hosted project (snapshot + journal) to ${result.backupPath}`);
18633
+ }
18634
+ if (!result.requestSent) {
18635
+ console.error(result.errorMessage ?? "Restore failed before a request could be sent.");
18636
+ process.exit(1);
18637
+ }
18638
+ if (result.status === 200) {
18639
+ if (result.dryRun) {
18640
+ console.log("[dry-run] server preview \u2014 nothing was written:");
18641
+ printDelta(result.delta);
18642
+ console.log("Re-run without --dry-run to apply \u2014 that run takes the backup.");
18643
+ } else {
18644
+ console.log(`Restored. New version ${result.version}.`);
18645
+ printDelta(result.delta);
18646
+ if (result.backupPath) {
18647
+ console.log(` Undo this restore with: arkaik restore ${result.backupPath}`);
18648
+ }
18649
+ }
18650
+ process.exitCode = 0;
18651
+ return;
18652
+ }
18653
+ console.error(result.errorMessage ?? `Restore failed (${result.status}).`);
18654
+ for (const finding of result.serverFindings ?? []) {
18655
+ console.error(` ${finding.message ?? JSON.stringify(finding)}`);
18656
+ }
18657
+ process.exit(1);
18658
+ }
18659
+ function runRestoreCli(argv) {
18660
+ let dryRun = false;
18661
+ let allowHistoryLoss = false;
18662
+ let apiBase;
18663
+ const positionals = [];
18664
+ for (let i = 0; i < argv.length; i++) {
18665
+ const arg = argv[i];
18666
+ if (arg === "-h" || arg === "--help") {
18667
+ console.log(USAGE11);
18668
+ process.exitCode = 0;
18669
+ return;
18670
+ } else if (arg === "--dry-run") {
18671
+ dryRun = true;
18672
+ } else if (arg === "--allow-history-loss") {
18673
+ allowHistoryLoss = true;
18674
+ } else if (arg === "--api") {
18675
+ const value = argv[++i];
18676
+ if (value === void 0) fail10(`Missing value for --api
18677
+
18678
+ ${USAGE11}`);
18679
+ apiBase = value;
18680
+ } else if (arg.startsWith("-")) {
18681
+ fail10(`Unknown option: ${arg}
18682
+
18683
+ ${USAGE11}`);
18684
+ } else {
18685
+ positionals.push(arg);
18686
+ }
18687
+ }
18688
+ if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
18689
+
18690
+ ${USAGE11}`);
18691
+ runRestore({ path: positionals[0], dryRun, allowHistoryLoss, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
18692
+ }
18693
+
18694
+ // src/commands/bootstrap.ts
18695
+ import { spawnSync as spawnSync2 } from "node:child_process";
18696
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, renameSync, writeFileSync as writeFileSync12 } from "node:fs";
18697
+ import path5 from "node:path";
18698
+
18699
+ // src/lib/bootstrap/corpus.ts
18700
+ import { spawnSync } from "node:child_process";
18701
+ import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
18702
+ import path2 from "node:path";
18703
+
18704
+ // src/lib/bootstrap/paths.ts
18705
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
18706
+ import path from "node:path";
18707
+ var BOOTSTRAP_ROOT = ".arkaik";
18708
+ var CORPUS_DIR = ".arkaik/corpus";
18709
+ var PLAN_DIR = ".arkaik/bootstrap";
18710
+ var FRAGMENTS_DIR = ".arkaik/bootstrap/fragments";
18711
+ var MANIFEST_FILE = ".arkaik/bootstrap/manifest.json";
18712
+ var PROFILE_FILE = ".arkaik/bootstrap/profile.json";
18713
+ var PRS_FILE = ".arkaik/corpus/prs.jsonl";
18714
+ var DOCS_FILE = ".arkaik/corpus/docs.json";
18715
+ var SURFACES_FILE = ".arkaik/corpus/surfaces.json";
18716
+ function at(cwd, relative) {
18717
+ return path.join(cwd, relative);
18718
+ }
18719
+ function ensureDir(dirPath) {
18720
+ mkdirSync6(dirPath, { recursive: true });
18721
+ }
18722
+ function ensureGitignored(cwd) {
18723
+ const file2 = path.join(cwd, ".gitignore");
18724
+ const line2 = `${BOOTSTRAP_ROOT}/`;
18725
+ const current = existsSync7(file2) ? readFileSync9(file2, "utf8") : "";
18726
+ const ignored = current.split("\n").map((l) => l.trim()).some((l) => l === line2 || l === BOOTSTRAP_ROOT);
18727
+ if (ignored) return false;
18728
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
18729
+ writeFileSync9(file2, `${current}${prefix}${line2}
18730
+ `);
18731
+ return true;
18732
+ }
18733
+
18734
+ // src/lib/bootstrap/corpus.ts
18735
+ var GH_FIELDS = "number,title,body,mergedAt,labels,files";
18736
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
18737
+ "node_modules",
18738
+ ".git",
18739
+ ".next",
18740
+ "dist",
18741
+ "build",
18742
+ "coverage",
18743
+ ".arkaik",
18744
+ "ios",
18745
+ "android",
18746
+ "Pods"
18747
+ ]);
18748
+ var SURFACE_RULES = [
18749
+ { test: /(^|\/)app\/api\/.*\/route\.[tj]sx?$/, kind: "api" },
18750
+ { test: /(^|\/)pages\/api\/.*\.[tj]sx?$/, kind: "api" },
18751
+ { test: /(^|\/)app\/.*\/page\.[tj]sx?$/, kind: "page" },
18752
+ { test: /(^|\/)pages\/(?!api\/).*\.[tj]sx?$/, kind: "page" },
18753
+ { test: /(^|\/)(screens|views)\/[^/]+\.[tj]sx?$/, kind: "screen" },
18754
+ { test: /(^|\/)app\/.*\/route\.[tj]sx?$/, kind: "route" },
18755
+ { test: /(^|\/)components\/[^/]+\.[tj]sx?$/, kind: "component" }
18756
+ ];
18757
+ function normalizePrs(raw) {
18758
+ if (!Array.isArray(raw)) return [];
18759
+ const prs = [];
18760
+ for (const row of raw) {
18761
+ if (typeof row !== "object" || row === null) continue;
18762
+ const r = row;
18763
+ const body = typeof r.body === "string" ? r.body : "";
18764
+ prs.push({
18765
+ // The primary key: readCorpusPrs blind-casts this back to `number`, and
18766
+ // JSON.stringify would silently write NaN as `null` otherwise.
18767
+ number: typeof r.number === "number" && Number.isFinite(r.number) ? r.number : 0,
18768
+ title: typeof r.title === "string" ? r.title : "",
18769
+ body,
18770
+ merged_at: typeof r.mergedAt === "string" ? r.mergedAt : "",
18771
+ labels: Array.isArray(r.labels) ? r.labels.map((l) => typeof l === "string" ? l : String(l?.name ?? "")).filter(Boolean) : [],
18772
+ files: Array.isArray(r.files) ? r.files.map((f) => typeof f === "string" ? f : String(f?.path ?? "")).filter(Boolean) : [],
18773
+ // A Lab Note means user-visible by definition — the story wave's cheapest
18774
+ // signal, and the reason its deliverable copy is nearly free.
18775
+ has_lab_note: /^##\s+Lab Note/m.test(body)
18776
+ });
18777
+ }
18778
+ return prs.sort((a, b) => {
18779
+ const aTime = Date.parse(a.merged_at);
18780
+ const bTime = Date.parse(b.merged_at);
18781
+ const av = Number.isNaN(aTime) ? Infinity : aTime;
18782
+ const bv = Number.isNaN(bTime) ? Infinity : bTime;
18783
+ if (av !== bv) return av - bv;
18784
+ return a.number - b.number;
18785
+ });
18786
+ }
18787
+ function fetchPrsViaGh(cwd, limit) {
18788
+ const res = spawnSync("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", GH_FIELDS], {
18789
+ cwd,
18790
+ encoding: "utf8",
18791
+ maxBuffer: 256 * 1024 * 1024
18792
+ });
18793
+ if (res.error) throw new Error(`gh not runnable: ${res.error.message}`);
18794
+ if (res.status !== 0) throw new Error(`gh exited ${res.status}: ${res.stderr.trim()}`);
18795
+ return JSON.parse(res.stdout);
18796
+ }
18797
+ function fetchPrsViaGit(cwd) {
18798
+ const res = spawnSync("git", ["log", "--merges", "--reverse", "--date=iso-strict", "--pretty=%ad%x1f%s"], {
18799
+ cwd,
18800
+ encoding: "utf8",
18801
+ maxBuffer: 64 * 1024 * 1024
18802
+ });
18803
+ if (res.error) throw new Error(`git not runnable: ${res.error.message}`);
18804
+ if (res.status !== 0) throw new Error(`git log failed: ${(res.stderr ?? "").trim()}`);
18805
+ const lines = res.stdout.split("\n").filter(Boolean);
18806
+ const parsed = lines.map((line2) => {
18807
+ const [date5, subject] = line2.split("");
18808
+ const matched = /^Merge pull request #(\d+) /.exec(subject ?? "");
18809
+ return { date: date5 ?? "", subject: subject ?? "", number: matched ? Number(matched[1]) : void 0 };
18810
+ });
18811
+ const taken = new Set(parsed.map((p) => p.number).filter((n) => n !== void 0));
18812
+ let next = 1;
18813
+ return parsed.map((p) => {
18814
+ let number4 = p.number;
18815
+ if (number4 === void 0) {
18816
+ while (taken.has(next)) next += 1;
18817
+ number4 = next;
18818
+ taken.add(number4);
18819
+ }
18820
+ return { number: number4, title: p.subject, body: "", mergedAt: p.date, labels: [], files: [] };
18821
+ });
18822
+ }
18823
+ function walk(root, cwd, out) {
18824
+ let entries;
18825
+ try {
18826
+ entries = readdirSync(root, { withFileTypes: true });
18827
+ } catch {
18828
+ return;
18829
+ }
18830
+ for (const entry of entries) {
18831
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
18832
+ const full = path2.join(root, entry.name);
18833
+ if (entry.isDirectory()) walk(full, cwd, out);
18834
+ else if (entry.isFile()) out.push(path2.relative(cwd, full).split(path2.sep).join("/"));
18835
+ }
18836
+ }
18837
+ function listFiles(cwd) {
18838
+ const out = [];
18839
+ walk(cwd, cwd, out);
18840
+ return out.sort();
18841
+ }
18842
+ function buildDocsManifest(cwd, files) {
18843
+ return files.filter((f) => f.startsWith("docs/") && f.endsWith(".md")).map((f) => {
18844
+ const text = readFileSync10(path2.join(cwd, f), "utf8");
18845
+ const heading = /^#\s+(.+)$/m.exec(text);
18846
+ return { path: f, title: heading ? heading[1].trim() : path2.basename(f, ".md") };
18847
+ });
18848
+ }
18849
+ function buildSurfaceInventory(files) {
18850
+ const out = [];
18851
+ for (const file2 of files) {
18852
+ const rule = SURFACE_RULES.find((r) => r.test.test(file2));
18853
+ if (rule) out.push({ path: file2, kind: rule.kind });
18854
+ }
18855
+ return out;
18856
+ }
18857
+ function buildCorpus(options) {
18858
+ const { cwd } = options;
18859
+ const raw = options.fromJson ? JSON.parse(readFileSync10(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
18860
+ let prs = normalizePrs(raw);
18861
+ let sinceDroppedUndated = 0;
18862
+ if (options.since) {
18863
+ const floor = Date.parse(options.since);
18864
+ if (Number.isNaN(floor)) {
18865
+ throw new Error(`--since is not a parseable date: ${options.since} (try an ISO date like 2026-01-31)`);
18866
+ }
18867
+ prs = prs.filter((pr) => {
18868
+ const merged = Date.parse(pr.merged_at);
18869
+ if (Number.isNaN(merged)) {
18870
+ sinceDroppedUndated += 1;
18871
+ return false;
18872
+ }
18873
+ return merged >= floor;
18874
+ });
18875
+ }
18876
+ const files = listFiles(cwd);
18877
+ const docs = buildDocsManifest(cwd, files);
18878
+ const surfaces = buildSurfaceInventory(files);
18879
+ ensureDir(at(cwd, CORPUS_DIR));
18880
+ writeFileSync10(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
18881
+ writeFileSync10(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
18882
+ `);
18883
+ writeFileSync10(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
18884
+ `);
18885
+ return { prs: prs.length, docs: docs.length, surfaces: surfaces.length, sinceDroppedUndated };
18886
+ }
18887
+ function readCorpusPrs(cwd) {
18888
+ const file2 = at(cwd, PRS_FILE);
18889
+ if (!existsSync8(file2)) return [];
18890
+ return readFileSync10(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
18891
+ }
18892
+
18893
+ // src/lib/bootstrap/fragments.ts
18894
+ import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
18895
+ import path3 from "node:path";
18896
+ function isArrayOfObjects(value) {
18897
+ return value === void 0 || Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null && !Array.isArray(v));
18898
+ }
18899
+ var SAFE_UNIT_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
18900
+ function fragmentPathFor(cwd, unitId) {
18901
+ if (typeof unitId !== "string" || !SAFE_UNIT_ID_RE.test(unitId)) {
18902
+ throw new Error(
18903
+ `manifest.json has an unsafe work-unit id: ${JSON.stringify(unitId)}. Refusing to resolve a fragment path from it \u2014 ids must be lowercase kebab-case (letters, digits, hyphens only). Fix manifest.json, or re-run \`arkaik bootstrap plan\`, and retry.`
18904
+ );
18905
+ }
18906
+ return path3.join(cwd, FRAGMENTS_DIR, `${unitId}.json`);
18907
+ }
18908
+ function loadFragments(cwd, manifest) {
18909
+ const loaded = [];
18910
+ const problems = [];
18911
+ const missing = [];
18912
+ for (const unit2 of manifest.units) {
18913
+ let file2;
18914
+ try {
18915
+ file2 = fragmentPathFor(cwd, unit2.id);
18916
+ } catch (err) {
18917
+ problems.push({ unit: String(unit2.id), message: err instanceof Error ? err.message : String(err) });
18918
+ continue;
18919
+ }
18920
+ if (!existsSync9(file2)) {
18921
+ missing.push(unit2.id);
18922
+ continue;
18923
+ }
18924
+ let parsed;
18925
+ try {
18926
+ parsed = JSON.parse(readFileSync11(file2, "utf8"));
18927
+ } catch (err) {
18928
+ problems.push({ unit: unit2.id, message: `not valid JSON: ${err instanceof Error ? err.message : "parse error"}` });
18929
+ continue;
18930
+ }
18931
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
18932
+ problems.push({ unit: unit2.id, message: "fragment must be a JSON object" });
18933
+ continue;
18934
+ }
18935
+ const fragment = parsed;
18936
+ for (const key of ["nodes", "edges", "add", "update", "retire", "events"]) {
18937
+ if (!isArrayOfObjects(fragment[key])) {
18938
+ problems.push({ unit: unit2.id, message: `\`${key}\` must be an array of objects` });
18939
+ }
18940
+ }
18941
+ loaded.push({ unit: unit2, fragment });
18942
+ }
18943
+ return { loaded, problems, missing };
18944
+ }
18945
+
18946
+ // src/lib/bootstrap/index-view.ts
18947
+ function tsvField(value, fallback = "") {
18948
+ const str2 = value === void 0 || value === null ? fallback : String(value);
18949
+ return str2.replace(/[\t\r\n]+/g, " ");
18950
+ }
18951
+ function renderIndex(bundle) {
18952
+ const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
18953
+ const lines = ["id species title product"];
18954
+ for (const node of nodes) {
18955
+ const product = node.metadata && typeof node.metadata === "object" ? node.metadata.product : void 0;
18956
+ lines.push([tsvField(node.id), tsvField(node.species), tsvField(node.title), product ? tsvField(product) : "-"].join(" "));
18957
+ }
18958
+ return `${lines.join("\n")}
18959
+ `;
18960
+ }
18961
+
18962
+ // src/lib/bootstrap/manifest.ts
18963
+ import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "node:fs";
18964
+ import path4 from "node:path";
18965
+
18966
+ // src/lib/bootstrap/era-window.ts
18967
+ var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
18968
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
18969
+ function eraStart(value) {
18970
+ return Date.parse(value);
18971
+ }
18972
+ function eraEnd(value) {
18973
+ const parsed = Date.parse(value);
18974
+ if (Number.isNaN(parsed)) return parsed;
18975
+ return DATE_ONLY_RE.test(value) ? parsed + ONE_DAY_MS : parsed;
18976
+ }
18977
+
18978
+ // src/lib/bootstrap/profile-validate.ts
18979
+ var SAFE_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
18980
+ var MAX_ID_LENGTH = 64;
18981
+ function assertSafeId(kind, id) {
18982
+ if (typeof id !== "string" || !SAFE_ID_RE.test(id) || id.length > MAX_ID_LENGTH) {
18983
+ throw new Error(
18984
+ `profile.json has an invalid ${kind} id: ${JSON.stringify(id)}. Work-unit ids become fragment filenames under ${FRAGMENTS_DIR}/, so they must be lowercase kebab-case (letters, digits and hyphens only, no uppercase, no "/", "..", or whitespace) and at most ${MAX_ID_LENGTH} characters. Fix profile.json and re-run \`arkaik bootstrap plan\`.`
18985
+ );
18986
+ }
18987
+ }
18988
+ function assertArrayField(field, value) {
18989
+ if (value !== void 0 && !Array.isArray(value)) {
18990
+ throw new Error(
18991
+ `profile.json "${field}" must be an array, got ${typeof value} (${JSON.stringify(value)}). Fix profile.json and re-run \`arkaik bootstrap plan\`.`
18992
+ );
18993
+ }
18994
+ }
18995
+ function assertEraWindow(era) {
18996
+ if (era.from === void 0 && era.to === void 0) {
18997
+ throw new Error(
18998
+ `profile.json era "${String(era.slug)}" has neither "from" nor "to". An unbounded era would hand that era's wave-3 agent zero PRs (bootstrap slice's date-window filter can't constrain anything), silently contributing nothing to the story. Add at least one date and re-run \`arkaik bootstrap plan\`.`
18999
+ );
19000
+ }
19001
+ for (const [key, value] of [
19002
+ ["from", era.from],
19003
+ ["to", era.to]
19004
+ ]) {
19005
+ if (value === void 0) continue;
19006
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
19007
+ throw new Error(
19008
+ `profile.json era "${String(era.slug)}" has an unparseable "${key}": ${JSON.stringify(value)} (try an ISO date like 2026-01-31). Fix profile.json and re-run \`arkaik bootstrap plan\`.`
19009
+ );
19010
+ }
19011
+ }
19012
+ }
19013
+ function assertNoOverlappingEras(eras) {
19014
+ const windows = eras.map((e) => ({
19015
+ slug: e.slug,
19016
+ start: e.from !== void 0 ? eraStart(e.from) : -Infinity,
19017
+ end: e.to !== void 0 ? eraEnd(e.to) : Infinity
19018
+ }));
19019
+ for (let i = 0; i < windows.length; i += 1) {
19020
+ for (let j = i + 1; j < windows.length; j += 1) {
19021
+ const a = windows[i];
19022
+ const b = windows[j];
19023
+ if (a.start < b.end && b.start < a.end) {
19024
+ const bothOpenEnded = a.end === Infinity && b.end === Infinity;
19025
+ throw new Error(
19026
+ bothOpenEnded ? `profile.json eras "${a.slug}" and "${b.slug}" both have only a "from" date, so both extend indefinitely and always overlap. Give the earlier era a "to" date (e.g. the later era's "from") and re-run \`arkaik bootstrap plan\`.` : `profile.json eras "${a.slug}" and "${b.slug}" have overlapping date windows. Eras must partition the corpus without overlap \u2014 narrow one or both windows and re-run \`arkaik bootstrap plan\`.`
19027
+ );
19028
+ }
19029
+ }
19030
+ }
19031
+ }
19032
+ function assertAreas(profile) {
19033
+ for (const rawArea of profile.areas ?? []) {
19034
+ if (rawArea === null || typeof rawArea !== "object") {
19035
+ throw new Error(
19036
+ `profile.json has a malformed area entry: ${JSON.stringify(rawArea)} (expected an object with id, title, paths).`
19037
+ );
19038
+ }
19039
+ const area = rawArea;
19040
+ assertSafeId("area", area.id);
19041
+ if (!Array.isArray(area.paths) || area.paths.length === 0) {
19042
+ throw new Error(
19043
+ `profile.json area "${String(area.id)}" has no paths. An empty slice would hand that unit's agent the entire corpus with no filtering. Add at least one path and re-run \`arkaik bootstrap plan\`.`
19044
+ );
19045
+ }
19046
+ }
19047
+ }
19048
+ function assertEras(profile) {
19049
+ for (const rawEra of profile.eras ?? []) {
19050
+ if (rawEra === null || typeof rawEra !== "object") {
19051
+ throw new Error(
19052
+ `profile.json has a malformed era entry: ${JSON.stringify(rawEra)} (expected an object with slug, title).`
19053
+ );
19054
+ }
19055
+ const era = rawEra;
19056
+ assertSafeId("era", era.slug);
19057
+ assertEraWindow(era);
19058
+ }
19059
+ assertNoOverlappingEras(profile.eras ?? []);
19060
+ }
19061
+ function assertValidProfile(profile) {
19062
+ if (!profile) return;
19063
+ assertArrayField("areas", profile.areas);
19064
+ assertArrayField("eras", profile.eras);
19065
+ assertAreas(profile);
19066
+ assertEras(profile);
19067
+ }
19068
+
19069
+ // src/lib/bootstrap/manifest.ts
19070
+ function readProfile(cwd) {
19071
+ const file2 = at(cwd, PROFILE_FILE);
19072
+ if (!existsSync10(file2)) return null;
19073
+ try {
19074
+ return JSON.parse(readFileSync12(file2, "utf8"));
19075
+ } catch (err) {
19076
+ throw new Error(`cannot read ${PROFILE_FILE}: ${err instanceof Error ? err.message : String(err)}`);
19077
+ }
19078
+ }
19079
+ function readManifest(cwd) {
19080
+ const file2 = at(cwd, MANIFEST_FILE);
19081
+ if (!existsSync10(file2)) return null;
19082
+ try {
19083
+ return JSON.parse(readFileSync12(file2, "utf8"));
19084
+ } catch (err) {
19085
+ throw new Error(`cannot read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
19086
+ }
19087
+ }
19088
+ function writeManifest(cwd, manifest) {
19089
+ ensureDir(at(cwd, PLAN_DIR));
19090
+ ensureDir(at(cwd, FRAGMENTS_DIR));
19091
+ writeFileSync11(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
19092
+ `);
19093
+ }
19094
+ function detectMode(cwd, bundlePath) {
19095
+ const file2 = path4.resolve(cwd, bundlePath);
19096
+ if (!existsSync10(file2)) return "greenfield";
19097
+ let parsed;
19098
+ try {
19099
+ parsed = JSON.parse(readFileSync12(file2, "utf8"));
19100
+ } catch (err) {
19101
+ throw new Error(`cannot read bundle at ${bundlePath}: ${err instanceof Error ? err.message : String(err)}`);
19102
+ }
19103
+ const nodes = parsed !== null && typeof parsed === "object" && Array.isArray(parsed.nodes) ? parsed.nodes : [];
19104
+ return nodes.length === 0 ? "greenfield" : "brownfield";
19105
+ }
19106
+ function unit(id, wave, title2, scope, slice) {
19107
+ return { id, wave, title: title2, scope, slice, fragment: `${FRAGMENTS_DIR}/${id}.json`, status: "pending" };
19108
+ }
19109
+ var RECON_SCOPE = "Read the corpus and the repo. Write .arkaik/bootstrap/profile.json declaring products, the platform axis, the areas to fan out over (id, title, code paths), and the thematic eras the merged PRs fall into. Then re-run `arkaik bootstrap plan` to expand waves 1-3.";
19110
+ function planUnits(options) {
19111
+ const { mode, bundle, profile, previous } = options;
19112
+ assertValidProfile(profile);
19113
+ const units = [unit("w0-recon", 0, "Recon", RECON_SCOPE, { docs: true })];
19114
+ for (const area of profile?.areas ?? []) {
19115
+ units.push(
19116
+ unit(
19117
+ `w1-${area.id}`,
19118
+ 1,
19119
+ `Anatomy \u2014 ${area.title}`,
19120
+ mode === "brownfield" ? `Reconcile the existing map for ${area.title} against the code. Emit add/update/retire; never delete.` : `Map the anatomy of ${area.title}: flows, views, data models, API endpoints, and the edges between them.`,
19121
+ { paths: area.paths }
19122
+ )
19123
+ );
19124
+ }
19125
+ for (const area of profile?.areas ?? []) {
19126
+ units.push(
19127
+ unit(
19128
+ `w2-${area.id}`,
19129
+ 2,
19130
+ `Acceptances \u2014 ${area.title}`,
19131
+ `Write acceptances for ${area.title}: one Given/When/Then each, 1-3 value elements, covers edges to real nodes, platform scoping per the platform axis in profile.json.`,
19132
+ { paths: area.paths }
19133
+ )
19134
+ );
19135
+ }
19136
+ for (const era of profile?.eras ?? []) {
19137
+ units.push(
19138
+ unit(
19139
+ `w3-${era.slug}`,
19140
+ 3,
19141
+ `Story \u2014 ${era.title}`,
19142
+ `Turn this era's user-visible PRs into deliverables and tag the era as a release. A PR with a Lab Note is user-visible by definition; judge the rest.`,
19143
+ { eras: [era.slug] }
19144
+ )
19145
+ );
19146
+ }
19147
+ if (profile) {
19148
+ units.push(
19149
+ unit("w3-decisions", 3, "Story \u2014 decisions", "Mine decisions from the design docs; emit DEC- nodes, their edges, and their events.", {
19150
+ docs: true
19151
+ }),
19152
+ unit("w3-status-arcs", 3, "Story \u2014 status arcs", "Give each anatomy node an honest 1-3 event status arc ending at its snapshot status.", {
19153
+ docs: false
19154
+ })
19155
+ );
19156
+ }
19157
+ const seen = /* @__PURE__ */ new Set();
19158
+ for (const u of units) {
19159
+ if (seen.has(u.id)) {
19160
+ throw new Error(
19161
+ `duplicate work-unit id "${u.id}" \u2014 check profile.json for a repeated area id or era slug (or one that collides with the reserved "decisions" / "status-arcs" era names).`
19162
+ );
19163
+ }
19164
+ seen.add(u.id);
19165
+ }
19166
+ const previousById = new Map((previous?.units ?? []).map((u) => [u.id, u]));
19167
+ for (const u of units) {
19168
+ const before = previousById.get(u.id);
19169
+ if (before && sameSlice(before, u)) {
19170
+ u.status = before.status;
19171
+ if (before.issueUrl) u.issueUrl = before.issueUrl;
19172
+ }
19173
+ }
19174
+ return { version: 1, mode, bundle, units };
19175
+ }
19176
+ function sameSlice(before, next) {
19177
+ return JSON.stringify(before.slice) === JSON.stringify(next.slice);
19178
+ }
19179
+ function renderIssues(manifest) {
19180
+ return manifest.units.filter((u) => u.status === "pending" && !u.issueUrl).map((u) => ({
19181
+ unit: u.id,
19182
+ title: `[bootstrap] ${u.id} \u2014 ${u.title}`,
19183
+ body: [
19184
+ `**Wave ${u.wave}.** ${u.scope}`,
19185
+ "",
19186
+ "### How to work this unit",
19187
+ "",
19188
+ "```bash",
19189
+ `arkaik bootstrap slice ${u.id} > slice.json`,
19190
+ "```",
19191
+ "",
19192
+ `Read \`slice.json\`, then write your fragment to \`${u.fragment}\`.`,
19193
+ "Do not edit the bundle. Do not edit another unit's fragment.",
19194
+ "",
19195
+ "The `arkaik-bootstrap` skill defines the fragment contract and the",
19196
+ "judgment rules for this wave. When the fragment is written, set this",
19197
+ `unit's status to \`done\` in \`${MANIFEST_FILE}\`.`
19198
+ ].join("\n")
19199
+ }));
19200
+ }
19201
+
19202
+ // src/lib/bootstrap/event-id.ts
19203
+ var ENCODING2 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
19204
+ var TIME_LEN2 = 10;
19205
+ var RANDOM_LEN2 = 16;
19206
+ var FNV_OFFSET = 2166136261;
19207
+ var FNV_PRIME = 16777619;
19208
+ function fnv1a(input) {
19209
+ let h = FNV_OFFSET;
19210
+ for (let i = 0; i < input.length; i += 1) {
19211
+ h ^= input.charCodeAt(i);
19212
+ h = Math.imul(h, FNV_PRIME);
19213
+ }
19214
+ return h >>> 0;
19215
+ }
19216
+ function fmix32(hIn) {
19217
+ let h = hIn >>> 0;
19218
+ h ^= h >>> 16;
19219
+ h = Math.imul(h, 2246822507) >>> 0;
19220
+ h ^= h >>> 13;
19221
+ h = Math.imul(h, 3266489909) >>> 0;
19222
+ h ^= h >>> 16;
19223
+ return h >>> 0;
19224
+ }
19225
+ function encodeTime2(ms) {
19226
+ let remaining = Math.max(0, Math.floor(ms));
19227
+ let out = "";
19228
+ for (let i = 0; i < TIME_LEN2; i += 1) {
19229
+ const digit = remaining % 32;
19230
+ out = ENCODING2[digit] + out;
19231
+ remaining = (remaining - digit) / 32;
19232
+ }
19233
+ return out;
19234
+ }
19235
+ function keySuffix(key) {
19236
+ let out = "";
19237
+ for (let i = 0; i < RANDOM_LEN2; i += 1) {
19238
+ const h = fmix32(fnv1a(`${i}:${key}`));
19239
+ out += ENCODING2[h % 32];
19240
+ }
19241
+ return out;
19242
+ }
19243
+ function deterministicEventId(ts, key) {
19244
+ const ms = Date.parse(ts);
19245
+ if (Number.isNaN(ms)) {
19246
+ throw new Error(`deterministicEventId: ts is not a parseable timestamp: ${JSON.stringify(ts)}`);
19247
+ }
19248
+ return encodeTime2(ms) + keySuffix(key);
19249
+ }
19250
+
19251
+ // src/lib/bootstrap/journal-merge.ts
19252
+ function sortEvents(events) {
19253
+ return [...events].sort((a, b) => {
19254
+ const at2 = String(a.ts ?? "");
19255
+ const bt = String(b.ts ?? "");
19256
+ if (at2 !== bt) return at2 < bt ? -1 : 1;
19257
+ const ai = String(a.id ?? "");
19258
+ const bi = String(b.id ?? "");
19259
+ return ai < bi ? -1 : ai > bi ? 1 : 0;
19260
+ });
19261
+ }
19262
+ function canonicalJson(value) {
19263
+ if (value === void 0) return "null";
19264
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
19265
+ if (value !== null && typeof value === "object") {
19266
+ const obj = value;
19267
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
19268
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
19269
+ }
19270
+ return JSON.stringify(value);
19271
+ }
19272
+ function isOrderSensitive(ev) {
19273
+ if (ev.type === "decision.status_changed") return true;
19274
+ if (ev.type === "node.status_changed") return ev.platform === void 0;
19275
+ return false;
19276
+ }
19277
+ function eventKey(raw, ts) {
19278
+ const stringify = (value) => typeof value === "string" ? value : canonicalJson(value);
19279
+ const type = String(raw.type ?? "");
19280
+ const parts = [type, `ts=${ts}`];
19281
+ const field = (label, value) => {
19282
+ if (value !== void 0) parts.push(`${label}=${stringify(value)}`);
19283
+ };
19284
+ field("node", raw.node_id);
19285
+ field("deliverable", raw.deliverable_id);
19286
+ field("version", raw.version);
19287
+ field("edge", raw.edge_id);
19288
+ field("ref", raw.ref_id);
19289
+ field("source", raw.source_id);
19290
+ field("target", raw.target_id);
19291
+ field("platform", raw.platform);
19292
+ field("from", raw.from);
19293
+ field("to", raw.to);
19294
+ field("title", raw.title);
19295
+ field("url", raw.url);
19296
+ return parts.join("|");
19297
+ }
19298
+ function mergeJournal(base, fresh) {
19299
+ const byId = /* @__PURE__ */ new Map();
19300
+ for (const ev of base) {
19301
+ const id = typeof ev.id === "string" ? ev.id : void 0;
19302
+ if (id) byId.set(id, ev);
19303
+ }
19304
+ let added = 0;
19305
+ const conflicts = [];
19306
+ for (const ev of fresh) {
19307
+ const id = typeof ev.id === "string" ? ev.id : void 0;
19308
+ if (!id) continue;
19309
+ const existing = byId.get(id);
19310
+ if (existing) {
19311
+ if (canonicalJson(existing) !== canonicalJson(ev)) {
19312
+ conflicts.push({ id, base: existing, fresh: ev });
19313
+ }
19314
+ continue;
19315
+ }
19316
+ byId.set(id, ev);
19317
+ added += 1;
19318
+ }
19319
+ return { journal: sortEvents([...byId.values()]), added, conflicts };
19320
+ }
19321
+
19322
+ // src/lib/bootstrap/merge.ts
19323
+ function asArray(value) {
19324
+ return Array.isArray(value) ? value : [];
19325
+ }
19326
+ function mergeFragments(input) {
19327
+ const errors = [];
19328
+ const projectId = String(input.base.project?.id ?? "");
19329
+ const nodes = /* @__PURE__ */ new Map();
19330
+ const nodeOrigin = /* @__PURE__ */ new Map();
19331
+ for (const node of asArray(input.base.nodes)) {
19332
+ const id = String(node.id);
19333
+ nodes.set(id, { ...node });
19334
+ nodeOrigin.set(id, "(already in the bundle)");
19335
+ }
19336
+ const edges = /* @__PURE__ */ new Map();
19337
+ const edgeOrigin = /* @__PURE__ */ new Map();
19338
+ for (const edge of asArray(input.base.edges)) {
19339
+ const id = String(edge.id ?? edgeId(String(edge.source_id), String(edge.target_id)));
19340
+ edges.set(id, { ...edge });
19341
+ edgeOrigin.set(id, "(already in the bundle)");
19342
+ }
19343
+ const newEvents = [];
19344
+ const counts = { nodesAdded: 0, nodesUpdated: 0, nodesRetired: 0, edgesAdded: 0, eventsAdded: 0 };
19345
+ const orderSensitiveSeen = /* @__PURE__ */ new Map();
19346
+ for (const ev of input.baseJournal) {
19347
+ if (!isOrderSensitive(ev)) continue;
19348
+ const key = `${String(ev.type)}:${String(ev.node_id ?? "")}:${String(ev.ts ?? "")}`;
19349
+ if (!orderSensitiveSeen.has(key)) {
19350
+ orderSensitiveSeen.set(key, { unit: "(already in the journal)", from: ev.from, to: ev.to });
19351
+ }
19352
+ }
19353
+ const eventUnit = /* @__PURE__ */ new Map();
19354
+ const pushEvent = (unitId, ev) => {
19355
+ if (isOrderSensitive(ev)) {
19356
+ const nodeId = String(ev.node_id ?? "");
19357
+ const key = `${String(ev.type)}:${nodeId}:${String(ev.ts ?? "")}`;
19358
+ const existing = orderSensitiveSeen.get(key);
19359
+ if (existing && String(existing.to) !== String(ev.to)) {
19360
+ errors.push({
19361
+ unit: unitId,
19362
+ message: `two \`${String(ev.type)}\` events for node \`${nodeId}\` land at the identical ts ${String(ev.ts)} but disagree on the outcome \u2014 ${JSON.stringify(existing.from)} -> ${JSON.stringify(existing.to)} (from ${existing.unit}) vs ${JSON.stringify(ev.from)} -> ${JSON.stringify(ev.to)} (from ${unitId}). crossCheckJournal reads only the LAST such event's \`to\` per node, and read-back order between events sharing one ts is not guaranteed to match write order \u2014 give one of them a distinct timestamp (\`update\`/\`retire\`: \`changed_ts\`; wave-3 \`events\`: \`ts\`) and re-run.`
19363
+ });
19364
+ return;
19365
+ }
19366
+ if (!existing) {
19367
+ orderSensitiveSeen.set(key, { unit: unitId, from: ev.from, to: ev.to });
19368
+ }
19369
+ }
19370
+ eventUnit.set(String(ev.id), unitId);
19371
+ newEvents.push(ev);
19372
+ };
19373
+ const mintEvent = (unitId, ts, payload, onFail) => {
19374
+ try {
19375
+ const id = deterministicEventId(ts, eventKey(payload, ts));
19376
+ pushEvent(unitId, { id, ts, actor: "bootstrap", ...payload });
19377
+ } catch (err) {
19378
+ errors.push({ unit: unitId, message: `${onFail}: ${err instanceof Error ? err.message : String(err)}` });
19379
+ }
19380
+ };
19381
+ for (const { unit: unit2, fragment } of input.fragments) {
19382
+ for (const raw of [...fragment.nodes ?? [], ...fragment.add ?? []]) {
19383
+ const id = String(raw.id ?? "");
19384
+ if (!id) {
19385
+ errors.push({ unit: unit2.id, message: "a node has no id" });
19386
+ continue;
19387
+ }
19388
+ const { created_ts: createdTs, ...node } = raw;
19389
+ if (!Array.isArray(node.platforms)) node.platforms = [];
19390
+ if (nodes.has(id)) {
19391
+ const existingTitle = String(nodes.get(id)?.title ?? "");
19392
+ const incomingTitle = String(node.title ?? "");
19393
+ const owner = nodeOrigin.get(id);
19394
+ if (existingTitle !== incomingTitle) {
19395
+ errors.push({
19396
+ unit: unit2.id,
19397
+ message: `id collision on \`${id}\`: "${existingTitle}" (from ${owner}) vs "${incomingTitle}" (from ${unit2.id})`
19398
+ });
19399
+ }
19400
+ continue;
19401
+ }
19402
+ nodes.set(id, { ...node, id, project_id: projectId });
19403
+ nodeOrigin.set(id, unit2.id);
19404
+ counts.nodesAdded += 1;
19405
+ const ts = typeof createdTs === "string" && createdTs ? createdTs : input.fallbackTs;
19406
+ mintEvent(
19407
+ unit2.id,
19408
+ ts,
19409
+ { type: "node.created", node_id: id, species: node.species, title: node.title },
19410
+ `cannot mint a node.created event for \`${id}\` (ts ${JSON.stringify(ts)})`
19411
+ );
19412
+ }
19413
+ for (const patch of fragment.update ?? []) {
19414
+ const id = String(patch.id ?? "");
19415
+ const target = nodes.get(id);
19416
+ if (!target) {
19417
+ errors.push({ unit: unit2.id, message: `update targets unknown node \`${id}\`` });
19418
+ continue;
19419
+ }
19420
+ const fromStatus = target.status;
19421
+ const existingMetadata = target.metadata ?? {};
19422
+ const fromDecisionStatus = String(existingMetadata.decision_status ?? "proposed");
19423
+ const rawPatch = patch.patch ?? {};
19424
+ const { metadata: patchMetadata, ...restPatch } = rawPatch;
19425
+ Object.assign(target, restPatch, { id, project_id: projectId });
19426
+ if (patchMetadata !== void 0 && typeof patchMetadata === "object" && patchMetadata !== null) {
19427
+ target.metadata = { ...existingMetadata, ...patchMetadata };
19428
+ }
19429
+ counts.nodesUpdated += 1;
19430
+ const toStatus = target.status;
19431
+ if (Object.prototype.hasOwnProperty.call(rawPatch, "status") && toStatus !== fromStatus) {
19432
+ const ts = typeof patch.changed_ts === "string" && patch.changed_ts ? patch.changed_ts : input.fallbackTs;
19433
+ mintEvent(
19434
+ unit2.id,
19435
+ ts,
19436
+ { type: "node.status_changed", node_id: id, from: fromStatus, to: toStatus },
19437
+ `cannot mint a node.status_changed event for \`${id}\` (ts ${JSON.stringify(ts)})`
19438
+ );
19439
+ }
19440
+ const finalMetadata = target.metadata ?? {};
19441
+ const toDecisionStatus = String(finalMetadata.decision_status ?? "proposed");
19442
+ const patchTouchesDecisionStatus = patchMetadata !== void 0 && typeof patchMetadata === "object" && patchMetadata !== null && Object.prototype.hasOwnProperty.call(patchMetadata, "decision_status");
19443
+ if (patchTouchesDecisionStatus && toDecisionStatus !== fromDecisionStatus) {
19444
+ const ts = typeof patch.changed_ts === "string" && patch.changed_ts ? patch.changed_ts : input.fallbackTs;
19445
+ mintEvent(
19446
+ unit2.id,
19447
+ ts,
19448
+ { type: "decision.status_changed", node_id: id, from: fromDecisionStatus, to: toDecisionStatus },
19449
+ `cannot mint a decision.status_changed event for \`${id}\` (ts ${JSON.stringify(ts)})`
19450
+ );
19451
+ }
19452
+ }
19453
+ for (const retire of fragment.retire ?? []) {
19454
+ const id = String(retire.id ?? "");
19455
+ const target = nodes.get(id);
19456
+ if (!target) {
19457
+ errors.push({ unit: unit2.id, message: `retire targets unknown node \`${id}\`` });
19458
+ continue;
19459
+ }
19460
+ const fromStatus = target.status;
19461
+ target.status = "archived";
19462
+ const metadata = target.metadata ?? {};
19463
+ target.metadata = { ...metadata, retired_reason: retire.reason };
19464
+ counts.nodesRetired += 1;
19465
+ if (fromStatus !== "archived") {
19466
+ const ts = typeof retire.changed_ts === "string" && retire.changed_ts ? retire.changed_ts : input.fallbackTs;
19467
+ mintEvent(
19468
+ unit2.id,
19469
+ ts,
19470
+ { type: "node.status_changed", node_id: id, from: fromStatus, to: "archived" },
19471
+ `cannot mint a node.status_changed event for \`${id}\` (retire, ts ${JSON.stringify(ts)})`
19472
+ );
19473
+ }
19474
+ }
19475
+ }
19476
+ for (const { unit: unit2, fragment } of input.fragments) {
19477
+ for (const raw of fragment.edges ?? []) {
19478
+ const source = String(raw.source_id ?? "");
19479
+ const target = String(raw.target_id ?? "");
19480
+ if (!nodes.has(source) || !nodes.has(target)) {
19481
+ errors.push({
19482
+ unit: unit2.id,
19483
+ message: `edge ${source} -> ${target} references ${!nodes.has(source) ? source : target}, which no fragment created`
19484
+ });
19485
+ continue;
19486
+ }
19487
+ const kindValue = raw.kind;
19488
+ const edgeTypeValue = raw.edge_type;
19489
+ const resolvedType = typeof edgeTypeValue === "string" ? edgeTypeValue : typeof kindValue === "string" ? kindValue : void 0;
19490
+ if (!resolvedType) {
19491
+ errors.push({ unit: unit2.id, message: `edge ${source} -> ${target} has no \`kind\` (its edge type)` });
19492
+ continue;
19493
+ }
19494
+ const id = edgeId(source, target);
19495
+ if (edges.has(id)) {
19496
+ const existingType = String(edges.get(id)?.edge_type ?? "");
19497
+ const owner = edgeOrigin.get(id);
19498
+ if (existingType !== resolvedType) {
19499
+ errors.push({
19500
+ unit: unit2.id,
19501
+ message: `edge ${source} -> ${target} disagrees on type: "${existingType}" (from ${owner}) vs "${resolvedType}" (from ${unit2.id}) \u2014 an edge id encodes only its endpoints, so it can hold exactly one type.`
19502
+ });
19503
+ }
19504
+ continue;
19505
+ }
19506
+ const edge = { ...raw, id, project_id: projectId, source_id: source, target_id: target, edge_type: resolvedType };
19507
+ delete edge.kind;
19508
+ edges.set(id, edge);
19509
+ edgeOrigin.set(id, unit2.id);
19510
+ counts.edgesAdded += 1;
19511
+ }
19512
+ }
19513
+ for (const { unit: unit2, fragment } of input.fragments) {
19514
+ for (const raw of fragment.events ?? []) {
19515
+ const type = String(raw.type ?? "");
19516
+ const ts = String(raw.ts ?? "");
19517
+ if (!type || !ts) {
19518
+ errors.push({ unit: unit2.id, message: "an event is missing `type` or `ts`" });
19519
+ continue;
19520
+ }
19521
+ if (typeof raw.id === "string" && raw.id) {
19522
+ pushEvent(unit2.id, { ...raw, id: raw.id, ts, type });
19523
+ continue;
19524
+ }
19525
+ try {
19526
+ const id = deterministicEventId(ts, eventKey(raw, ts));
19527
+ pushEvent(unit2.id, { ...raw, id, ts, type });
19528
+ } catch (err) {
19529
+ errors.push({ unit: unit2.id, message: `cannot mint an id for a \`${type}\` event: ${err instanceof Error ? err.message : String(err)}` });
19530
+ }
19531
+ }
19532
+ }
19533
+ const { journal, added, conflicts } = mergeJournal(input.baseJournal, newEvents);
19534
+ counts.eventsAdded = added;
19535
+ for (const conflict of conflicts) {
19536
+ const unitId = eventUnit.get(conflict.id);
19537
+ errors.push({
19538
+ unit: unitId,
19539
+ message: `event \`${conflict.id}\` already exists in the journal with different content \u2014 the committed copy always wins, but this looks like a real correction, not a harmless re-run. Committed: ${JSON.stringify(conflict.base)}. Freshly derived: ${JSON.stringify(conflict.fresh)}. merge never rewrites committed history \u2014 correct the committed event by hand (or via \`arkaik log\`) if it's wrong.`
19540
+ });
19541
+ }
19542
+ const bundle = {
19543
+ ...input.base,
19544
+ nodes: [...nodes.values()],
19545
+ edges: [...edges.values()]
19546
+ };
19547
+ delete bundle.journal;
19548
+ return { bundle, journal, errors, counts };
19549
+ }
19550
+
19551
+ // src/lib/bootstrap/slice.ts
19552
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
19553
+
19554
+ // src/lib/bootstrap/body-budget.ts
19555
+ var LAB_NOTE_HEADING_RE = /^##\s+Lab Note.*$/m;
19556
+ var NEXT_HEADING_RE = /^##\s+/m;
19557
+ var MAX_BODY_CHARS = 4e3;
19558
+ function splitLabNoteSection(body) {
19559
+ const heading = LAB_NOTE_HEADING_RE.exec(body);
19560
+ if (!heading) return null;
19561
+ const noteStart = heading.index;
19562
+ const restStart = noteStart + heading[0].length;
19563
+ const next = NEXT_HEADING_RE.exec(body.slice(restStart));
19564
+ const noteEnd = next ? restStart + next.index : body.length;
19565
+ return { before: body.slice(0, noteStart), note: body.slice(noteStart, noteEnd), after: body.slice(noteEnd) };
19566
+ }
19567
+ function truncationMarker(cutChars) {
19568
+ return `
19569
+
19570
+ [\u2026 truncated ${cutChars} characters \u2026]
19571
+
19572
+ `;
19573
+ }
19574
+ function headTruncate(text, budget) {
19575
+ if (text.length <= budget) return text;
19576
+ const cut = text.length - budget;
19577
+ return `${text.slice(0, budget)}${truncationMarker(cut)}`;
19578
+ }
19579
+ function boundAroundNote(section) {
19580
+ const remaining = Math.max(0, MAX_BODY_CHARS - section.note.length);
19581
+ const beforeBudget = Math.floor(remaining / 2);
19582
+ const afterBudget = remaining - beforeBudget;
19583
+ return `${headTruncate(section.before, beforeBudget)}${section.note}${headTruncate(section.after, afterBudget)}`;
19584
+ }
19585
+ function boundBody(body) {
19586
+ if (body.length <= MAX_BODY_CHARS) return body;
19587
+ const section = splitLabNoteSection(body);
19588
+ const bounded = section ? boundAroundNote(section) : headTruncate(body, MAX_BODY_CHARS);
19589
+ return bounded.length < body.length ? bounded : body;
19590
+ }
19591
+
19592
+ // src/lib/bootstrap/slice.ts
19593
+ function readJsonArray(file2) {
19594
+ if (!existsSync11(file2)) return [];
19595
+ const parsed = JSON.parse(readFileSync13(file2, "utf8"));
19596
+ return Array.isArray(parsed) ? parsed : [];
19597
+ }
19598
+ function toPosix(value) {
19599
+ return value.includes("\\") ? value.split("\\").join("/") : value;
19600
+ }
19601
+ function matchesPaths(filePath, paths) {
19602
+ const file2 = toPosix(filePath);
19603
+ return paths.some((raw) => {
19604
+ const p = toPosix(raw);
19605
+ return file2 === p || file2.startsWith(p.endsWith("/") ? p : `${p}/`);
19606
+ });
19607
+ }
19608
+ function eraWindows(cwd, slugs) {
19609
+ if (slugs.length === 0) return [];
19610
+ const profile = readProfile(cwd);
19611
+ const bySlug = new Map((profile?.eras ?? []).map((e) => [e.slug, e]));
19612
+ return slugs.map((slug) => {
19613
+ const era = bySlug.get(slug);
19614
+ if (!era) {
19615
+ throw new Error(
19616
+ `era "${slug}" is not declared in ${PROFILE_FILE}'s "eras" list, but a work unit's slice references it. profile.json may have been edited (or the era removed) since \`arkaik bootstrap plan\` last ran. Restore the era in profile.json and re-run \`arkaik bootstrap plan\`, or reconcile the manifest by hand.`
19617
+ );
19618
+ }
19619
+ assertEraWindow(era);
19620
+ return { from: era.from, to: era.to };
19621
+ });
19622
+ }
19623
+ function matchesEras(mergedAt, windows) {
19624
+ const merged = Date.parse(mergedAt);
19625
+ if (Number.isNaN(merged)) return false;
19626
+ return windows.some((w) => {
19627
+ const start = w.from !== void 0 ? eraStart(w.from) : void 0;
19628
+ const end = w.to !== void 0 ? eraEnd(w.to) : void 0;
19629
+ const usableStart = start !== void 0 && !Number.isNaN(start) ? start : void 0;
19630
+ const usableEnd = end !== void 0 && !Number.isNaN(end) ? end : void 0;
19631
+ if (usableStart === void 0 && usableEnd === void 0) return false;
19632
+ if (usableStart !== void 0 && merged < usableStart) return false;
19633
+ if (usableEnd !== void 0 && merged >= usableEnd) return false;
19634
+ return true;
19635
+ });
19636
+ }
19637
+ function resolveSlice(cwd, unit2) {
19638
+ const paths = unit2.slice.paths ?? [];
19639
+ const eraSlugs = unit2.slice.eras ?? [];
19640
+ const allPrs = readCorpusPrs(cwd);
19641
+ const surfaces = readJsonArray(at(cwd, SURFACES_FILE));
19642
+ let prs;
19643
+ let surfacesOut;
19644
+ if (paths.length > 0) {
19645
+ prs = allPrs.filter((pr) => pr.files.some((f) => matchesPaths(f, paths)));
19646
+ surfacesOut = surfaces.filter((s) => matchesPaths(s.path, paths));
19647
+ } else if (eraSlugs.length > 0) {
19648
+ const windows = eraWindows(cwd, eraSlugs);
19649
+ prs = allPrs.filter((pr) => matchesEras(pr.merged_at, windows));
19650
+ surfacesOut = [];
19651
+ } else {
19652
+ prs = allPrs;
19653
+ surfacesOut = surfaces;
19654
+ }
19655
+ prs = prs.map((pr) => ({ ...pr, body: boundBody(pr.body) }));
19656
+ const slice = {
19657
+ unit: unit2.id,
19658
+ wave: unit2.wave,
19659
+ scope: unit2.scope,
19660
+ fragment: unit2.fragment,
19661
+ prs,
19662
+ surfaces: surfacesOut
19663
+ };
19664
+ if (unit2.slice.docs) slice.docs = readJsonArray(at(cwd, DOCS_FILE));
19665
+ return slice;
19666
+ }
19667
+
19668
+ // src/commands/bootstrap.ts
19669
+ var USAGE12 = `arkaik bootstrap <subcommand> [options]
19670
+
19671
+ Subcommands:
19672
+ corpus [options] Mine merged PRs, docs and surfaces into .arkaik/corpus/.
19673
+ plan [options] Emit the work-unit manifest (--issues files GitHub issues).
19674
+ slice <unit> Print exactly the corpus subset one work unit needs.
19675
+ index [path] Print a compact id/title/species listing of the map.
19676
+ merge [options] Assemble fragments onto the bundle, then validate.
19677
+
19678
+ Options:
19679
+ -h, --help Show this help.
19680
+
19681
+ Run "arkaik bootstrap <subcommand> --help" for subcommand help.`;
19682
+ var CORPUS_USAGE = `arkaik bootstrap corpus [options]
19683
+
19684
+ Mine merged PRs, design docs and code surfaces into .arkaik/corpus/.
19685
+
19686
+ Options:
19687
+ --from-json <file> Replay a captured \`gh pr list --json\` payload instead of calling gh.
19688
+ --from-git Mine merge commits with git instead of gh (loses Lab Notes).
19689
+ --limit <n> Max PRs to fetch from gh (default: 1000).
19690
+ --since <iso-date> Keep only PRs merged at or after this date.
19691
+ -h, --help Show this help.`;
19692
+ function fail11(message) {
19693
+ console.error(message);
19694
+ process.exit(1);
19695
+ }
19696
+ function nextValue(argv, i, flag, usage) {
19697
+ const value = argv[i];
19698
+ if (value === void 0) fail11(`Missing value for ${flag}
19699
+
19700
+ ${usage}`);
19701
+ return value;
19702
+ }
19703
+ function writeFileAtomic(filePath, content) {
19704
+ const tmpPath = `${filePath}.tmp-${process.pid}`;
19705
+ writeFileSync12(tmpPath, content);
19706
+ renameSync(tmpPath, filePath);
19707
+ }
19708
+ function runCorpus(argv) {
19709
+ const cwd = process.cwd();
19710
+ let fromJson;
19711
+ let fromGit = false;
19712
+ let limit = 1e3;
19713
+ let since;
19714
+ for (let i = 0; i < argv.length; i += 1) {
19715
+ const arg = argv[i];
19716
+ if (arg === "-h" || arg === "--help") {
19717
+ console.log(CORPUS_USAGE);
19718
+ process.exit(0);
19719
+ } else if (arg === "--from-json") {
19720
+ fromJson = nextValue(argv, ++i, "--from-json", CORPUS_USAGE);
19721
+ } else if (arg === "--from-git") {
19722
+ fromGit = true;
19723
+ } else if (arg === "--limit") {
19724
+ const raw = nextValue(argv, ++i, "--limit", CORPUS_USAGE);
19725
+ const parsed = Number(raw);
19726
+ if (!Number.isInteger(parsed) || parsed <= 0) {
19727
+ fail11(`--limit must be a positive integer, got: ${raw}
19728
+
19729
+ ${CORPUS_USAGE}`);
19730
+ }
19731
+ limit = parsed;
19732
+ } else if (arg === "--since") {
19733
+ since = nextValue(argv, ++i, "--since", CORPUS_USAGE);
19734
+ } else {
19735
+ fail11(`Unknown option: ${arg}
19736
+
19737
+ ${CORPUS_USAGE}`);
19738
+ }
19739
+ }
19740
+ if (!existsSync12(path5.join(cwd, ".git"))) {
19741
+ fail11("`arkaik bootstrap corpus` must run from the repository root (no .git here).");
19742
+ }
19743
+ try {
19744
+ const result = buildCorpus({ cwd, fromJson, fromGit, limit, since });
19745
+ const ignored = ensureGitignored(cwd);
19746
+ console.log(`Corpus written to ${CORPUS_DIR}/`);
19747
+ console.log(` ${result.prs} merged PRs, ${result.docs} docs, ${result.surfaces} surfaces`);
19748
+ if (result.sinceDroppedUndated > 0) {
19749
+ console.log(` --since also dropped ${result.sinceDroppedUndated} PR(s) with a missing/unparseable merge date`);
19750
+ }
19751
+ if (ignored) console.log(` added ${BOOTSTRAP_ROOT}/ to .gitignore`);
19752
+ } catch (err) {
19753
+ console.error(err instanceof Error ? err.message : String(err));
19754
+ process.exit(1);
19755
+ }
19756
+ }
19757
+ var PLAN_USAGE = `arkaik bootstrap plan [options]
19758
+
19759
+ Emit the work-unit manifest at .arkaik/bootstrap/manifest.json. With no recon
19760
+ profile only the wave-0 recon unit is planned; re-run after recon writes
19761
+ profile.json to expand waves 1-3. Existing unit statuses are preserved for
19762
+ units whose scope/slice is unchanged since the last plan.
19763
+
19764
+ Options:
19765
+ --bundle <path> Bundle to bootstrap (default: docs/arkaik/bundle.json).
19766
+ --issues File one GitHub issue per pending unit instead of driving
19767
+ in-session. Same manifest, alternate driver: durable and
19768
+ parallel across machines, at the cost of a cold-start
19769
+ context tax per unit. A unit that already has a filed
19770
+ issue (tracked locally in manifest.json, not checked
19771
+ against GitHub) is skipped, not re-filed.
19772
+ --print With --issues, print the rendered issues as JSON instead
19773
+ of filing them \u2014 never calls \`gh\`. Requires --issues.
19774
+ -h, --help Show this help.`;
19775
+ function runPlan(argv) {
19776
+ const cwd = process.cwd();
19777
+ let bundle = "docs/arkaik/bundle.json";
19778
+ let issues = false;
19779
+ let print = false;
19780
+ for (let i = 0; i < argv.length; i += 1) {
19781
+ const arg = argv[i];
19782
+ if (arg === "-h" || arg === "--help") {
19783
+ console.log(PLAN_USAGE);
19784
+ process.exit(0);
19785
+ } else if (arg === "--bundle") {
19786
+ bundle = nextValue(argv, ++i, "--bundle", PLAN_USAGE);
19787
+ } else if (arg === "--issues") {
19788
+ issues = true;
19789
+ } else if (arg === "--print") {
19790
+ print = true;
19791
+ } else {
19792
+ fail11(`Unknown option: ${arg}
19793
+
19794
+ ${PLAN_USAGE}`);
19795
+ }
19796
+ }
19797
+ if (print && !issues) {
19798
+ fail11(`--print requires --issues
19799
+
19800
+ ${PLAN_USAGE}`);
19801
+ }
19802
+ if (!existsSync12(path5.join(cwd, ".git"))) {
19803
+ fail11("`arkaik bootstrap plan` must run from the repository root (no .git here).");
19804
+ }
19805
+ try {
19806
+ const mode = detectMode(cwd, bundle);
19807
+ const manifest = planUnits({ mode, bundle, profile: readProfile(cwd), previous: readManifest(cwd) });
19808
+ writeManifest(cwd, manifest);
19809
+ const ignored = ensureGitignored(cwd);
19810
+ if (ignored) console.log(` added ${BOOTSTRAP_ROOT}/ to .gitignore`);
19811
+ if (issues) {
19812
+ runPlanIssues(cwd, manifest, print);
19813
+ return;
19814
+ }
19815
+ const pending = manifest.units.filter((u) => u.status === "pending").length;
19816
+ console.log(`Planned ${manifest.units.length} units (${pending} pending) in ${mode} mode.`);
19817
+ for (const u of manifest.units) console.log(` [${u.status}] w${u.wave} ${u.id} \u2014 ${u.title}`);
19818
+ } catch (err) {
19819
+ console.error(err instanceof Error ? err.message : String(err));
19820
+ process.exit(1);
19821
+ }
19822
+ }
19823
+ function runPlanIssues(cwd, manifest, print) {
19824
+ const rendered = renderIssues(manifest);
19825
+ if (print) {
19826
+ console.log(JSON.stringify(rendered, null, 2));
19827
+ return;
19828
+ }
19829
+ if (rendered.length === 0) {
19830
+ console.log("Every pending unit already has a filed issue \u2014 nothing to do.");
19831
+ return;
19832
+ }
19833
+ let filedThisRun = 0;
19834
+ for (const issue2 of rendered) {
19835
+ const res = spawnSync2("gh", ["issue", "create", "--title", issue2.title, "--body", issue2.body], {
19836
+ cwd,
19837
+ encoding: "utf8"
19838
+ });
19839
+ if (res.error) {
19840
+ console.error(`gh not runnable: ${res.error.message}`);
19841
+ process.exit(1);
19842
+ }
19843
+ if (res.status !== 0) {
19844
+ console.error(`gh issue create failed for ${issue2.unit}: ${(res.stderr ?? "").trim()}`);
19845
+ console.error(
19846
+ `${filedThisRun} of ${rendered.length} issue(s) filed before this failure \u2014 re-run \`arkaik bootstrap plan --issues\` after fixing the problem above; already-filed units are not re-filed.`
19847
+ );
19848
+ process.exit(1);
19849
+ }
19850
+ const url2 = res.stdout.trim();
19851
+ const target = manifest.units.find((u) => u.id === issue2.unit);
19852
+ if (target) target.issueUrl = url2;
19853
+ filedThisRun += 1;
19854
+ writeManifest(cwd, manifest);
19855
+ console.log(`Filed ${issue2.unit}: ${url2}`);
19856
+ }
19857
+ }
19858
+ var SLICE_USAGE = `arkaik bootstrap slice <unit>
19859
+
19860
+ Print exactly the corpus subset one work unit needs, as compact JSON (id,
19861
+ scope, matching PRs, matching surfaces, and \u2014 only when the unit asks for it
19862
+ \u2014 the docs manifest). Reads .arkaik/bootstrap/manifest.json; run
19863
+ \`arkaik bootstrap plan\` first.`;
19864
+ function runSlice(argv) {
19865
+ const cwd = process.cwd();
19866
+ const unitId = argv[0];
19867
+ if (unitId === "-h" || unitId === "--help") {
19868
+ console.log(SLICE_USAGE);
19869
+ process.exit(0);
19870
+ }
19871
+ if (unitId === void 0) fail11(`Missing required argument: <unit>
19872
+
19873
+ ${SLICE_USAGE}`);
19874
+ const manifest = readManifest(cwd);
19875
+ if (!manifest) fail11("No manifest. Run `arkaik bootstrap plan` first.");
19876
+ const unit2 = manifest.units.find((u) => u.id === unitId);
19877
+ if (!unit2) fail11(`Unknown unit: ${unitId}
19878
+ Known units: ${manifest.units.map((u) => u.id).join(", ")}`);
19879
+ try {
19880
+ console.log(JSON.stringify(resolveSlice(cwd, unit2)));
19881
+ } catch (err) {
19882
+ console.error(err instanceof Error ? err.message : String(err));
19883
+ process.exit(1);
19884
+ }
19885
+ }
19886
+ var INDEX_USAGE = `arkaik bootstrap index [path]
19887
+
19888
+ Print a compact id/species/title/product listing of the map, one
19889
+ tab-separated line per node (default: docs/arkaik/bundle.json).`;
19890
+ function runIndex(argv) {
19891
+ if (argv[0] === "-h" || argv[0] === "--help") {
19892
+ console.log(INDEX_USAGE);
19893
+ process.exit(0);
19894
+ }
19895
+ if (argv[0] !== void 0 && argv[0].startsWith("-")) {
19896
+ fail11(`Unknown option: ${argv[0]}
19897
+
19898
+ ${INDEX_USAGE}`);
19899
+ }
19900
+ const target = argv[0] ?? path5.join("docs", "arkaik", "bundle.json");
19901
+ try {
19902
+ process.stdout.write(renderIndex(readBundle(path5.resolve(process.cwd(), target))));
19903
+ } catch (err) {
19904
+ console.error(err instanceof Error ? err.message : String(err));
19905
+ process.exit(1);
19906
+ }
19907
+ }
19908
+ var MERGE_USAGE = `arkaik bootstrap merge [options]
19909
+
19910
+ Assemble every fragment named by the manifest onto the bundle: verify ID
19911
+ uniqueness, resolve edge endpoints, apply reconcile ops, synthesize the
19912
+ required node.created / node.status_changed / decision.status_changed
19913
+ events, validate the result (errors block the write; warnings are reported
19914
+ but never block it), and write the bundle plus its journal.jsonl sidecar.
19915
+
19916
+ Options:
19917
+ --dry-run Report what would change (including validation); write nothing.
19918
+ -h, --help Show this help.`;
19919
+ function runMerge(argv) {
19920
+ const cwd = process.cwd();
19921
+ let dryRun = false;
19922
+ for (let i = 0; i < argv.length; i += 1) {
19923
+ const arg = argv[i];
19924
+ if (arg === "-h" || arg === "--help") {
19925
+ console.log(MERGE_USAGE);
19926
+ process.exit(0);
19927
+ } else if (arg === "--dry-run") {
19928
+ dryRun = true;
19929
+ } else {
19930
+ fail11(`Unknown option: ${arg}
19931
+
19932
+ ${MERGE_USAGE}`);
19933
+ }
19934
+ }
19935
+ const manifest = readManifest(cwd);
19936
+ if (!manifest) {
19937
+ fail11("No manifest. Run `arkaik bootstrap plan` first.");
19938
+ }
19939
+ try {
19940
+ const bundlePath = path5.resolve(cwd, manifest.bundle);
19941
+ const base = existsSync12(bundlePath) ? readBundle(bundlePath) : {
19942
+ schema_version: 3,
19943
+ project: {
19944
+ id: path5.basename(cwd),
19945
+ title: path5.basename(cwd),
19946
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
19947
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
19948
+ },
19949
+ nodes: [],
19950
+ edges: []
19951
+ };
19952
+ const baseJournal = loadJournalEvents(base, bundlePath);
19953
+ const { loaded, problems, missing } = loadFragments(cwd, manifest);
19954
+ for (const problem of problems) console.error(`fragment ${problem.unit}: ${problem.message}`);
19955
+ if (problems.length > 0) process.exit(1);
19956
+ const fallbackTs = String(base.project?.created_at ?? (/* @__PURE__ */ new Date()).toISOString());
19957
+ const result = mergeFragments({ base, baseJournal, fragments: loaded, fallbackTs });
19958
+ for (const error51 of result.errors) console.error(`merge ${error51.unit}: ${error51.message}`);
19959
+ if (result.errors.length > 0) process.exit(1);
19960
+ const project = result.bundle.project;
19961
+ const lastTs = result.journal.length > 0 ? String(result.journal[result.journal.length - 1].ts) : void 0;
19962
+ result.bundle.project = { ...project, updated_at: lastTs ?? project.updated_at };
19963
+ const validation = validateBundle({ ...result.bundle, journal: result.journal });
19964
+ if (validation.warnings.length > 0) {
19965
+ console.log(` ${validation.warnings.length} validation warning(s):`);
19966
+ for (const warning of validation.warnings) console.log(` ${formatFinding(warning)}`);
19967
+ }
19968
+ if (validation.errors.length > 0) {
19969
+ for (const error51 of validation.errors) console.error(formatFinding(error51));
19970
+ console.error("Merged bundle fails validation \u2014 nothing was written. Fix the fragment(s) above and re-run.");
19971
+ process.exit(1);
19972
+ }
19973
+ const serialized = serializeBundle(result.bundle);
19974
+ const journalPath = journalPathFor(bundlePath);
19975
+ const journalText = result.journal.map((e) => JSON.stringify(e)).join("\n") + (result.journal.length ? "\n" : "");
19976
+ if (!dryRun) {
19977
+ mkdirSync7(path5.dirname(bundlePath), { recursive: true });
19978
+ writeFileAtomic(bundlePath, serialized);
19979
+ writeFileAtomic(journalPath, journalText);
19980
+ }
19981
+ console.log(`${dryRun ? "[dry-run] " : ""}Merged ${loaded.length} fragments:`);
19982
+ console.log(
19983
+ ` +${result.counts.nodesAdded} nodes, ~${result.counts.nodesUpdated} updated, ${result.counts.nodesRetired} retired, +${result.counts.edgesAdded} edges, +${result.counts.eventsAdded} events`
19984
+ );
19985
+ console.log(` bundle ${(Buffer.byteLength(serialized) / 1024).toFixed(0)}KB, journal ${result.journal.length} events`);
19986
+ if (missing.length > 0) console.log(` ${missing.length} units have no fragment yet: ${missing.join(", ")}`);
19987
+ const wroteOrWould = dryRun ? "would be written" : "written";
19988
+ console.log(
19989
+ validation.warnings.length > 0 ? `Validated (0 errors, ${validation.warnings.length} warnings above) \u2014 ${wroteOrWould}.` : `Validated clean \u2014 ${wroteOrWould}.`
19990
+ );
19991
+ } catch (err) {
19992
+ console.error(err instanceof Error ? err.message : String(err));
19993
+ process.exit(1);
19994
+ }
19995
+ }
19996
+ function runBootstrap(argv) {
19997
+ const [sub, ...rest] = argv;
19998
+ if (sub === void 0 || sub === "-h" || sub === "--help" || sub === "help") {
19999
+ console.log(USAGE12);
20000
+ process.exit(0);
20001
+ }
20002
+ switch (sub) {
20003
+ case "corpus":
20004
+ runCorpus(rest);
20005
+ return;
20006
+ case "plan":
20007
+ runPlan(rest);
20008
+ return;
20009
+ case "slice":
20010
+ runSlice(rest);
20011
+ return;
20012
+ case "index":
20013
+ runIndex(rest);
20014
+ return;
20015
+ case "merge":
20016
+ runMerge(rest);
20017
+ return;
20018
+ default:
20019
+ console.error(`Unknown bootstrap subcommand: ${sub}
20020
+
20021
+ ${USAGE12}`);
20022
+ process.exit(1);
20023
+ }
20024
+ }
20025
+
20026
+ // src/index.ts
20027
+ var USAGE13 = `arkaik \u2014 CLI for Arkaik project bundles
20028
+
20029
+ Usage:
20030
+ arkaik <command> [options]
20031
+
20032
+ Commands:
20033
+ init [options] Scaffold docs/arkaik/, install the agent skill (--update to upgrade).
20034
+ validate [path] Validate a project bundle (folds in a journal.jsonl sidecar).
20035
+ log [--node <id>] [path] Print the journal: project changelog, or one node's timeline.
20036
+ release <version> [path] Tag a release (append release.tagged) and draft its notes.
20037
+ deliverable <title> [path] Record a deliverable (append deliverable.shipped).
20038
+ sync [options] [path] Mirror external ref status (GitHub issues/PRs) into node refs.
20039
+ pack [options] [path] Produce a single self-contained interchange bundle (embeds the journal).
20040
+ open [options] [path] Validate, then hand off the packed bundle to arkaik.app import.
20041
+ push [options] [path] Validate, pack (journal stripped), and publish to Publik.
20042
+ --delete <id> --key <owner_key> removes a snapshot.
20043
+ link [options] [path] Point this repo at a hosted project so an agent can edit it.
20044
+ --list shows the projects your token can reach.
20045
+ restore [options] [path] Replace the linked hosted project's bundle + journal (backs up first).
20046
+ bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
20047
+
20048
+ Options:
20049
+ -h, --help Show this help.
20050
+ -v, --version Print the version.
20051
+
20052
+ Run "arkaik <command> --help" for command-specific help.`;
20053
+ var VERSION = "0.1.1";
20054
+ function main(argv) {
20055
+ const [command, ...rest] = argv;
20056
+ if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
20057
+ console.log(USAGE13);
20058
+ process.exit(0);
20059
+ }
20060
+ if (command === "--version" || command === "-v" || command === "version") {
20061
+ console.log(VERSION);
20062
+ process.exit(0);
20063
+ }
20064
+ switch (command) {
20065
+ case "init":
20066
+ runInit(rest);
20067
+ return;
20068
+ case "validate":
20069
+ runValidate(rest);
20070
+ return;
20071
+ case "log":
20072
+ runLog(rest);
20073
+ return;
20074
+ case "release":
20075
+ runRelease(rest);
20076
+ return;
20077
+ case "deliverable":
20078
+ runDeliverable(rest);
20079
+ return;
20080
+ case "sync":
20081
+ runSyncCli(rest);
20082
+ return;
20083
+ case "pack":
20084
+ runPackCli(rest);
20085
+ return;
20086
+ case "open":
20087
+ runOpenCli(rest);
20088
+ return;
20089
+ case "push":
20090
+ runPushCli(rest);
20091
+ return;
20092
+ case "link":
20093
+ runLinkCli(rest);
20094
+ return;
20095
+ case "restore":
20096
+ runRestoreCli(rest);
20097
+ return;
20098
+ case "bootstrap":
20099
+ runBootstrap(rest);
17685
20100
  return;
17686
20101
  default:
17687
20102
  console.error(`Unknown command: ${command}
17688
20103
  `);
17689
- console.error(USAGE10);
20104
+ console.error(USAGE13);
17690
20105
  process.exit(1);
17691
20106
  }
17692
20107
  }