@namewta/speculo 1.0.13 → 1.0.15
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/README.md +3 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-goal-plan.md +9 -5
- package/template/canonical/canonical-specdev-grill-with-docs.md +6 -4
- package/template/canonical/canonical-specdev-spec.md +8 -4
- package/template/canonical/canonical-specdev-tickets.md +20 -4
- package/template/commands/handoff.md +1 -1
- package/template/commands/status.md +1 -1
- package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +3 -3
- package/template/workflows/specdev/I-implement/references/implementation-procedure.md +1 -1
- package/template/workflows/specdev/I-init-setup/tracking-template.md +1 -1
- package/template/workflows/specdev/INDEX.md +1 -1
- package/template/workflows/specdev/README.md +4 -4
- package/template/workflows/specdev/T-tickets/ticket-template.md +1 -0
- package/template/workflows/specdev/T-triage/T-triage.md +80 -15
- package/template/workflows/specdev/T-triage/capture-protocol.md +89 -0
- package/template/workflows/specdev/T-triage/capture-template.md +42 -0
- package/template/workflows/specdev/T-triage/close-comment-template.md +42 -0
- package/template/workflows/specdev/T-triage/intake-protocol.md +8 -2
- package/template/workflows/specdev/T-triage/issue-body-template.md +34 -0
- package/template/workflows/specdev/T-triage/issue-record-template.md +27 -0
- package/template/workflows/specdev/T-triage/publish-protocol.md +92 -0
- package/template/workflows/specdev/T-triage/publish-template.md +49 -0
- package/template/workflows/specdev/T-triage/reconcile-protocol.md +3 -3
- package/template/workflows/specdev/T-triage/references/classification-map.md +54 -0
- package/template/workflows/specdev/T-triage/references/public-projection.md +89 -0
- package/template/workflows/specdev/T-triage/tools/capture-status.mjs +125 -0
- package/template/workflows/specdev/T-triage/tools/publish-status.mjs +148 -0
- package/template/workflows/specdev/T-triage/triage-template.md +12 -1
- package/template/workflows/specdev/common/README.md +2 -0
- package/template/workflows/specdev/common/rules/artifact-contract.md +6 -4
- package/template/workflows/specdev/common/rules/change-completion.md +1 -1
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +2 -0
- package/template/workflows/specdev/common/rules/workflow-routing.md +3 -1
- package/template/workflows/specdev/common/rules/workflow-state-and-lifecycle.md +1 -0
- package/template/workflows/specdev/common/schemas/capture.schema.json +15 -0
- package/template/workflows/specdev/common/schemas/publish.schema.json +21 -0
- package/template/workflows/specdev/common/schemas/ticket.schema.json +11 -0
- package/template/workflows/specdev/common/schemas/triage.schema.json +3 -1
- package/template/workflows/specdev/common/tools/README.md +7 -0
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +304 -6
|
@@ -66,6 +66,57 @@ const VALID_TICKET_STATUS = new Set([
|
|
|
66
66
|
"deviated",
|
|
67
67
|
"cancelled",
|
|
68
68
|
]);
|
|
69
|
+
const VALID_TICKET_KIND = new Set([
|
|
70
|
+
"bug",
|
|
71
|
+
"feature",
|
|
72
|
+
"refactor",
|
|
73
|
+
"investigation",
|
|
74
|
+
"operations",
|
|
75
|
+
"documentation",
|
|
76
|
+
"review",
|
|
77
|
+
]);
|
|
78
|
+
const VALID_TRIAGE_MODE = new Set(["intake", "reconcile", "publish"]);
|
|
79
|
+
const VALID_PUBLISH_ACTION = new Set([
|
|
80
|
+
"not-requested",
|
|
81
|
+
"pending",
|
|
82
|
+
"published",
|
|
83
|
+
"publish-failed",
|
|
84
|
+
"waived",
|
|
85
|
+
]);
|
|
86
|
+
const VALID_PUBLISH_ROW_STATE = new Set([
|
|
87
|
+
"planned",
|
|
88
|
+
"created",
|
|
89
|
+
"commented",
|
|
90
|
+
"closed",
|
|
91
|
+
"skipped:cancelled",
|
|
92
|
+
"skipped:excluded",
|
|
93
|
+
"failed",
|
|
94
|
+
]);
|
|
95
|
+
const FORBIDDEN_PUBLISH_LABELS = new Set([
|
|
96
|
+
"needs-triage",
|
|
97
|
+
"needs-info",
|
|
98
|
+
"ready-for-agent",
|
|
99
|
+
"ready-for-human",
|
|
100
|
+
"wontfix",
|
|
101
|
+
"duplicate",
|
|
102
|
+
"invalid",
|
|
103
|
+
"stale",
|
|
104
|
+
]);
|
|
105
|
+
const COUNTED_PUBLISH_STATES = new Set(["closed", "created", "commented"]);
|
|
106
|
+
const VALID_CAPTURE_ROW_STATE = new Set([
|
|
107
|
+
"planned",
|
|
108
|
+
"open",
|
|
109
|
+
"skipped:duplicate",
|
|
110
|
+
"intaken",
|
|
111
|
+
"waived",
|
|
112
|
+
"failed",
|
|
113
|
+
]);
|
|
114
|
+
const FORBIDDEN_CAPTURE_LABELS = new Set([
|
|
115
|
+
...FORBIDDEN_PUBLISH_LABELS,
|
|
116
|
+
"specdev:published",
|
|
117
|
+
"specdev:change",
|
|
118
|
+
]);
|
|
119
|
+
const COUNTED_CAPTURE_REMOTE_STATES = new Set(["open", "intaken"]);
|
|
69
120
|
const VALID_DEPTH = new Set(["lite", "standard", "deep"]);
|
|
70
121
|
const VALID_RISK = new Set(["low", "medium", "high", "critical"]);
|
|
71
122
|
const VALID_PLAN_MODES = new Set([
|
|
@@ -175,6 +226,8 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
175
226
|
"status.json",
|
|
176
227
|
".status.json",
|
|
177
228
|
"triage.md",
|
|
229
|
+
"publish.md",
|
|
230
|
+
"capture.md",
|
|
178
231
|
"diagnosis.md",
|
|
179
232
|
"source.md",
|
|
180
233
|
"architecture-review.md",
|
|
@@ -973,7 +1026,7 @@ function capabilityChecks(root) {
|
|
|
973
1026
|
"triage",
|
|
974
1027
|
[
|
|
975
1028
|
join(root, "T-triage", "T-triage.md"),
|
|
976
|
-
["source.md", "intake", "reconcile", "唯一权威", "远程写入为零"],
|
|
1029
|
+
["source.md", "intake", "reconcile", "publish", "publish.md", "specdev:published", "capture", "capture.md", "specdev:captured", "唯一权威", "远程写入为零"],
|
|
977
1030
|
],
|
|
978
1031
|
],
|
|
979
1032
|
[
|
|
@@ -1061,7 +1114,7 @@ function capabilityChecks(root) {
|
|
|
1061
1114
|
"archive",
|
|
1062
1115
|
[
|
|
1063
1116
|
join(root, "A-archive-and-consolidate", "A-archive-and-consolidate.md"),
|
|
1064
|
-
["archive-single", "dry-run", "external_action", "consolidate-from-code", "archive-and-consolidate"],
|
|
1117
|
+
["archive-single", "dry-run", "external_action", "publish_action", "consolidate-from-code", "archive-and-consolidate"],
|
|
1065
1118
|
],
|
|
1066
1119
|
],
|
|
1067
1120
|
[
|
|
@@ -1102,6 +1155,11 @@ function capabilityChecks(root) {
|
|
|
1102
1155
|
"common/rules/parent-implementation-orchestration.md",
|
|
1103
1156
|
"common/schemas/implementation-map.schema.json",
|
|
1104
1157
|
"common/schemas/implementation-plan.schema.json",
|
|
1158
|
+
"common/schemas/capture.schema.json",
|
|
1159
|
+
"T-triage/capture-protocol.md",
|
|
1160
|
+
"T-triage/capture-template.md",
|
|
1161
|
+
"T-triage/issue-record-template.md",
|
|
1162
|
+
"T-triage/tools/capture-status.mjs",
|
|
1105
1163
|
]) {
|
|
1106
1164
|
if (!isFile(join(root, required))) errors.push(`missing architecture/wayfinding contract ${required}`);
|
|
1107
1165
|
}
|
|
@@ -1327,7 +1385,7 @@ function validateTriage(path, expectedChange, errors) {
|
|
|
1327
1385
|
errors.push("triage.md: artifact/schema_version must be triage/1");
|
|
1328
1386
|
}
|
|
1329
1387
|
if (meta.change !== expectedChange) errors.push("triage.md: change must equal directory name");
|
|
1330
|
-
if (!
|
|
1388
|
+
if (!VALID_TRIAGE_MODE.has(meta.mode)) errors.push(`triage.md: invalid mode ${meta.mode}`);
|
|
1331
1389
|
if (!VALID_RISK.has(meta.risk)) errors.push(`triage.md: invalid risk ${meta.risk}`);
|
|
1332
1390
|
if (typeof meta.route !== "string" || !meta.route.startsWith("specdev/")) {
|
|
1333
1391
|
errors.push("triage.md: route must be a specdev work id");
|
|
@@ -1338,15 +1396,223 @@ function validateTriage(path, expectedChange, errors) {
|
|
|
1338
1396
|
if (!new Set(["not-applicable", "pending-close", "closed", "close-failed", "waived"]).has(meta.external_action)) {
|
|
1339
1397
|
errors.push(`triage.md: invalid external_action ${meta.external_action}`);
|
|
1340
1398
|
}
|
|
1399
|
+
const publishAction = meta.publish_action == null || meta.publish_action === ""
|
|
1400
|
+
? "not-requested"
|
|
1401
|
+
: meta.publish_action;
|
|
1402
|
+
if (!VALID_PUBLISH_ACTION.has(publishAction)) {
|
|
1403
|
+
errors.push(`triage.md: invalid publish_action ${meta.publish_action}`);
|
|
1404
|
+
}
|
|
1405
|
+
meta.publish_action = publishAction;
|
|
1341
1406
|
if (!String(meta.source ?? "").includes("/source.md</Path>")) {
|
|
1342
1407
|
errors.push("triage.md: source must reference the local source.md artifact");
|
|
1343
1408
|
}
|
|
1344
1409
|
for (const heading of ["## 当前判定", "## 未知项", "## 路由", "## 外部动作"]) {
|
|
1345
1410
|
if (!body.includes(heading)) errors.push(`triage.md: missing '${heading}'`);
|
|
1346
1411
|
}
|
|
1412
|
+
if (publishAction !== "not-requested" && !body.includes("## 发布投影")) {
|
|
1413
|
+
errors.push("triage.md: missing '## 发布投影'");
|
|
1414
|
+
}
|
|
1347
1415
|
return { path, meta, body };
|
|
1348
1416
|
}
|
|
1349
1417
|
|
|
1418
|
+
function parsePublishLedger(body) {
|
|
1419
|
+
const rows = [];
|
|
1420
|
+
for (const line of body.split(/\r?\n/)) {
|
|
1421
|
+
if (!/^\|/.test(line) || /^\|\s*-+/.test(line) || /^\|\s*ticket\s*\|/i.test(line)) continue;
|
|
1422
|
+
const cells = line.split("|").map((cell) => cell.trim());
|
|
1423
|
+
const inner = cells.slice(1, cells.length - 1);
|
|
1424
|
+
if (inner.length < 8) continue;
|
|
1425
|
+
rows.push({
|
|
1426
|
+
ticket: inner[0],
|
|
1427
|
+
kind: inner[1],
|
|
1428
|
+
labels: inner[2],
|
|
1429
|
+
number: inner[3],
|
|
1430
|
+
url: inner[4],
|
|
1431
|
+
marker: inner[5],
|
|
1432
|
+
sha256: inner[6],
|
|
1433
|
+
state: inner[7],
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
return rows;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function validatePublish(path, expectedChange, triage, errors) {
|
|
1440
|
+
if (!isFile(path)) {
|
|
1441
|
+
errors.push("missing publish.md ledger");
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
const { meta, body } = parseFrontmatter(path);
|
|
1445
|
+
const required = [
|
|
1446
|
+
"schema_version",
|
|
1447
|
+
"artifact",
|
|
1448
|
+
"change",
|
|
1449
|
+
"mode",
|
|
1450
|
+
"repo",
|
|
1451
|
+
"publish_action",
|
|
1452
|
+
"include_cancelled",
|
|
1453
|
+
"origin",
|
|
1454
|
+
"updated_at",
|
|
1455
|
+
];
|
|
1456
|
+
const missing = required.filter((key) => !(key in meta));
|
|
1457
|
+
if (missing.length) errors.push(`publish.md: missing keys ${JSON.stringify(missing)}`);
|
|
1458
|
+
if (meta.schema_version !== 1 || meta.artifact !== "publish") {
|
|
1459
|
+
errors.push("publish.md: artifact/schema_version must be publish/1");
|
|
1460
|
+
}
|
|
1461
|
+
if (meta.change !== expectedChange) errors.push("publish.md: change must equal directory name");
|
|
1462
|
+
if (meta.mode !== "publish") errors.push(`publish.md: invalid mode ${meta.mode}`);
|
|
1463
|
+
if (!new Set(["pending", "published", "publish-failed", "waived"]).has(meta.publish_action)) {
|
|
1464
|
+
errors.push(`publish.md: invalid publish_action ${meta.publish_action}`);
|
|
1465
|
+
}
|
|
1466
|
+
if (!new Set(["local", "intake"]).has(meta.origin)) {
|
|
1467
|
+
errors.push(`publish.md: invalid origin ${meta.origin}`);
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof meta.repo !== "string" || !/.+\/.+/.test(meta.repo)) {
|
|
1470
|
+
errors.push("publish.md: repo must be owner/repo");
|
|
1471
|
+
}
|
|
1472
|
+
for (const heading of ["## 发布计划", "## 账本", "## 计数", "## 重试"]) {
|
|
1473
|
+
if (!body.includes(heading)) errors.push(`publish.md: missing '${heading}'`);
|
|
1474
|
+
}
|
|
1475
|
+
const rows = parsePublishLedger(body);
|
|
1476
|
+
if (!rows.length) errors.push("publish.md: ledger table has no ticket rows");
|
|
1477
|
+
const mixed = triage && triage.meta.classification === "mixed";
|
|
1478
|
+
for (const row of rows) {
|
|
1479
|
+
if (!/^T-\d{2,}$/.test(row.ticket)) {
|
|
1480
|
+
errors.push(`publish.md: invalid ticket id ${row.ticket}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (!VALID_PUBLISH_ROW_STATE.has(row.state)) {
|
|
1483
|
+
errors.push(`publish.md: ${row.ticket}: invalid state ${row.state}`);
|
|
1484
|
+
}
|
|
1485
|
+
const labels = row.labels.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1486
|
+
for (const label of labels) {
|
|
1487
|
+
if (FORBIDDEN_PUBLISH_LABELS.has(label)) {
|
|
1488
|
+
errors.push(`publish.md: ${row.ticket}: forbidden label ${label}`);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const skipped = row.state.startsWith("skipped:");
|
|
1492
|
+
if (!skipped && !VALID_TICKET_KIND.has(row.kind)) {
|
|
1493
|
+
errors.push(`publish.md: ${row.ticket}: invalid kind ${row.kind}`);
|
|
1494
|
+
}
|
|
1495
|
+
if (mixed && !skipped && !VALID_TICKET_KIND.has(row.kind)) {
|
|
1496
|
+
errors.push(`publish.md: mixed change requires kind on ${row.ticket}`);
|
|
1497
|
+
}
|
|
1498
|
+
if (COUNTED_PUBLISH_STATES.has(row.state) || row.state === "closed") {
|
|
1499
|
+
if (!/^\d+$/.test(row.number)) {
|
|
1500
|
+
errors.push(`publish.md: ${row.ticket}: ${row.state} row requires issue number`);
|
|
1501
|
+
}
|
|
1502
|
+
if (!String(row.url).startsWith("https://github.com/")) {
|
|
1503
|
+
errors.push(`publish.md: ${row.ticket}: ${row.state} row requires GitHub url`);
|
|
1504
|
+
}
|
|
1505
|
+
const expectedMarker = `specdev:${expectedChange}:${row.ticket}:published`;
|
|
1506
|
+
if (row.marker !== expectedMarker) {
|
|
1507
|
+
errors.push(`publish.md: ${row.ticket}: marker must be ${expectedMarker}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (row.state === "closed" || row.state === "created" || row.state === "commented") {
|
|
1511
|
+
if (!labels.includes("specdev:published")) {
|
|
1512
|
+
errors.push(`publish.md: ${row.ticket}: missing specdev:published`);
|
|
1513
|
+
}
|
|
1514
|
+
const originLabel = meta.origin === "intake" ? "origin:intake" : "origin:local";
|
|
1515
|
+
if (!labels.includes(originLabel)) {
|
|
1516
|
+
errors.push(`publish.md: ${row.ticket}: missing ${originLabel}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return { path, meta, body, rows };
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function parseCaptureLedger(body) {
|
|
1524
|
+
const rows = [];
|
|
1525
|
+
for (const line of body.split(/\r?\n/)) {
|
|
1526
|
+
if (!/^\|/.test(line) || /^\|\s*-+/.test(line) || /^\|\s*id\s*\|/i.test(line)) continue;
|
|
1527
|
+
const cells = line.split("|").map((cell) => cell.trim());
|
|
1528
|
+
const inner = cells.slice(1, cells.length - 1);
|
|
1529
|
+
if (inner.length < 9) continue;
|
|
1530
|
+
rows.push({
|
|
1531
|
+
id: inner[0],
|
|
1532
|
+
kind: inner[1],
|
|
1533
|
+
title: inner[2],
|
|
1534
|
+
labels: inner[3],
|
|
1535
|
+
number: inner[4],
|
|
1536
|
+
url: inner[5],
|
|
1537
|
+
marker: inner[6],
|
|
1538
|
+
sha256: inner[7],
|
|
1539
|
+
state: inner[8],
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return rows;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function validateCapture(path, errors) {
|
|
1546
|
+
if (!isFile(path)) {
|
|
1547
|
+
errors.push("missing capture.md ledger");
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
const { meta, body } = parseFrontmatter(path);
|
|
1551
|
+
const required = [
|
|
1552
|
+
"schema_version",
|
|
1553
|
+
"artifact",
|
|
1554
|
+
"mode",
|
|
1555
|
+
"repo",
|
|
1556
|
+
"updated_at",
|
|
1557
|
+
];
|
|
1558
|
+
const missing = required.filter((key) => !(key in meta));
|
|
1559
|
+
if (missing.length) errors.push(`capture.md: missing keys ${JSON.stringify(missing)}`);
|
|
1560
|
+
if (meta.schema_version !== 1 || meta.artifact !== "capture-index") {
|
|
1561
|
+
errors.push("capture.md: artifact/schema_version must be capture-index/1");
|
|
1562
|
+
}
|
|
1563
|
+
if (meta.mode !== "capture") errors.push(`capture.md: invalid mode ${meta.mode}`);
|
|
1564
|
+
if (typeof meta.repo !== "string" || !/.+\/.+/.test(meta.repo)) {
|
|
1565
|
+
errors.push("capture.md: repo must be owner/repo");
|
|
1566
|
+
}
|
|
1567
|
+
for (const heading of ["## 捕获计划", "## 账本", "## 计数", "## 重试"]) {
|
|
1568
|
+
if (!body.includes(heading)) errors.push(`capture.md: missing '${heading}'`);
|
|
1569
|
+
}
|
|
1570
|
+
const rows = parseCaptureLedger(body);
|
|
1571
|
+
for (const row of rows) {
|
|
1572
|
+
if (!CHANGE_NAME.test(row.id)) {
|
|
1573
|
+
errors.push(`capture.md: invalid id ${row.id}`);
|
|
1574
|
+
}
|
|
1575
|
+
if (!VALID_CAPTURE_ROW_STATE.has(row.state)) {
|
|
1576
|
+
errors.push(`capture.md: ${row.id}: invalid state ${row.state}`);
|
|
1577
|
+
}
|
|
1578
|
+
const labels = row.labels.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1579
|
+
for (const label of labels) {
|
|
1580
|
+
if (FORBIDDEN_CAPTURE_LABELS.has(label)) {
|
|
1581
|
+
errors.push(`capture.md: ${row.id}: forbidden label ${label}`);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const skipped = row.state.startsWith("skipped:") || row.state === "waived" || row.state === "planned" || row.state === "failed";
|
|
1585
|
+
if (!skipped && !VALID_TICKET_KIND.has(row.kind)) {
|
|
1586
|
+
errors.push(`capture.md: ${row.id}: invalid kind ${row.kind}`);
|
|
1587
|
+
}
|
|
1588
|
+
if (COUNTED_CAPTURE_REMOTE_STATES.has(row.state)) {
|
|
1589
|
+
if (!VALID_TICKET_KIND.has(row.kind)) {
|
|
1590
|
+
errors.push(`capture.md: ${row.id}: invalid kind ${row.kind}`);
|
|
1591
|
+
}
|
|
1592
|
+
if (!/^\d+$/.test(row.number)) {
|
|
1593
|
+
errors.push(`capture.md: ${row.id}: ${row.state} row requires issue number`);
|
|
1594
|
+
}
|
|
1595
|
+
if (!String(row.url).startsWith("https://github.com/")) {
|
|
1596
|
+
errors.push(`capture.md: ${row.id}: ${row.state} row requires GitHub url`);
|
|
1597
|
+
}
|
|
1598
|
+
const expectedMarker = `specdev:capture:${row.id}`;
|
|
1599
|
+
if (row.marker !== expectedMarker) {
|
|
1600
|
+
errors.push(`capture.md: ${row.id}: marker must be ${expectedMarker}`);
|
|
1601
|
+
}
|
|
1602
|
+
if (!labels.includes("specdev:captured")) {
|
|
1603
|
+
errors.push(`capture.md: ${row.id}: missing specdev:captured`);
|
|
1604
|
+
}
|
|
1605
|
+
if (!labels.includes("origin:local")) {
|
|
1606
|
+
errors.push(`capture.md: ${row.id}: missing origin:local`);
|
|
1607
|
+
}
|
|
1608
|
+
if (labels.includes("origin:intake")) {
|
|
1609
|
+
errors.push(`capture.md: ${row.id}: capture origin must be local`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return { path, meta, body, rows };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1350
1616
|
function validateDiagnosis(path, expectedChange, errors) {
|
|
1351
1617
|
if (!isFile(path)) {
|
|
1352
1618
|
errors.push("missing diagnosis artifact");
|
|
@@ -2048,6 +2314,9 @@ function validateTicket(path, errors) {
|
|
|
2048
2314
|
const ticketId = String(meta.id);
|
|
2049
2315
|
if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
|
|
2050
2316
|
if (!VALID_TICKET_STATUS.has(meta.status)) errors.push(`${basename(path)}: invalid status ${meta.status}`);
|
|
2317
|
+
if (meta.kind != null && meta.kind !== "" && !VALID_TICKET_KIND.has(meta.kind)) {
|
|
2318
|
+
errors.push(`${basename(path)}: invalid kind ${meta.kind}`);
|
|
2319
|
+
}
|
|
2051
2320
|
if (!VALID_DEPTH.has(meta.planning_depth)) {
|
|
2052
2321
|
errors.push(`${basename(path)}: invalid planning_depth ${meta.planning_depth}`);
|
|
2053
2322
|
}
|
|
@@ -3001,6 +3270,9 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3001
3270
|
if (isFile(join(change, "source-issue.md"))) {
|
|
3002
3271
|
errors.push("obsolete source-issue.md is forbidden; use source.md without compatibility fallback");
|
|
3003
3272
|
}
|
|
3273
|
+
if (isFile(join(change, "capture.md"))) {
|
|
3274
|
+
errors.push("capture.md is workspace-owned at specdev/capture.md; change-level copies are forbidden");
|
|
3275
|
+
}
|
|
3004
3276
|
const sourceRequired = stage === "triage";
|
|
3005
3277
|
const sourcePath = join(change, "source.md");
|
|
3006
3278
|
const source = isFile(sourcePath) || sourceRequired
|
|
@@ -3010,6 +3282,13 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3010
3282
|
const triage = isFile(triagePath) || sourceRequired
|
|
3011
3283
|
? validateTriage(triagePath, basename(change), errors)
|
|
3012
3284
|
: null;
|
|
3285
|
+
const publishPath = join(change, "publish.md");
|
|
3286
|
+
const publishRequired = Boolean(
|
|
3287
|
+
triage && new Set(["pending", "published", "publish-failed", "waived"]).has(triage.meta.publish_action),
|
|
3288
|
+
);
|
|
3289
|
+
if (isFile(publishPath) || publishRequired) {
|
|
3290
|
+
validatePublish(publishPath, basename(change), triage, errors);
|
|
3291
|
+
}
|
|
3013
3292
|
const diagnosisPath = join(change, "diagnosis.md");
|
|
3014
3293
|
if (isFile(diagnosisPath) || stage === "diagnosis") {
|
|
3015
3294
|
validateDiagnosis(diagnosisPath, basename(change), errors);
|
|
@@ -3298,6 +3577,13 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3298
3577
|
) {
|
|
3299
3578
|
errors.push(`complete stage cannot archive external_action=${triage.meta.external_action}`);
|
|
3300
3579
|
}
|
|
3580
|
+
if (
|
|
3581
|
+
stage === "complete" &&
|
|
3582
|
+
triage &&
|
|
3583
|
+
new Set(["pending", "publish-failed"]).has(triage.meta.publish_action)
|
|
3584
|
+
) {
|
|
3585
|
+
errors.push(`complete stage cannot archive publish_action=${triage.meta.publish_action}`);
|
|
3586
|
+
}
|
|
3301
3587
|
|
|
3302
3588
|
for (const path of walk(change).filter((item) => isFile(item) && extname(item) === ".md")) {
|
|
3303
3589
|
const text = readText(path);
|
|
@@ -3325,7 +3611,7 @@ function printResults(errors, warnings) {
|
|
|
3325
3611
|
}
|
|
3326
3612
|
|
|
3327
3613
|
function usage() {
|
|
3328
|
-
console.error("Usage: node validate-specdev.mjs [--stage <stage>] [--repo <project-root>] <change-directory> | --self-check");
|
|
3614
|
+
console.error("Usage: node validate-specdev.mjs [--stage <stage>] [--repo <project-root>] <change-directory> | --self-check | --capture <capture.md>");
|
|
3329
3615
|
return 2;
|
|
3330
3616
|
}
|
|
3331
3617
|
|
|
@@ -3333,6 +3619,7 @@ function main(argv) {
|
|
|
3333
3619
|
let selfCheckRequested = false;
|
|
3334
3620
|
let stage = null;
|
|
3335
3621
|
let repoRoot = null;
|
|
3622
|
+
let capturePath = null;
|
|
3336
3623
|
const positional = [];
|
|
3337
3624
|
for (let index = 0; index < argv.length; index += 1) {
|
|
3338
3625
|
const arg = argv[index];
|
|
@@ -3348,6 +3635,11 @@ function main(argv) {
|
|
|
3348
3635
|
index += 1;
|
|
3349
3636
|
} else if (arg.startsWith("--repo=")) {
|
|
3350
3637
|
repoRoot = arg.slice("--repo=".length);
|
|
3638
|
+
} else if (arg === "--capture") {
|
|
3639
|
+
capturePath = argv[index + 1] ?? null;
|
|
3640
|
+
index += 1;
|
|
3641
|
+
} else if (arg.startsWith("--capture=")) {
|
|
3642
|
+
capturePath = arg.slice("--capture=".length);
|
|
3351
3643
|
} else if (arg.startsWith("--")) {
|
|
3352
3644
|
return usage();
|
|
3353
3645
|
} else {
|
|
@@ -3356,12 +3648,18 @@ function main(argv) {
|
|
|
3356
3648
|
}
|
|
3357
3649
|
if (stage !== null && !VALID_STAGES.has(stage)) return usage();
|
|
3358
3650
|
if (selfCheckRequested) {
|
|
3359
|
-
if (positional.length || stage !== null) return usage();
|
|
3651
|
+
if (positional.length || stage !== null || capturePath) return usage();
|
|
3360
3652
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
3361
3653
|
const root = resolve(scriptDirectory, "..", "..");
|
|
3362
3654
|
const result = selfCheck(root);
|
|
3363
3655
|
return printResults(result.errors, result.warnings);
|
|
3364
3656
|
}
|
|
3657
|
+
if (capturePath) {
|
|
3658
|
+
if (positional.length || stage !== null || repoRoot) return usage();
|
|
3659
|
+
const errors = [];
|
|
3660
|
+
validateCapture(resolve(capturePath), errors);
|
|
3661
|
+
return printResults(errors, []);
|
|
3662
|
+
}
|
|
3365
3663
|
if (positional.length === 1) {
|
|
3366
3664
|
const result = validateChange(resolve(positional[0]), stage, repoRoot);
|
|
3367
3665
|
return printResults(result.errors, result.warnings);
|
|
@@ -3369,7 +3667,7 @@ function main(argv) {
|
|
|
3369
3667
|
return usage();
|
|
3370
3668
|
}
|
|
3371
3669
|
|
|
3372
|
-
export { parseFrontmatter, validateChange, validateTicket, validateMap, pathsOverlap, findCycle };
|
|
3670
|
+
export { parseFrontmatter, validateChange, validateTicket, validateMap, pathsOverlap, findCycle, validateCapture };
|
|
3373
3671
|
|
|
3374
3672
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
3375
3673
|
try { process.exitCode = main(process.argv.slice(2)); }
|