@namewta/speculo 1.0.14 → 1.0.16
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 +2 -2
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-goal-plan.md +25 -13
- package/template/canonical/canonical-specdev-grill-with-docs.md +2 -1
- package/template/canonical/canonical-specdev-spec.md +2 -1
- package/template/canonical/canonical-specdev-tickets.md +3 -2
- package/template/commands/handoff.md +1 -1
- package/template/commands/status.md +1 -1
- package/template/skills/engineering-standards-builder/examples/README.md +6 -0
- package/template/skills/engineering-standards-builder/manifest.txt +1 -0
- package/template/skills/engineering-standards-builder/scripts/self-test.mjs +1 -0
- package/template/skills/engineering-standards-builder/scripts/sync-manifest.mjs +2 -2
- package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +7 -3
- package/template/workflows/specdev/I-init-setup/tracking-template.md +1 -1
- package/template/workflows/specdev/INDEX.md +1 -1
- package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +4 -2
- package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +2 -0
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +13 -8
- package/template/workflows/specdev/P-goal-plan/references/map-control.md +1 -1
- package/template/workflows/specdev/P-goal-plan/references/single-change-plan.md +4 -2
- package/template/workflows/specdev/README.md +4 -4
- package/template/workflows/specdev/T-triage/T-triage.md +44 -10
- 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/intake-protocol.md +2 -0
- package/template/workflows/specdev/T-triage/issue-record-template.md +27 -0
- package/template/workflows/specdev/T-triage/references/classification-map.md +10 -9
- package/template/workflows/specdev/T-triage/references/public-projection.md +27 -4
- package/template/workflows/specdev/T-triage/tools/capture-status.mjs +125 -0
- package/template/workflows/specdev/common/README.md +1 -0
- package/template/workflows/specdev/common/rules/artifact-contract.md +2 -1
- package/template/workflows/specdev/common/rules/workflow-routing.md +3 -2
- 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/tools/README.md +14 -1
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +151 -22
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
| 场景 | 入口 | 正常出口 |
|
|
5
5
|
|---|---|---|
|
|
6
6
|
| 需要冻结外部来源或审计摄入 | T-triage intake | D / G / W / P / S / C / T |
|
|
7
|
+
| 尚未成 Change,只要先在 GitHub 留一条仍 open 的记事项 | T-triage capture | 停止 / 以后 intake / W |
|
|
7
8
|
| 本地 change 完成且来源可关闭 | T-triage reconcile | A |
|
|
8
9
|
| 本地 change 完成且要把 Ticket 记到 GitHub | T-triage publish | reconcile / A / 停止 |
|
|
9
10
|
| 疑难 bug 或性能回归 | D-diagnose-bugs | S / T / I / R / W |
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
| 固定点 diff、branch 或 PR review | C-code-review | completed / T / S / G |
|
|
14
15
|
| 外部行为已清楚 | S-spec | T-tickets |
|
|
15
16
|
| Ready Spec 需要垂直切片 | T-tickets | P-goal-plan / I |
|
|
16
|
-
|
|
|
17
|
+
| 单个已拆票 change 或多个 Ready change 的 Goal 规划、持续实现与恢复 | P-goal-plan | I / verify / completed / blocked |
|
|
17
18
|
| 旧 O 调用或恢复键 | P-goal-plan | 统一 P 多 change 模式 |
|
|
18
19
|
| Ready 执行 | I-implement | Triage / A / blocked / deviation |
|
|
19
20
|
| 开发完成后需要理解当前 change 或追问实现 | L-learn-change | 返回用户 / 继续提问 / Triage / A |
|
|
@@ -22,4 +23,4 @@
|
|
|
22
23
|
同 change 下一阶段需要当前一手推理且上下文健康时继续;切换 repo/person/harness 或旁路时使用 `<Path>{roots.commands}/handoff.md</Path>`;严格限定且可独立派单时使用 Dispatch Packet;其他长上下文以权威工件路径恢复。平台不支持 clear/compact 时不虚构操作。
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
本地已清晰请求不强制绕行 Triage。完成后要记账才选 publish
|
|
26
|
+
本地已清晰请求不强制绕行 Triage。完成后要记账才选 publish;尚未成 Change、只想先留 inbox 记录才选 capture。publish、reconcile 与 capture 写不同远程对象,可先后执行。G 是单 change Grill,P-prototype 是 UI 设计,不与统一 P-goal-plan 混淆。W 发现候选后逐个交 G/S/T;单 change 拆票后可交 P 规划,多个 child 创建父 Goal 前仍须全部 Ready。原缺陷诊断 D、风险分诊、远程 reconcile、完成后发布投影和记事项捕获职责不删除。
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- 全局状态:`<Path>{roots.state}/specdev/status.json</Path>`
|
|
10
10
|
- 活跃 change:`<Path>{roots.state}/specdev/changes/</Path>`
|
|
11
11
|
- 历史归档:`<Path>{roots.state}/specdev/archive/</Path>`
|
|
12
|
+
- 捕获账本(可选,缺失合法):`<Path>{roots.state}/specdev/capture.md</Path>`
|
|
12
13
|
|
|
13
14
|
刷新时 CLI 依据 `<Path>{roots.workflows}/specdev/runtime-contract.json</Path>` 处理持久化数据:配置使用 baseline 三方合并,登记的状态 schema 使用显式 migrator,其他 runtime 文件按字节保留。只有字段删除或结构迁移时才在 `<Path>{roots.state}/back/</Path>` 写入 targeted backup;冲突在替换 active 安装前阻塞。`<Path>{roots.state}/back/</Path>`、`<Path>{roots.state}/install.json</Path>`、`<Path>{roots.state}/managed.json</Path>` 与 `<Path>{roots.state}/baselines/</Path>` 均不属于 SpecDev 写入 namespace。
|
|
14
15
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:speculo:specdev:capture:v1",
|
|
4
|
+
"title": "SpecDev Capture Index Frontmatter",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema_version", "artifact", "mode", "repo", "updated_at"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": {"const": 1},
|
|
9
|
+
"artifact": {"const": "capture-index"},
|
|
10
|
+
"mode": {"const": "capture"},
|
|
11
|
+
"repo": {"type": "string", "pattern": "^.+/.+$"},
|
|
12
|
+
"updated_at": {"type": "string", "minLength": 1}
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": true
|
|
15
|
+
}
|
|
@@ -9,7 +9,18 @@ node <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> \
|
|
|
9
9
|
<Path>{roots.state}/specdev/changes/{change}</Path>
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`--stage`
|
|
12
|
+
`--stage` 只要求该阶段已经拥有的工件;所有已经存在的工件仍会验证。单 change 的 `goal-plan` 要求自己的 Goal Plan,允许合法的 `draft / ready_for_execution: false`;Tickets 阶段可尚无 Goal。Goal 的就绪字段必须是布尔值并与状态对应。
|
|
13
|
+
|
|
14
|
+
存在 Implementation Map、Implementation Plan 或 goal-tickets-map 入口中的任一项即走父编排校验;缺少另一父工件会报错,不回退单 change,也不额外要求单 change Goal。父编排读取 sibling 成员,创建时要求每个成员已有 Ready Spec/Tickets,继续校验组合 Ticket DAG、唯一父归属、serialization、跨 Ticket 写路径、全局 workspace/实现配额和完成门。已有父 Map/Plan 尚未补建无状态入口时仍可验证。
|
|
15
|
+
|
|
16
|
+
省略 stage 时验证当前存在的工件,不会因未来 Work 尚未运行而报错。`--repo` 可选;提供后会把状态中的 SHA、祖先关系、当前分支和完成时 clean 状态与真实 Git 仓库交叉验证。
|
|
17
|
+
|
|
18
|
+
校验 workspace 捕获账本(文件必须已存在;缺失时不要为了校验去创建):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
node <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> \
|
|
22
|
+
--capture <Path>{roots.state}/specdev/capture.md</Path>
|
|
23
|
+
```
|
|
13
24
|
|
|
14
25
|
## 校验 SpecDev 工作流包
|
|
15
26
|
|
|
@@ -27,6 +38,8 @@ node <Path>{roots.workflows}/specdev/common/tools/ticket-control.mjs</Path> --ma
|
|
|
27
38
|
|
|
28
39
|
接受普通 tickets-map、父 goal-tickets-map 入口或旧 Implementation Map。可选 --previous 读取上轮 JSON 输出作漂移检查;该输出应保存到调用方已有 Evidence,不建立第二套权威状态。输出 frontier、blocked、deferred、in_flight、invalidated 与逐票契约摘要。
|
|
29
40
|
|
|
41
|
+
单 change 的 Goal Ready 不要求所有未来票同时 Ready。合法 draft 票保留缺口与重审条件,只阻塞自身及依赖闭包;`ready/in_progress/review` 却没有 `ready=true` 是逐票错误。frontier 只包含自身 Ready、依赖成功完成、owner/资源/并发条件满足的票;上游 done 不自动修改消费者状态。全局执行门未满足时无可派单票,空 frontier 的 blocked/deferred 说明恢复条件。多 change 父创建的全员 Ready 输入门保持不变。
|
|
42
|
+
|
|
30
43
|
只读工具不调用 Skill、不执行实现、不检查真实授权/正式记忆网关,不获取锁或自动解锁;Lead 必须在 dispatch 前完成这些检查。结构错误返回非零;局部错误仍可能带独立 frontier,调用方必须检查节点和全局 diagnostics。结构检查成功的退出码 0 不代表任务完成,且业务门禁可能仍阻塞全部票。eligible_for_final_verification 只表示可进入最终验收,不是 Goal completed。
|
|
31
44
|
|
|
32
45
|
`<Path>{roots.workflows}/specdev/common/tools/plan-contract.mjs</Path>` 是两个工具共用的只读校验库:新 Ticket 的真实 Skill 名称/摘要、必需调用证据、Map 数量合同、父入口与 W 候选图。普通 --repo 仍支持原 Git 事实验证;存在未完成票的真实项目 Skill 绑定时必须提供项目根。旧票可读,开始新实现前必须显式补齐 Plan 合同;已完成历史证据不要求当前 Skill 包仍与历史版本相同。
|
|
@@ -103,6 +103,20 @@ const FORBIDDEN_PUBLISH_LABELS = new Set([
|
|
|
103
103
|
"stale",
|
|
104
104
|
]);
|
|
105
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"]);
|
|
106
120
|
const VALID_DEPTH = new Set(["lite", "standard", "deep"]);
|
|
107
121
|
const VALID_RISK = new Set(["low", "medium", "high", "critical"]);
|
|
108
122
|
const VALID_PLAN_MODES = new Set([
|
|
@@ -213,6 +227,7 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
213
227
|
".status.json",
|
|
214
228
|
"triage.md",
|
|
215
229
|
"publish.md",
|
|
230
|
+
"capture.md",
|
|
216
231
|
"diagnosis.md",
|
|
217
232
|
"source.md",
|
|
218
233
|
"architecture-review.md",
|
|
@@ -1011,7 +1026,7 @@ function capabilityChecks(root) {
|
|
|
1011
1026
|
"triage",
|
|
1012
1027
|
[
|
|
1013
1028
|
join(root, "T-triage", "T-triage.md"),
|
|
1014
|
-
["source.md", "intake", "reconcile", "publish", "publish.md", "specdev:published", "唯一权威", "远程写入为零"],
|
|
1029
|
+
["source.md", "intake", "reconcile", "publish", "publish.md", "specdev:published", "capture", "capture.md", "specdev:captured", "唯一权威", "远程写入为零"],
|
|
1015
1030
|
],
|
|
1016
1031
|
],
|
|
1017
1032
|
[
|
|
@@ -1140,6 +1155,11 @@ function capabilityChecks(root) {
|
|
|
1140
1155
|
"common/rules/parent-implementation-orchestration.md",
|
|
1141
1156
|
"common/schemas/implementation-map.schema.json",
|
|
1142
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",
|
|
1143
1163
|
]) {
|
|
1144
1164
|
if (!isFile(join(root, required))) errors.push(`missing architecture/wayfinding contract ${required}`);
|
|
1145
1165
|
}
|
|
@@ -1500,6 +1520,99 @@ function validatePublish(path, expectedChange, triage, errors) {
|
|
|
1500
1520
|
return { path, meta, body, rows };
|
|
1501
1521
|
}
|
|
1502
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
|
+
|
|
1503
1616
|
function validateDiagnosis(path, expectedChange, errors) {
|
|
1504
1617
|
if (!isFile(path)) {
|
|
1505
1618
|
errors.push("missing diagnosis artifact");
|
|
@@ -1910,7 +2023,10 @@ function validateMap(path, errors, repoRoot = null) {
|
|
|
1910
2023
|
}
|
|
1911
2024
|
|
|
1912
2025
|
function validateGoalPlan(path, errors) {
|
|
1913
|
-
if (!isFile(path))
|
|
2026
|
+
if (!isFile(path)) {
|
|
2027
|
+
errors.push(`missing Goal Plan: ${basename(path)}`);
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
1914
2030
|
const { meta, body } = parseFrontmatter(path);
|
|
1915
2031
|
const required = [
|
|
1916
2032
|
"schema_version",
|
|
@@ -1971,6 +2087,7 @@ function validateGoalPlan(path, errors) {
|
|
|
1971
2087
|
const validReadyStates = new Set(["ready", "in_progress"]);
|
|
1972
2088
|
const validNotReadyStates = new Set(["draft", "blocked", "completed"]);
|
|
1973
2089
|
if (
|
|
2090
|
+
typeof meta.ready_for_execution !== "boolean" ||
|
|
1974
2091
|
(meta.ready_for_execution === true && !validReadyStates.has(meta.status)) ||
|
|
1975
2092
|
(meta.ready_for_execution === false && !validNotReadyStates.has(meta.status))
|
|
1976
2093
|
) {
|
|
@@ -2201,6 +2318,10 @@ function validateTicket(path, errors) {
|
|
|
2201
2318
|
const ticketId = String(meta.id);
|
|
2202
2319
|
if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
|
|
2203
2320
|
if (!VALID_TICKET_STATUS.has(meta.status)) errors.push(`${basename(path)}: invalid status ${meta.status}`);
|
|
2321
|
+
if (typeof meta.ready !== "boolean") errors.push(`${basename(path)}: ready must be a boolean`);
|
|
2322
|
+
if (new Set(["ready", "in_progress", "review"]).has(meta.status) && meta.ready !== true) {
|
|
2323
|
+
errors.push(`${basename(path)}: executable Ticket must keep ready=true`);
|
|
2324
|
+
}
|
|
2204
2325
|
if (meta.kind != null && meta.kind !== "" && !VALID_TICKET_KIND.has(meta.kind)) {
|
|
2205
2326
|
errors.push(`${basename(path)}: invalid kind ${meta.kind}`);
|
|
2206
2327
|
}
|
|
@@ -2697,12 +2818,10 @@ function validateGitEvidence(repoRoot, changeStatus, errors) {
|
|
|
2697
2818
|
}
|
|
2698
2819
|
|
|
2699
2820
|
function validateParentImplementation(change, parentStatus, stage, errors, warnings, repoRoot = null) {
|
|
2700
|
-
const required = stage === "goal-plan";
|
|
2701
2821
|
const mapPath = join(change, "implementation-map.md");
|
|
2702
2822
|
const planPath = join(change, "implementation-plan.md");
|
|
2703
|
-
if (!
|
|
2704
|
-
if (!isFile(
|
|
2705
|
-
if (!isFile(planPath)) errors.push("goal-plan stage requires implementation-plan.md");
|
|
2823
|
+
if (!isFile(mapPath)) errors.push("parent implementation requires implementation-map.md");
|
|
2824
|
+
if (!isFile(planPath)) errors.push("parent implementation requires implementation-plan.md");
|
|
2706
2825
|
if (!isFile(mapPath) || !isFile(planPath)) return null;
|
|
2707
2826
|
|
|
2708
2827
|
const parentName = basename(change);
|
|
@@ -3080,7 +3199,7 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
3080
3199
|
if (count > 1) errors.push(`repository/ref integration must be serialized for ${ref}; found ${count}`);
|
|
3081
3200
|
}
|
|
3082
3201
|
|
|
3083
|
-
if (
|
|
3202
|
+
if (stage === "goal-plan" && parentStatus && new Set(["active", "blocked"]).has(parentStatus.change_status) && !new Set(["specdev/goal-plan"]).has(parentStatus.current_work)) {
|
|
3084
3203
|
errors.push("parent active/blocked status must keep current_work=specdev/goal-plan");
|
|
3085
3204
|
}
|
|
3086
3205
|
if (parentStatus?.change_status === "completed") {
|
|
@@ -3151,12 +3270,20 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3151
3270
|
const workspace = resolveWorkspaceContract(repoRoot, change);
|
|
3152
3271
|
errors.push(...workspace.errors);
|
|
3153
3272
|
|
|
3273
|
+
// A partial parent must never fall back to the single-change artifact contract.
|
|
3274
|
+
const mapPath = join(change, "tickets-map.md");
|
|
3275
|
+
const isParentImplementation = isFile(join(change, "implementation-map.md")) ||
|
|
3276
|
+
isFile(join(change, "implementation-plan.md")) ||
|
|
3277
|
+
(isFile(mapPath) && parseFrontmatter(mapPath).meta.artifact === "goal-tickets-map");
|
|
3154
3278
|
const changeStatus = validateChangeStatus(join(change, ".status.json"), basename(change), errors);
|
|
3155
|
-
validateParentImplementation(change, changeStatus, stage, errors, warnings, repoRoot);
|
|
3279
|
+
if (isParentImplementation) validateParentImplementation(change, changeStatus, stage, errors, warnings, repoRoot);
|
|
3156
3280
|
errors.push(...validateInitiative(change));
|
|
3157
3281
|
if (isFile(join(change, "source-issue.md"))) {
|
|
3158
3282
|
errors.push("obsolete source-issue.md is forbidden; use source.md without compatibility fallback");
|
|
3159
3283
|
}
|
|
3284
|
+
if (isFile(join(change, "capture.md"))) {
|
|
3285
|
+
errors.push("capture.md is workspace-owned at specdev/capture.md; change-level copies are forbidden");
|
|
3286
|
+
}
|
|
3160
3287
|
const sourceRequired = stage === "triage";
|
|
3161
3288
|
const sourcePath = join(change, "source.md");
|
|
3162
3289
|
const source = isFile(sourcePath) || sourceRequired
|
|
@@ -3181,7 +3308,6 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3181
3308
|
validatePrototypes(change, stage === "prototype", errors);
|
|
3182
3309
|
validateChangeLearning(change, stage === "learn-change", errors);
|
|
3183
3310
|
|
|
3184
|
-
const isParentImplementation = isFile(join(change, "implementation-map.md"));
|
|
3185
3311
|
const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage) && !isParentImplementation;
|
|
3186
3312
|
const specPath = join(change, "spec.md");
|
|
3187
3313
|
const spec = isFile(specPath) || specRequired
|
|
@@ -3189,10 +3315,9 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3189
3315
|
: null;
|
|
3190
3316
|
const ticketMode = isDirectory(join(change, "ticket"));
|
|
3191
3317
|
const mapRequired = !isParentImplementation && (new Set(["tickets", "goal-plan"]).has(stage) || (stage === "implement" && ticketMode));
|
|
3192
|
-
const mapPath = join(change, "tickets-map.md");
|
|
3193
3318
|
const ticketsMap = isFile(mapPath) || mapRequired ? validateMap(mapPath, errors, repoRoot) : null;
|
|
3194
3319
|
const goalPlanPath = join(change, "goal-plan.md");
|
|
3195
|
-
const goalPlan = isFile(goalPlanPath) || stage === "goal-plan"
|
|
3320
|
+
const goalPlan = isFile(goalPlanPath) || (stage === "goal-plan" && !isParentImplementation)
|
|
3196
3321
|
? validateGoalPlan(goalPlanPath, errors)
|
|
3197
3322
|
: null;
|
|
3198
3323
|
validateDesignTree(join(change, "design-tree.json"), change, errors);
|
|
@@ -3411,14 +3536,6 @@ function validateChange(change, stage = null, repoRoot = null) {
|
|
|
3411
3536
|
}
|
|
3412
3537
|
|
|
3413
3538
|
if (goalPlan?.meta.ready_for_execution === true) {
|
|
3414
|
-
const notReady = [...tickets]
|
|
3415
|
-
.filter(
|
|
3416
|
-
([, artifact]) =>
|
|
3417
|
-
!new Set(["done", "cancelled"]).has(artifact.meta.status) &&
|
|
3418
|
-
artifact.meta.ready !== true,
|
|
3419
|
-
)
|
|
3420
|
-
.map(([ticketId]) => ticketId);
|
|
3421
|
-
if (notReady.length) errors.push(`Goal Plan is Ready but Tickets are not Ready: ${JSON.stringify(notReady)}`);
|
|
3422
3539
|
if (spec && spec.meta.ready_for_tickets !== true) {
|
|
3423
3540
|
errors.push("Goal Plan is Ready but Spec is not ready_for_tickets");
|
|
3424
3541
|
}
|
|
@@ -3495,7 +3612,7 @@ function printResults(errors, warnings) {
|
|
|
3495
3612
|
}
|
|
3496
3613
|
|
|
3497
3614
|
function usage() {
|
|
3498
|
-
console.error("Usage: node validate-specdev.mjs [--stage <stage>] [--repo <project-root>] <change-directory> | --self-check");
|
|
3615
|
+
console.error("Usage: node validate-specdev.mjs [--stage <stage>] [--repo <project-root>] <change-directory> | --self-check | --capture <capture.md>");
|
|
3499
3616
|
return 2;
|
|
3500
3617
|
}
|
|
3501
3618
|
|
|
@@ -3503,6 +3620,7 @@ function main(argv) {
|
|
|
3503
3620
|
let selfCheckRequested = false;
|
|
3504
3621
|
let stage = null;
|
|
3505
3622
|
let repoRoot = null;
|
|
3623
|
+
let capturePath = null;
|
|
3506
3624
|
const positional = [];
|
|
3507
3625
|
for (let index = 0; index < argv.length; index += 1) {
|
|
3508
3626
|
const arg = argv[index];
|
|
@@ -3518,6 +3636,11 @@ function main(argv) {
|
|
|
3518
3636
|
index += 1;
|
|
3519
3637
|
} else if (arg.startsWith("--repo=")) {
|
|
3520
3638
|
repoRoot = arg.slice("--repo=".length);
|
|
3639
|
+
} else if (arg === "--capture") {
|
|
3640
|
+
capturePath = argv[index + 1] ?? null;
|
|
3641
|
+
index += 1;
|
|
3642
|
+
} else if (arg.startsWith("--capture=")) {
|
|
3643
|
+
capturePath = arg.slice("--capture=".length);
|
|
3521
3644
|
} else if (arg.startsWith("--")) {
|
|
3522
3645
|
return usage();
|
|
3523
3646
|
} else {
|
|
@@ -3526,12 +3649,18 @@ function main(argv) {
|
|
|
3526
3649
|
}
|
|
3527
3650
|
if (stage !== null && !VALID_STAGES.has(stage)) return usage();
|
|
3528
3651
|
if (selfCheckRequested) {
|
|
3529
|
-
if (positional.length || stage !== null) return usage();
|
|
3652
|
+
if (positional.length || stage !== null || capturePath) return usage();
|
|
3530
3653
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
3531
3654
|
const root = resolve(scriptDirectory, "..", "..");
|
|
3532
3655
|
const result = selfCheck(root);
|
|
3533
3656
|
return printResults(result.errors, result.warnings);
|
|
3534
3657
|
}
|
|
3658
|
+
if (capturePath) {
|
|
3659
|
+
if (positional.length || stage !== null || repoRoot) return usage();
|
|
3660
|
+
const errors = [];
|
|
3661
|
+
validateCapture(resolve(capturePath), errors);
|
|
3662
|
+
return printResults(errors, []);
|
|
3663
|
+
}
|
|
3535
3664
|
if (positional.length === 1) {
|
|
3536
3665
|
const result = validateChange(resolve(positional[0]), stage, repoRoot);
|
|
3537
3666
|
return printResults(result.errors, result.warnings);
|
|
@@ -3539,7 +3668,7 @@ function main(argv) {
|
|
|
3539
3668
|
return usage();
|
|
3540
3669
|
}
|
|
3541
3670
|
|
|
3542
|
-
export { parseFrontmatter, validateChange, validateTicket, validateMap, pathsOverlap, findCycle };
|
|
3671
|
+
export { parseFrontmatter, validateChange, validateTicket, validateMap, pathsOverlap, findCycle, validateCapture };
|
|
3543
3672
|
|
|
3544
3673
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
3545
3674
|
try { process.exitCode = main(process.argv.slice(2)); }
|