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/assets/bootstrap-skill/references/fragments.md +203 -0
- package/dist/assets/bootstrap-skill/references/waves.md +120 -0
- package/dist/assets/bootstrap-skill/skill.md +190 -0
- package/dist/assets/skill/references/schema.md +76 -3
- package/dist/assets/skill/scripts/validate-bundle.js +4 -4
- package/dist/assets/skill/skill.md +104 -6
- package/dist/index.js +2696 -281
- package/dist/io.js +497 -61
- package/package.json +1 -1
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,
|
|
787
|
-
if (!
|
|
786
|
+
function getElementAtPath(obj, path6) {
|
|
787
|
+
if (!path6)
|
|
788
788
|
return obj;
|
|
789
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 }, [...
|
|
1352
|
+
issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
|
|
1353
1353
|
} else if (issue2.code === "invalid_key") {
|
|
1354
|
-
processError({ issues: issue2.issues }, [...
|
|
1354
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
1355
1355
|
} else if (issue2.code === "invalid_element") {
|
|
1356
|
-
processError({ issues: issue2.issues }, [...
|
|
1356
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
1357
1357
|
} else {
|
|
1358
|
-
const fullpath = [...
|
|
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,
|
|
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 }, [...
|
|
1389
|
+
issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
|
|
1390
1390
|
} else if (issue2.code === "invalid_key") {
|
|
1391
|
-
processError({ issues: issue2.issues }, [...
|
|
1391
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
1392
1392
|
} else if (issue2.code === "invalid_element") {
|
|
1393
|
-
processError({ issues: issue2.issues }, [...
|
|
1393
|
+
processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
|
|
1394
1394
|
} else {
|
|
1395
|
-
const fullpath = [...
|
|
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
|
|
1428
|
-
for (const seg of
|
|
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
|
|
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()} ${
|
|
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
|
|
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()} ${
|
|
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
|
|
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()} ${
|
|
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
|
|
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()} ${
|
|
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
|
|
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()}${
|
|
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
|
|
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()}${
|
|
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
|
|
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()} ${
|
|
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
|
|
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()} ${
|
|
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
|
|
14121
|
-
if (
|
|
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 (
|
|
14126
|
-
const key =
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
14882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
15130
|
-
const warn = (
|
|
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
|
-
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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,
|
|
15901
|
+
const collectPlaylistRefs = (entries, flowId, path6, depth = 0) => {
|
|
15463
15902
|
if (depth > 50) {
|
|
15464
|
-
error51(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
15484
|
-
if (entry.if_false) refs.push(...collectPlaylistRefs(entry.if_false, flowId,
|
|
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,
|
|
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
|
|
15937
|
+
const path6 = `nodes[${i}].metadata.playlist`;
|
|
15499
15938
|
const nodeId = node.id;
|
|
15500
|
-
const refs = collectPlaylistRefs(md.playlist.entries, nodeId,
|
|
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
|
-
|
|
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 ===
|
|
15849
|
-
skipped.push({ node_id: node.id, ref_id: ref.id, reason: "already-there", detail:
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
16450
|
+
opts.product = nextValue2(++i, arg);
|
|
15930
16451
|
} else if (arg === "--bundle") {
|
|
15931
|
-
opts.bundle =
|
|
16452
|
+
opts.bundle = nextValue2(++i, arg);
|
|
15932
16453
|
} else if (arg === "--journal") {
|
|
15933
|
-
opts.journal =
|
|
16454
|
+
opts.journal = nextValue2(++i, arg);
|
|
15934
16455
|
} else if (arg === "--skills-dir") {
|
|
15935
|
-
opts.skillsDir =
|
|
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(
|
|
15954
|
-
if (existsSync(
|
|
15955
|
-
console.log(`Skipping ${label} (already exists): ${
|
|
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(
|
|
15959
|
-
writeFileSync(
|
|
15960
|
-
console.log(`Created ${
|
|
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
|
-
|
|
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
|
-
|
|
16594
|
-
|
|
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
|
-
|
|
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/
|
|
16615
|
-
|
|
16616
|
-
|
|
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
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
|
|
16637
|
-
|
|
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
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
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
|
|
16708
|
-
var
|
|
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
|
|
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: ${
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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(
|
|
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)
|
|
17651
|
+
if (value === void 0) fail6(`Missing value for --provider
|
|
16937
17652
|
|
|
16938
|
-
${
|
|
17653
|
+
${USAGE6}`);
|
|
16939
17654
|
provider = value;
|
|
16940
17655
|
} else if (arg.startsWith("-")) {
|
|
16941
|
-
|
|
17656
|
+
fail6(`Unknown option: ${arg}
|
|
16942
17657
|
|
|
16943
|
-
${
|
|
17658
|
+
${USAGE6}`);
|
|
16944
17659
|
} else {
|
|
16945
17660
|
positionals.push(arg);
|
|
16946
17661
|
}
|
|
16947
17662
|
}
|
|
16948
|
-
const filePath = positionals[0] ??
|
|
17663
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH4;
|
|
16949
17664
|
runSync({ path: filePath, provider, dryRun, promote }).then((result) => {
|
|
16950
|
-
if (!result.ok)
|
|
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) =>
|
|
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
|
|
16960
|
-
var
|
|
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: ${
|
|
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
|
|
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 ??
|
|
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(
|
|
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)
|
|
17807
|
+
if (value === void 0) fail7(`Missing value for --out
|
|
17093
17808
|
|
|
17094
|
-
${
|
|
17809
|
+
${USAGE7}`);
|
|
17095
17810
|
out = value;
|
|
17096
17811
|
} else if (arg.startsWith("-")) {
|
|
17097
|
-
|
|
17812
|
+
fail7(`Unknown option: ${arg}
|
|
17098
17813
|
|
|
17099
|
-
${
|
|
17814
|
+
${USAGE7}`);
|
|
17100
17815
|
} else {
|
|
17101
17816
|
positionals.push(arg);
|
|
17102
17817
|
}
|
|
17103
17818
|
}
|
|
17104
|
-
const filePath = positionals[0] ??
|
|
17819
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH5;
|
|
17105
17820
|
const result = runPack({ path: filePath, out, noJournal, inlineAssets });
|
|
17106
|
-
if (!result.ok)
|
|
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
|
|
17848
|
+
var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
|
|
17134
17849
|
var OPEN_URL = "https://arkaik.app/projects";
|
|
17135
|
-
var
|
|
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: ${
|
|
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
|
|
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 ??
|
|
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(
|
|
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)
|
|
17928
|
+
if (value === void 0) fail8(`Missing value for --out
|
|
17214
17929
|
|
|
17215
|
-
${
|
|
17930
|
+
${USAGE8}`);
|
|
17216
17931
|
out = value;
|
|
17217
17932
|
} else if (arg.startsWith("-")) {
|
|
17218
|
-
|
|
17933
|
+
fail8(`Unknown option: ${arg}
|
|
17219
17934
|
|
|
17220
|
-
${
|
|
17935
|
+
${USAGE8}`);
|
|
17221
17936
|
} else {
|
|
17222
17937
|
positionals.push(arg);
|
|
17223
17938
|
}
|
|
17224
17939
|
}
|
|
17225
|
-
const filePath = positionals[0] ??
|
|
17940
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
|
|
17226
17941
|
runOpen({ path: filePath, out, noOpen }).then((result) => {
|
|
17227
|
-
if (!result.ok)
|
|
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) =>
|
|
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
|
|
17965
|
+
var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
|
|
17251
17966
|
var DEFAULT_API_BASE = "https://arkaik.app";
|
|
17252
|
-
var
|
|
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: ${
|
|
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
|
|
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 ??
|
|
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)
|
|
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(
|
|
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)
|
|
18191
|
+
if (value === void 0) fail9(`Missing value for --api
|
|
17477
18192
|
|
|
17478
|
-
${
|
|
18193
|
+
${USAGE9}`);
|
|
17479
18194
|
apiBase = value;
|
|
17480
18195
|
} else if (arg === "--delete") {
|
|
17481
18196
|
const value = args[++i];
|
|
17482
|
-
if (value === void 0)
|
|
18197
|
+
if (value === void 0) fail9(`Missing value for --delete
|
|
17483
18198
|
|
|
17484
|
-
${
|
|
18199
|
+
${USAGE9}`);
|
|
17485
18200
|
deleteId = value;
|
|
17486
18201
|
} else if (arg === "--key") {
|
|
17487
18202
|
const value = args[++i];
|
|
17488
|
-
if (value === void 0)
|
|
18203
|
+
if (value === void 0) fail9(`Missing value for --key
|
|
17489
18204
|
|
|
17490
|
-
${
|
|
18205
|
+
${USAGE9}`);
|
|
17491
18206
|
key = value;
|
|
17492
18207
|
} else if (arg.startsWith("-")) {
|
|
17493
|
-
|
|
18208
|
+
fail9(`Unknown option: ${arg}
|
|
17494
18209
|
|
|
17495
|
-
${
|
|
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)
|
|
18216
|
+
if (key === void 0) fail9(`--delete requires --key <owner_key>
|
|
17502
18217
|
|
|
17503
|
-
${
|
|
18218
|
+
${USAGE9}`);
|
|
17504
18219
|
if (positionals.length > 0) {
|
|
17505
|
-
|
|
18220
|
+
fail9(`Unexpected argument(s) with --delete: ${positionals.join(" ")}
|
|
17506
18221
|
|
|
17507
|
-
${
|
|
18222
|
+
${USAGE9}`);
|
|
17508
18223
|
}
|
|
17509
|
-
runPushDelete({ id: deleteId, key, apiBase }).then((result) => reportDelete(deleteId, result)).catch((e) =>
|
|
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)
|
|
18227
|
+
if (key !== void 0) fail9(`--key is only valid with --delete
|
|
17513
18228
|
|
|
17514
|
-
${
|
|
17515
|
-
const filePath = positionals[0] ??
|
|
18229
|
+
${USAGE9}`);
|
|
18230
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
|
|
17516
18231
|
runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
|
|
17517
|
-
if (!result.ok)
|
|
18232
|
+
if (!result.ok) fail9(`FATAL: ${result.fatal}`);
|
|
17518
18233
|
reportPush(result);
|
|
17519
|
-
}).catch((e) =>
|
|
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
|
|
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(
|
|
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/
|
|
17630
|
-
|
|
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
|
-
|
|
17633
|
-
|
|
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
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
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
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
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(
|
|
20104
|
+
console.error(USAGE13);
|
|
17690
20105
|
process.exit(1);
|
|
17691
20106
|
}
|
|
17692
20107
|
}
|