@namewta/speculo 1.0.13 → 1.0.14
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 +8 -5
- package/template/canonical/canonical-specdev-grill-with-docs.md +5 -4
- package/template/canonical/canonical-specdev-spec.md +7 -4
- package/template/canonical/canonical-specdev-tickets.md +19 -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 +44 -13
- package/template/workflows/specdev/T-triage/close-comment-template.md +42 -0
- package/template/workflows/specdev/T-triage/intake-protocol.md +6 -2
- package/template/workflows/specdev/T-triage/issue-body-template.md +34 -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 +53 -0
- package/template/workflows/specdev/T-triage/references/public-projection.md +66 -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 +1 -0
- package/template/workflows/specdev/common/rules/artifact-contract.md +5 -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 +2 -1
- 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/validate-specdev.mjs +173 -3
|
@@ -66,6 +66,43 @@ 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"]);
|
|
69
106
|
const VALID_DEPTH = new Set(["lite", "standard", "deep"]);
|
|
70
107
|
const VALID_RISK = new Set(["low", "medium", "high", "critical"]);
|
|
71
108
|
const VALID_PLAN_MODES = new Set([
|
|
@@ -175,6 +212,7 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
175
212
|
"status.json",
|
|
176
213
|
".status.json",
|
|
177
214
|
"triage.md",
|
|
215
|
+
"publish.md",
|
|
178
216
|
"diagnosis.md",
|
|
179
217
|
"source.md",
|
|
180
218
|
"architecture-review.md",
|
|
@@ -973,7 +1011,7 @@ function capabilityChecks(root) {
|
|
|
973
1011
|
"triage",
|
|
974
1012
|
[
|
|
975
1013
|
join(root, "T-triage", "T-triage.md"),
|
|
976
|
-
["source.md", "intake", "reconcile", "唯一权威", "远程写入为零"],
|
|
1014
|
+
["source.md", "intake", "reconcile", "publish", "publish.md", "specdev:published", "唯一权威", "远程写入为零"],
|
|
977
1015
|
],
|
|
978
1016
|
],
|
|
979
1017
|
[
|
|
@@ -1061,7 +1099,7 @@ function capabilityChecks(root) {
|
|
|
1061
1099
|
"archive",
|
|
1062
1100
|
[
|
|
1063
1101
|
join(root, "A-archive-and-consolidate", "A-archive-and-consolidate.md"),
|
|
1064
|
-
["archive-single", "dry-run", "external_action", "consolidate-from-code", "archive-and-consolidate"],
|
|
1102
|
+
["archive-single", "dry-run", "external_action", "publish_action", "consolidate-from-code", "archive-and-consolidate"],
|
|
1065
1103
|
],
|
|
1066
1104
|
],
|
|
1067
1105
|
[
|
|
@@ -1327,7 +1365,7 @@ function validateTriage(path, expectedChange, errors) {
|
|
|
1327
1365
|
errors.push("triage.md: artifact/schema_version must be triage/1");
|
|
1328
1366
|
}
|
|
1329
1367
|
if (meta.change !== expectedChange) errors.push("triage.md: change must equal directory name");
|
|
1330
|
-
if (!
|
|
1368
|
+
if (!VALID_TRIAGE_MODE.has(meta.mode)) errors.push(`triage.md: invalid mode ${meta.mode}`);
|
|
1331
1369
|
if (!VALID_RISK.has(meta.risk)) errors.push(`triage.md: invalid risk ${meta.risk}`);
|
|
1332
1370
|
if (typeof meta.route !== "string" || !meta.route.startsWith("specdev/")) {
|
|
1333
1371
|
errors.push("triage.md: route must be a specdev work id");
|
|
@@ -1338,15 +1376,130 @@ function validateTriage(path, expectedChange, errors) {
|
|
|
1338
1376
|
if (!new Set(["not-applicable", "pending-close", "closed", "close-failed", "waived"]).has(meta.external_action)) {
|
|
1339
1377
|
errors.push(`triage.md: invalid external_action ${meta.external_action}`);
|
|
1340
1378
|
}
|
|
1379
|
+
const publishAction = meta.publish_action == null || meta.publish_action === ""
|
|
1380
|
+
? "not-requested"
|
|
1381
|
+
: meta.publish_action;
|
|
1382
|
+
if (!VALID_PUBLISH_ACTION.has(publishAction)) {
|
|
1383
|
+
errors.push(`triage.md: invalid publish_action ${meta.publish_action}`);
|
|
1384
|
+
}
|
|
1385
|
+
meta.publish_action = publishAction;
|
|
1341
1386
|
if (!String(meta.source ?? "").includes("/source.md</Path>")) {
|
|
1342
1387
|
errors.push("triage.md: source must reference the local source.md artifact");
|
|
1343
1388
|
}
|
|
1344
1389
|
for (const heading of ["## 当前判定", "## 未知项", "## 路由", "## 外部动作"]) {
|
|
1345
1390
|
if (!body.includes(heading)) errors.push(`triage.md: missing '${heading}'`);
|
|
1346
1391
|
}
|
|
1392
|
+
if (publishAction !== "not-requested" && !body.includes("## 发布投影")) {
|
|
1393
|
+
errors.push("triage.md: missing '## 发布投影'");
|
|
1394
|
+
}
|
|
1347
1395
|
return { path, meta, body };
|
|
1348
1396
|
}
|
|
1349
1397
|
|
|
1398
|
+
function parsePublishLedger(body) {
|
|
1399
|
+
const rows = [];
|
|
1400
|
+
for (const line of body.split(/\r?\n/)) {
|
|
1401
|
+
if (!/^\|/.test(line) || /^\|\s*-+/.test(line) || /^\|\s*ticket\s*\|/i.test(line)) continue;
|
|
1402
|
+
const cells = line.split("|").map((cell) => cell.trim());
|
|
1403
|
+
const inner = cells.slice(1, cells.length - 1);
|
|
1404
|
+
if (inner.length < 8) continue;
|
|
1405
|
+
rows.push({
|
|
1406
|
+
ticket: inner[0],
|
|
1407
|
+
kind: inner[1],
|
|
1408
|
+
labels: inner[2],
|
|
1409
|
+
number: inner[3],
|
|
1410
|
+
url: inner[4],
|
|
1411
|
+
marker: inner[5],
|
|
1412
|
+
sha256: inner[6],
|
|
1413
|
+
state: inner[7],
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return rows;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function validatePublish(path, expectedChange, triage, errors) {
|
|
1420
|
+
if (!isFile(path)) {
|
|
1421
|
+
errors.push("missing publish.md ledger");
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
const { meta, body } = parseFrontmatter(path);
|
|
1425
|
+
const required = [
|
|
1426
|
+
"schema_version",
|
|
1427
|
+
"artifact",
|
|
1428
|
+
"change",
|
|
1429
|
+
"mode",
|
|
1430
|
+
"repo",
|
|
1431
|
+
"publish_action",
|
|
1432
|
+
"include_cancelled",
|
|
1433
|
+
"origin",
|
|
1434
|
+
"updated_at",
|
|
1435
|
+
];
|
|
1436
|
+
const missing = required.filter((key) => !(key in meta));
|
|
1437
|
+
if (missing.length) errors.push(`publish.md: missing keys ${JSON.stringify(missing)}`);
|
|
1438
|
+
if (meta.schema_version !== 1 || meta.artifact !== "publish") {
|
|
1439
|
+
errors.push("publish.md: artifact/schema_version must be publish/1");
|
|
1440
|
+
}
|
|
1441
|
+
if (meta.change !== expectedChange) errors.push("publish.md: change must equal directory name");
|
|
1442
|
+
if (meta.mode !== "publish") errors.push(`publish.md: invalid mode ${meta.mode}`);
|
|
1443
|
+
if (!new Set(["pending", "published", "publish-failed", "waived"]).has(meta.publish_action)) {
|
|
1444
|
+
errors.push(`publish.md: invalid publish_action ${meta.publish_action}`);
|
|
1445
|
+
}
|
|
1446
|
+
if (!new Set(["local", "intake"]).has(meta.origin)) {
|
|
1447
|
+
errors.push(`publish.md: invalid origin ${meta.origin}`);
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof meta.repo !== "string" || !/.+\/.+/.test(meta.repo)) {
|
|
1450
|
+
errors.push("publish.md: repo must be owner/repo");
|
|
1451
|
+
}
|
|
1452
|
+
for (const heading of ["## 发布计划", "## 账本", "## 计数", "## 重试"]) {
|
|
1453
|
+
if (!body.includes(heading)) errors.push(`publish.md: missing '${heading}'`);
|
|
1454
|
+
}
|
|
1455
|
+
const rows = parsePublishLedger(body);
|
|
1456
|
+
if (!rows.length) errors.push("publish.md: ledger table has no ticket rows");
|
|
1457
|
+
const mixed = triage && triage.meta.classification === "mixed";
|
|
1458
|
+
for (const row of rows) {
|
|
1459
|
+
if (!/^T-\d{2,}$/.test(row.ticket)) {
|
|
1460
|
+
errors.push(`publish.md: invalid ticket id ${row.ticket}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (!VALID_PUBLISH_ROW_STATE.has(row.state)) {
|
|
1463
|
+
errors.push(`publish.md: ${row.ticket}: invalid state ${row.state}`);
|
|
1464
|
+
}
|
|
1465
|
+
const labels = row.labels.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1466
|
+
for (const label of labels) {
|
|
1467
|
+
if (FORBIDDEN_PUBLISH_LABELS.has(label)) {
|
|
1468
|
+
errors.push(`publish.md: ${row.ticket}: forbidden label ${label}`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
const skipped = row.state.startsWith("skipped:");
|
|
1472
|
+
if (!skipped && !VALID_TICKET_KIND.has(row.kind)) {
|
|
1473
|
+
errors.push(`publish.md: ${row.ticket}: invalid kind ${row.kind}`);
|
|
1474
|
+
}
|
|
1475
|
+
if (mixed && !skipped && !VALID_TICKET_KIND.has(row.kind)) {
|
|
1476
|
+
errors.push(`publish.md: mixed change requires kind on ${row.ticket}`);
|
|
1477
|
+
}
|
|
1478
|
+
if (COUNTED_PUBLISH_STATES.has(row.state) || row.state === "closed") {
|
|
1479
|
+
if (!/^\d+$/.test(row.number)) {
|
|
1480
|
+
errors.push(`publish.md: ${row.ticket}: ${row.state} row requires issue number`);
|
|
1481
|
+
}
|
|
1482
|
+
if (!String(row.url).startsWith("https://github.com/")) {
|
|
1483
|
+
errors.push(`publish.md: ${row.ticket}: ${row.state} row requires GitHub url`);
|
|
1484
|
+
}
|
|
1485
|
+
const expectedMarker = `specdev:${expectedChange}:${row.ticket}:published`;
|
|
1486
|
+
if (row.marker !== expectedMarker) {
|
|
1487
|
+
errors.push(`publish.md: ${row.ticket}: marker must be ${expectedMarker}`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (row.state === "closed" || row.state === "created" || row.state === "commented") {
|
|
1491
|
+
if (!labels.includes("specdev:published")) {
|
|
1492
|
+
errors.push(`publish.md: ${row.ticket}: missing specdev:published`);
|
|
1493
|
+
}
|
|
1494
|
+
const originLabel = meta.origin === "intake" ? "origin:intake" : "origin:local";
|
|
1495
|
+
if (!labels.includes(originLabel)) {
|
|
1496
|
+
errors.push(`publish.md: ${row.ticket}: missing ${originLabel}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return { path, meta, body, rows };
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1350
1503
|
function validateDiagnosis(path, expectedChange, errors) {
|
|
1351
1504
|
if (!isFile(path)) {
|
|
1352
1505
|
errors.push("missing diagnosis artifact");
|
|
@@ -2048,6 +2201,9 @@ function validateTicket(path, errors) {
|
|
|
2048
2201
|
const ticketId = String(meta.id);
|
|
2049
2202
|
if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
|
|
2050
2203
|
if (!VALID_TICKET_STATUS.has(meta.status)) errors.push(`${basename(path)}: invalid status ${meta.status}`);
|
|
2204
|
+
if (meta.kind != null && meta.kind !== "" && !VALID_TICKET_KIND.has(meta.kind)) {
|
|
2205
|
+
errors.push(`${basename(path)}: invalid kind ${meta.kind}`);
|
|
2206
|
+
}
|
|
2051
2207
|
if (!VALID_DEPTH.has(meta.planning_depth)) {
|
|
2052
2208
|
errors.push(`${basename(path)}: invalid planning_depth ${meta.planning_depth}`);
|
|
2053
2209
|
}
|
|
@@ -3010,6 +3166,13 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3010
3166
|
const triage = isFile(triagePath) || sourceRequired
|
|
3011
3167
|
? validateTriage(triagePath, basename(change), errors)
|
|
3012
3168
|
: null;
|
|
3169
|
+
const publishPath = join(change, "publish.md");
|
|
3170
|
+
const publishRequired = Boolean(
|
|
3171
|
+
triage && new Set(["pending", "published", "publish-failed", "waived"]).has(triage.meta.publish_action),
|
|
3172
|
+
);
|
|
3173
|
+
if (isFile(publishPath) || publishRequired) {
|
|
3174
|
+
validatePublish(publishPath, basename(change), triage, errors);
|
|
3175
|
+
}
|
|
3013
3176
|
const diagnosisPath = join(change, "diagnosis.md");
|
|
3014
3177
|
if (isFile(diagnosisPath) || stage === "diagnosis") {
|
|
3015
3178
|
validateDiagnosis(diagnosisPath, basename(change), errors);
|
|
@@ -3298,6 +3461,13 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3298
3461
|
) {
|
|
3299
3462
|
errors.push(`complete stage cannot archive external_action=${triage.meta.external_action}`);
|
|
3300
3463
|
}
|
|
3464
|
+
if (
|
|
3465
|
+
stage === "complete" &&
|
|
3466
|
+
triage &&
|
|
3467
|
+
new Set(["pending", "publish-failed"]).has(triage.meta.publish_action)
|
|
3468
|
+
) {
|
|
3469
|
+
errors.push(`complete stage cannot archive publish_action=${triage.meta.publish_action}`);
|
|
3470
|
+
}
|
|
3301
3471
|
|
|
3302
3472
|
for (const path of walk(change).filter((item) => isFile(item) && extname(item) === ".md")) {
|
|
3303
3473
|
const text = readText(path);
|