@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.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/template/canonical/canonical-specdev-goal-plan.md +8 -5
  4. package/template/canonical/canonical-specdev-grill-with-docs.md +5 -4
  5. package/template/canonical/canonical-specdev-spec.md +7 -4
  6. package/template/canonical/canonical-specdev-tickets.md +19 -4
  7. package/template/commands/handoff.md +1 -1
  8. package/template/commands/status.md +1 -1
  9. package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +3 -3
  10. package/template/workflows/specdev/I-implement/references/implementation-procedure.md +1 -1
  11. package/template/workflows/specdev/I-init-setup/tracking-template.md +1 -1
  12. package/template/workflows/specdev/INDEX.md +1 -1
  13. package/template/workflows/specdev/README.md +4 -4
  14. package/template/workflows/specdev/T-tickets/ticket-template.md +1 -0
  15. package/template/workflows/specdev/T-triage/T-triage.md +44 -13
  16. package/template/workflows/specdev/T-triage/close-comment-template.md +42 -0
  17. package/template/workflows/specdev/T-triage/intake-protocol.md +6 -2
  18. package/template/workflows/specdev/T-triage/issue-body-template.md +34 -0
  19. package/template/workflows/specdev/T-triage/publish-protocol.md +92 -0
  20. package/template/workflows/specdev/T-triage/publish-template.md +49 -0
  21. package/template/workflows/specdev/T-triage/reconcile-protocol.md +3 -3
  22. package/template/workflows/specdev/T-triage/references/classification-map.md +53 -0
  23. package/template/workflows/specdev/T-triage/references/public-projection.md +66 -0
  24. package/template/workflows/specdev/T-triage/tools/publish-status.mjs +148 -0
  25. package/template/workflows/specdev/T-triage/triage-template.md +12 -1
  26. package/template/workflows/specdev/common/README.md +1 -0
  27. package/template/workflows/specdev/common/rules/artifact-contract.md +5 -4
  28. package/template/workflows/specdev/common/rules/change-completion.md +1 -1
  29. package/template/workflows/specdev/common/rules/evidence-and-verification.md +2 -0
  30. package/template/workflows/specdev/common/rules/workflow-routing.md +2 -1
  31. package/template/workflows/specdev/common/schemas/publish.schema.json +21 -0
  32. package/template/workflows/specdev/common/schemas/ticket.schema.json +11 -0
  33. package/template/workflows/specdev/common/schemas/triage.schema.json +3 -1
  34. package/template/workflows/specdev/common/tools/validate-specdev.mjs +173 -3
@@ -0,0 +1,34 @@
1
+ # Published Ticket 正文模板
2
+
3
+ publish 创建 GitHub Issue 时使用。先套 `<Path>{roots.workflows}/specdev/T-triage/references/public-projection.md</Path>` 脱敏,再填本模板。占位符不得把 state Path 带出去。
4
+
5
+ ```markdown
6
+ > *This was generated by AI during SpecDev T-triage publish.*
7
+
8
+ ## Published Ticket
9
+
10
+ **Category:** <bug | feature | refactor | investigation | operations | documentation | review>
11
+ **Change:** <change-name>
12
+ **Ticket:** T-01
13
+ **Origin:** local | intake
14
+ **Parent:** Part of #<n> / none
15
+
16
+ **Summary:** <一句话,已经发生的事实。>
17
+
18
+ **Delivered behavior:**
19
+ <完成后系统对外可见的行为。写「现在会怎样」,不写「去改哪个文件」。>
20
+
21
+ **Acceptance:**
22
+ - [x] AC-001:<可公开验收,状态按 Evidence 勾选>
23
+
24
+ **Blocked by:**
25
+ - #<n> <title>
26
+ - None
27
+
28
+ **Out of scope:**
29
+ <票里 OUT 列的可公开项;没有则写「无」。>
30
+
31
+ **Provenance:**
32
+ - Public commit / PR: <url or sha or 无>
33
+ - Marker: `specdev:<change>:T-01:published`
34
+ ```
@@ -0,0 +1,92 @@
1
+ # Publish Protocol
2
+
3
+ 仅在 Triage `mode=publish` 时加载。目标是把已完成 Change 的计划内 Ticket 投影为带分类标签的 GitHub Issue,用脱敏 Evidence 关闭它们,并留下 `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>`。
4
+
5
+ GitHub 是投影和计数器,不是开发权威。源 Issue 的关闭仍走 reconcile,与本协议互不覆盖。
6
+
7
+ ## 1. 触发与对象
8
+
9
+ 用户必须指定一个已存在的 change(active 或尚未归档的 completed)。禁止扫整个 workspace 自动发。
10
+
11
+ 可选开关:`include-cancelled`、`force-parent` / `no-parent`、排除票号列表。目标 repo 默认从 `git remote` 推断,必须展示确认。
12
+
13
+ 没有先前 intake 时允许补写:
14
+
15
+ - `<Path>{roots.state}/specdev/changes/{change}/triage.md</Path>`,`mode: publish`,`publish_action: pending`;
16
+ - 轻量 `<Path>{roots.state}/specdev/changes/{change}/source.md</Path>`:`source_type` 为 `conversation | pasted | local-file`,`canonical_locator: null`,`close_capability: not-applicable`。禁止虚构 GitHub locator。
17
+
18
+ publish **不得**改 Ticket / Spec / Evidence / Goal / change completed 事实。只写 `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/triage.md</Path>` 的 publish 字段,以及经授权的远程投影。
19
+
20
+ ## 2. 硬门
21
+
22
+ 失败即停,零远程写入。`publish_action` 保持 `not-requested`(尚未开始)或变为 `publish-failed`(已开始后失败)。
23
+
24
+ 1. 按 `<Path>{roots.workflows}/specdev/common/rules/change-completion.md</Path>` 重验本地完成。
25
+ 2. 至少一张可发布票:status `done`,或用户选择纳入的 `cancelled`。没有 Ticket 文件(纯 Direct Spec)时停止;v1 不发 Change 级单 Issue。
26
+ 3. 调用 `<Path>{roots.skills}/github-npm-ops/SKILL.md</Path>` 能读目标 repo、能 dry-run `issue-create`。
27
+ 4. 分类映射见 `<Path>{roots.workflows}/specdev/T-triage/references/classification-map.md</Path>`。缺 type 标签时 dry-run 表列出将创建的标签,授权后才 `gh label create`。
28
+ 5. Change `classification` 为 `mixed` 时,确认表上每张纳入票都有 `kind`。
29
+ 6. 用户看见完整 dry-run 表(标题、标签、正文、关闭评论、reason)并给出 **本次** 明确确认。
30
+
31
+ cancelled 默认跳过,账本记 `skipped:cancelled`。只有 `include-cancelled` 或确认表勾选才建 Issue,并以 `not_planned` 关闭。
32
+
33
+ 父 Change Issue:纳入票数 ≥ 2 时默认创建;1 票时票 Issue 本身就是记录。`force-parent` / `no-parent` 覆盖默认。v1 只用正文 `Part of #<n>` / `Blocked by #<n>`,不上 native sub-issue API。
34
+
35
+ ## 3. 准备投影
36
+
37
+ 对每张纳入票读取 Ticket 与对应 Evidence,按 `<Path>{roots.workflows}/specdev/T-triage/references/public-projection.md</Path>` 脱敏。
38
+
39
+ - Issue 正文:`<Path>{roots.workflows}/specdev/T-triage/issue-body-template.md</Path>`(过去时)。
40
+ - 关闭评论:`<Path>{roots.workflows}/specdev/T-triage/close-comment-template.md</Path>`。
41
+ - `kind` 缺省继承 Change 分类;写回票 frontmatter 可选,账本该行必须有 `kind`。
42
+ - origin:源 `close_capability` 为 supported 或 locator 是 GitHub Issue/PR 则为 `intake`,否则 `local`。
43
+ - marker:`specdev:<change>:<ticket-id>:published`。
44
+
45
+ 用 `<Path>{roots.workflows}/specdev/T-triage/publish-template.md</Path>` 写或更新 `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>`。先把纳入行标 `planned`,跳过行标 `skipped:cancelled` 或 `skipped:excluded`,`publish_action: pending`。
46
+
47
+ ## 4. 幂等执行
48
+
49
+ 顺序:先父(若有)→ 按 `blocked_by` 拓扑建子 → 回写父正文真实编号 → 关子 → 关父。
50
+
51
+ 对每张票:
52
+
53
+ 1. `issue-search` 查 marker 或精确标题 + `specdev:published`。
54
+ 2. 已存在且 marker 在且已关闭 → 账本 `closed`,跳过。
55
+ 3. 已存在且 marker 在但仍 open → 只补 `issue-comment-close`。
56
+ 4. 已存在但无 marker → 停止该行,展示冲突,不覆盖别人的 Issue。
57
+ 5. 不存在 → dry-run `issue-create` → 授权后 `--apply` → 立刻 `issue-comment-close`,marker 双写(正文 Provenance + 关闭评论)。
58
+ 6. 任一步失败:该行 `failed`,记录已完成步骤和下一重试动作;已成功的票不回滚。
59
+
60
+ `--apply` 只表示调用方完成本次确认,不替代授权。远程失败不回滚本地完成事实。
61
+
62
+ 全部纳入票到达 `closed` 或合法 `skipped:*`,且无 `failed` 时:`publish_action: published`,填写 `published_at`。用户明确放弃:`waived`。重跑是恢复,不是重发。
63
+
64
+ 摄入源 Change:本协议只建票 Issue。原来的源 Issue 仍由 reconcile 关闭;源 Issue ≠ 票 Issue。reconcile 评论可以链接已发布编号。
65
+
66
+ ## 5. 校验与计数
67
+
68
+ ```bash
69
+ node <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> \
70
+ --stage triage \
71
+ <Path>{roots.state}/specdev/changes/{change}</Path>
72
+ ```
73
+
74
+ workspace 计数:
75
+
76
+ ```bash
77
+ node <Path>{roots.workflows}/specdev/T-triage/tools/publish-status.mjs</Path> \
78
+ --state-root <Path>{roots.state}/specdev</Path>
79
+ ```
80
+
81
+ 数字只来自 active + archive 的 `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>`,不把 GitHub 搜索当权威。
82
+
83
+ ## 完成标准
84
+
85
+ - 指定 completed Change 的 done 票均有对应 GitHub Issue,或账本有合法 skip;
86
+ - 每条已发布 Issue 带且仅带一个 type 标签,以及 `specdev:published` 与 `origin:*`;
87
+ - 关闭评论是 Evidence 脱敏投影;
88
+ - marker 使重跑不重复建贴;
89
+ - 本地源计入 `published_issues`;
90
+ - `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>` 与远程重读一致,失败行可恢复;
91
+ - Ticket / Spec / Evidence / completed 事实未被改写;
92
+ - 未授权时远程写入为零。
@@ -0,0 +1,49 @@
1
+ ---
2
+ schema_version: 1
3
+ artifact: publish
4
+ change: <YYYY-MM-DD-topic>
5
+ mode: publish
6
+ repo: owner/repo
7
+ publish_action: pending
8
+ include_cancelled: false
9
+ parent_issue: null
10
+ origin: local
11
+ published_at: null
12
+ updated_at: <ISO-8601>
13
+ ---
14
+
15
+ # Publish: <change>
16
+
17
+ ## 发布计划
18
+
19
+ - **目标 repo:** `owner/repo`
20
+ - **origin:** local / intake
21
+ - **含 cancelled:** false
22
+ - **父 Issue:** 默认(纳入票 ≥ 2 时创建)/ 强制开 / 强制关
23
+ - **排除票:** 无 / T-0N
24
+ - **确认记录:** 无 / 本次授权时间与操作员
25
+
26
+ publish 不改写 Ticket、Spec、Evidence、Goal 或 change completed 事实。
27
+
28
+ ## 账本
29
+
30
+ | ticket | kind | labels | number | url | marker | sha256 | state |
31
+ |---|---|---|---|---|---|---|---|
32
+ | T-01 | bug | bug, specdev:published, origin:local | — | — | specdev:<change>:T-01:published | — | planned |
33
+
34
+ `state`:`planned | created | commented | closed | skipped:cancelled | skipped:excluded | failed`。
35
+
36
+ `sha256` 覆盖实际发出的 Issue 正文 + 关闭评论。漂移只警告,不自动重发。
37
+
38
+ ## 计数
39
+
40
+ - **published_closed:** 0
41
+ - **published_open:** 0
42
+ - **skipped:** 0
43
+ - **failed:** 0
44
+
45
+ 计数权威是本文件,不是 GitHub 搜索。`published_issues` = closed + created(仍 open 的失败中途行)。
46
+
47
+ ## 重试
48
+
49
+ 无 / 失败行的已完成步骤和下一步。已成功的票不回滚。
@@ -1,6 +1,6 @@
1
1
  # Reconcile Protocol
2
2
 
3
- 仅在 Triage `mode=reconcile` 时加载。目标是把本地完成结果最小化投影到来源 Issue,并关闭它。
3
+ 仅在 Triage `mode=reconcile` 时加载。目标是把本地完成结果最小化投影到来源 Issue,并关闭它。票级发布走 publish,不走本协议。
4
4
 
5
5
  ## 1. 本地完成硬门
6
6
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## 2. 准备关闭计划
12
12
 
13
- 从本地 Evidence 生成简短评论:完成结果、关键验证、可公开的 commit/PR URL、残余风险或后续工作。不得发布 state Path、机器路径、秘密、内部日志或未核对声明。
13
+ 从本地 Evidence 生成简短评论:完成结果、关键验证、可公开的 commit/PR URL、残余风险或后续工作。遵守 `<Path>{roots.workflows}/specdev/T-triage/references/public-projection.md</Path>`。若同一 change 已 publish,评论可以链接票 Issue 编号;源 Issue 仍不是那些票 Issue。
14
14
 
15
15
  向用户展示准确 repo、Issue 编号、评论全文、close reason 和将执行的动作。只有本次明确确认后才继续;拒绝或暂不执行时零远程写入。用户明确放弃关闭可将状态置为 `waived`。
16
16
 
@@ -23,7 +23,7 @@
23
23
  - marker 不存在:评论成功后再 close;
24
24
  - 任一步失败:记录已完成步骤、错误和下一重试动作,状态为 `close-failed`。
25
25
 
26
- 成功后重读远程状态,只有观察到 closed 才写 `external_action: closed`。
26
+ 成功后重读远程状态,只有观察到 closed 才写 `external_action: closed`。不改写 `publish_action`。
27
27
 
28
28
  ## 完成标准
29
29
 
@@ -0,0 +1,53 @@
1
+ # Classification Map
2
+
3
+ T-triage 本地分类与 GitHub type 标签的对照。publish 只打本表允许的标签;intake 仍把 8 值写在 `<Path>{roots.state}/specdev/changes/{change}/triage.md</Path>`,v1 不回写源 Issue。
4
+
5
+ ## SpecDev 分类
6
+
7
+ Change 级 `classification`:`bug | feature | refactor | investigation | operations | documentation | review | mixed`。
8
+
9
+ 票级 `kind` 与上相同,去掉 `mixed`。缺省合法:publish 时继承 Change 分类;Change 为 `mixed` 时确认表必须给出每张纳入票的 `kind`。
10
+
11
+ ## GitHub type 标签
12
+
13
+ 优先使用目标仓库已有标签。确认表展示实际将打的字符串。
14
+
15
+ | SpecDev | GitHub type(优先) | 降级 | 标题前缀 |
16
+ |---|---|---|---|
17
+ | `bug` | `bug` | — | `bug:` |
18
+ | `feature` | `feature-request` | `enhancement` | `feature:` |
19
+ | `refactor` | `enhancement`,若仓库已有 `refactor` 则额外打 `refactor` | — | `refactor:` |
20
+ | `investigation` | `question` | `enhancement` | `investigate:` |
21
+ | `operations` | `operations` | `enhancement` | `ops:` |
22
+ | `documentation` | `documentation` | — | `docs:` |
23
+ | `review` | `enhancement` | — | `review:` |
24
+ | `mixed` | 禁止直接发布 | 拆到票级 `kind` | — |
25
+
26
+ 每个已发布 Issue 恰好一个 type 标签(`refactor` 的额外 `refactor` 不算第二 type)。
27
+
28
+ ## 固定附加标签
29
+
30
+ - `specdev:published`:计数与去重。
31
+ - `origin:local` 或 `origin:intake`:来源。本地对话/粘贴/项目文件为 `local`;冻结自 GitHub Issue/PR 为 `intake`。
32
+ - 父 Change 索引 Issue 另打 `specdev:change`。`mixed` 的父 Issue 不打 type。
33
+
34
+ ## 可选标签
35
+
36
+ - `area:*`:仅当 tickets-map / `expected_changes` 能唯一推断顶层目录时才打;不确定则不问不打。
37
+ - `priority:*`:由 triage `risk` 映射:`critical→priority:critical`,`high→priority:high`,`medium→priority:medium`,`low→priority:low`。确认表可改。
38
+
39
+ ## 禁止标签
40
+
41
+ 不得打到自发布 Issue 或父索引上:
42
+
43
+ `needs-triage`、`needs-info`、`ready-for-agent`、`ready-for-human`、`wontfix`、`duplicate`、`invalid`、`stale`。
44
+
45
+ cancelled 票若被纳入,使用 close reason `not_planned`,不打 `wontfix`。
46
+
47
+ ## 标题
48
+
49
+ ```text
50
+ <前缀> <ticket.title>
51
+ ```
52
+
53
+ 不把内部 id `T-01` 写入标题。内部 id 只出现在正文 Provenance 和 `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>`。
@@ -0,0 +1,66 @@
1
+ # Public Projection
2
+
3
+ T-triage 远程正文的脱敏与耐久规则。intake 不写远程。reconcile 关闭评论与 publish 的 Issue 正文 / 关闭评论都遵守本文件。
4
+
5
+ ## Disclaimer
6
+
7
+ 每条远程 Issue 正文和评论必须以这一行开头:
8
+
9
+ ```markdown
10
+ > *This was generated by AI during SpecDev T-triage publish.*
11
+ ```
12
+
13
+ reconcile 关闭评论把 `publish` 换成 `reconcile`:
14
+
15
+ ```markdown
16
+ > *This was generated by AI during SpecDev T-triage reconcile.*
17
+ ```
18
+
19
+ ## 允许写入远程的字段
20
+
21
+ - 已发生的行为与验收(过去时)
22
+ - 可公开 commit / PR URL
23
+ - 验证结论:定向 / 回归 / E2E 是否跑过、是否通过;未跑时一句原因
24
+ - 残余风险(可公开)
25
+ - 分类、origin、Change 名、Ticket id、marker
26
+ - `Part of #<n>` / `Blocked by #<n>`
27
+ - cancelled 原因:`cancelled-already-implemented`(指到已有公开位置)或 `cancelled-out-of-scope`(指到 ADR / 用户决定)
28
+
29
+ 项目相对路径只在描述公开模块边界时允许,并注明「当时路径,可能已变」。
30
+
31
+ ## 禁止写入远程
32
+
33
+ - 状态根、工作流根、技能根别名与任何 Path 标签
34
+ - 机器绝对路径、worktree 路径、state 文件路径
35
+ - token、Cookie、密钥、私人日志、未核对声明
36
+ - skill JSON、workspace 策略原文、candidate 内部字段、内部 SHA 链(除非该 SHA 已是将推送的公开 commit)
37
+ - 「请 agent 去实现」的祈使句
38
+ - 文件行号
39
+
40
+ 关闭评论是 Evidence 的公共投影,不是副本。不得把 Evidence 原文贴出去。
41
+
42
+ ## 最低关闭评论
43
+
44
+ 即使脱敏后也必须保留:
45
+
46
+ 1. 结果一句话(通过 / 取消原因)
47
+ 2. 公开 commit 或 PR URL;没有则写「无公开 commit/PR」
48
+ 3. 验证结论四字级摘要(通过 / 未跑 + 原因)
49
+
50
+ ## Marker
51
+
52
+ 票 Issue:
53
+
54
+ ```text
55
+ specdev:<change>:<ticket-id>:published
56
+ ```
57
+
58
+ 同时写入 Issue 正文 Provenance 和关闭评论,供 `issue-search` 去重。
59
+
60
+ 父索引:
61
+
62
+ ```text
63
+ specdev:<change>:publish-index
64
+ ```
65
+
66
+ reconcile 源 Issue 仍用 `specdev:<change>:completion`,与 publish marker 互不替代。
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Aggregate published-issue counts from active + archive publish.md ledgers.
4
+ * Local files are authoritative; this tool does not query GitHub.
5
+ */
6
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const COUNTED = new Set(["closed", "created", "commented"]);
11
+ const SKIPPED = new Set(["skipped:cancelled", "skipped:excluded"]);
12
+
13
+ function parseFrontmatter(text) {
14
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
15
+ if (!match) return {};
16
+ const meta = {};
17
+ for (const line of match[1].split(/\r?\n/)) {
18
+ const idx = line.indexOf(":");
19
+ if (idx === -1) continue;
20
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
21
+ }
22
+ return meta;
23
+ }
24
+
25
+ function parseLedgerStates(body) {
26
+ const states = [];
27
+ for (const line of body.split(/\r?\n/)) {
28
+ if (!/^\|/.test(line) || /^\|\s*-+/.test(line) || /^\|\s*ticket\s*\|/i.test(line)) continue;
29
+ const cells = line.split("|").map((c) => c.trim()).filter((_, i, arr) => i > 0 && i < arr.length - 1);
30
+ if (cells.length < 8) continue;
31
+ states.push(cells[7]);
32
+ }
33
+ return { states };
34
+ }
35
+
36
+ function collectLedgers(stateRoot) {
37
+ const roots = [
38
+ join(stateRoot, "changes"),
39
+ join(stateRoot, "archive"),
40
+ ];
41
+ const files = [];
42
+ function walk(dir, depth = 0) {
43
+ if (!existsSync(dir) || depth > 6) return;
44
+ for (const name of readdirSync(dir)) {
45
+ const p = join(dir, name);
46
+ let st;
47
+ try {
48
+ st = statSync(p);
49
+ } catch {
50
+ continue;
51
+ }
52
+ if (st.isDirectory()) walk(p, depth + 1);
53
+ else if (name === "publish.md") files.push(p);
54
+ }
55
+ }
56
+ for (const root of roots) walk(root);
57
+ return files;
58
+ }
59
+
60
+ function summarize(stateRoot) {
61
+ const files = collectLedgers(stateRoot);
62
+ let published = 0;
63
+ let skipped = 0;
64
+ let failed = 0;
65
+ let originLocal = 0;
66
+ let originIntake = 0;
67
+ const byChange = [];
68
+
69
+ for (const file of files) {
70
+ const text = readFileSync(file, "utf8");
71
+ const meta = parseFrontmatter(text);
72
+ const body = text.replace(/^---[\s\S]*?---/, "");
73
+ const { states } = parseLedgerStates(body);
74
+ const origin = meta.origin === "intake" ? "intake" : "local";
75
+ let changePublished = 0;
76
+ let changeSkipped = 0;
77
+ let changeFailed = 0;
78
+ for (const state of states) {
79
+ if (COUNTED.has(state)) {
80
+ changePublished += 1;
81
+ if (origin === "intake") originIntake += 1;
82
+ else originLocal += 1;
83
+ } else if (SKIPPED.has(state)) {
84
+ changeSkipped += 1;
85
+ } else if (state === "failed") {
86
+ changeFailed += 1;
87
+ }
88
+ }
89
+ published += changePublished;
90
+ skipped += changeSkipped;
91
+ failed += changeFailed;
92
+ byChange.push({
93
+ change: meta.change || file,
94
+ origin,
95
+ publish_action: meta.publish_action || "unknown",
96
+ published: changePublished,
97
+ skipped: changeSkipped,
98
+ failed: changeFailed,
99
+ });
100
+ }
101
+
102
+ return {
103
+ published_issues: published,
104
+ origin: { local: originLocal, intake: originIntake },
105
+ publish_skipped: skipped,
106
+ publish_failed: failed,
107
+ ledgers: files.length,
108
+ changes: byChange,
109
+ };
110
+ }
111
+
112
+ function usage() {
113
+ console.error("Usage: node publish-status.mjs --state-root <specdev-state-root> [--json]");
114
+ process.exit(2);
115
+ }
116
+
117
+ function main(argv) {
118
+ let stateRoot = null;
119
+ let json = false;
120
+ for (let i = 0; i < argv.length; i += 1) {
121
+ if (argv[i] === "--state-root") {
122
+ stateRoot = argv[i + 1];
123
+ i += 1;
124
+ } else if (argv[i] === "--json") {
125
+ json = true;
126
+ } else {
127
+ usage();
128
+ }
129
+ }
130
+ if (!stateRoot) usage();
131
+ const summary = summarize(stateRoot);
132
+ if (json) {
133
+ console.log(JSON.stringify(summary, null, 2));
134
+ return 0;
135
+ }
136
+ console.log(
137
+ `published_issues: ${summary.published_issues} (origin:local ${summary.origin.local}, origin:intake ${summary.origin.intake})`,
138
+ );
139
+ console.log(`publish_skipped: ${summary.publish_skipped}`);
140
+ console.log(`publish_failed: ${summary.publish_failed}`);
141
+ return 0;
142
+ }
143
+
144
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
145
+ process.exit(main(process.argv.slice(2)));
146
+ }
147
+
148
+ export { summarize };
@@ -9,6 +9,8 @@ risk: medium
9
9
  route: specdev/wayfinder
10
10
  ready_for_implementation: false
11
11
  external_action: not-applicable
12
+ publish_action: not-requested
13
+ publish: null
12
14
  updated_at: <ISO-8601>
13
15
  ---
14
16
 
@@ -40,4 +42,13 @@ updated_at: <ISO-8601>
40
42
  - **授权记录:** 无
41
43
  - **尝试与结果:** 无
42
44
 
43
- 外部动作只投影最终完成,不替代本地状态、Ticket、Map 或 Evidence。
45
+ 外部动作只投影来源 Issue 的最终完成,不替代本地状态、Ticket、Map 或 Evidence。
46
+
47
+ ## 发布投影
48
+
49
+ - **publish_action:** not-requested / pending / published / publish-failed / waived
50
+ - **账本:** 无 / `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>`
51
+ - **origin:** local / intake
52
+ - **计数:** 见账本;workspace 汇总用 publish-status
53
+
54
+ 发布投影只记账已完成 Ticket,不把 GitHub 提升为开发权威。`publish_action` 与 `external_action` 分立。
@@ -39,6 +39,7 @@
39
39
  - Wayfinder Ticket:`<Path>{roots.workflows}/specdev/common/schemas/wayfinder-ticket.schema.json</Path>`
40
40
  - 来源快照:`<Path>{roots.workflows}/specdev/common/schemas/source.schema.json</Path>`
41
41
  - 分诊:`<Path>{roots.workflows}/specdev/common/schemas/triage.schema.json</Path>`
42
+ - 发布账本:`<Path>{roots.workflows}/specdev/common/schemas/publish.schema.json</Path>`
42
43
  - 诊断:`<Path>{roots.workflows}/specdev/common/schemas/diagnosis.schema.json</Path>`
43
44
  - 代码审查:`<Path>{roots.workflows}/specdev/common/schemas/code-review.schema.json</Path>`
44
45
  - UI 设计包:`<Path>{roots.workflows}/specdev/P-prototype/design-package.schema.json</Path>`
@@ -7,20 +7,21 @@ SpecDev 通过分层工件避免同一决策被多个模型反复重做。每个
7
7
  | 工件 | 具体位置 | 必须决定 | 不应决定 |
8
8
  |---|---|---|---|
9
9
  | 来源快照 | `<Path>{roots.state}/specdev/changes/{change}/source.md</Path>` | 原始请求、捕获时间、locator、hash 和关闭能力 | 当前产品合同或实现状态 |
10
- | 分诊 | `<Path>{roots.state}/specdev/changes/{change}/triage.md</Path>` | 请求类别、影响、风险、缺失输入、下一 work 和远程 reconcile 状态 | 详细实现方案或开发进度 |
10
+ | 分诊 | `<Path>{roots.state}/specdev/changes/{change}/triage.md</Path>` | 请求类别、影响、风险、缺失输入、下一 work、源 Issue reconcile 状态和 publish_action | 详细实现方案、开发进度或票级发布账本 |
11
+ | 发布账本 | `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>` | 票级 GitHub 投影的编号、标签、marker、state 和发布计数 | Ticket 契约、Evidence 原文或源 Issue 关闭 |
11
12
  | 诊断 | `<Path>{roots.state}/specdev/changes/{change}/diagnosis.md</Path>` | 复现、证据、根因、修复不变量和回归契约 | 未经验证的修复实现 |
12
13
  | 设计日志 | `<Path>{roots.state}/specdev/changes/{change}/LOG.md</Path>` | 讨论轨迹、确认、延后、替代与废弃结论 | 当前架构权威摘要 |
13
14
  | 设计树 | `<Path>{roots.state}/specdev/changes/{change}/design-tree.json</Path>` | 决策节点、依赖、当前 frontier、轮次与共识状态 | 领域真相或架构决定正文 |
14
15
  | Change 领域上下文 | `<Path>{roots.state}/specdev/changes/{change}/CONTEXT.md</Path>` | 本 change 已确认、供下游使用的领域术语和语义 | 永久领域知识或临时会议记录 |
15
16
  | Change 架构决策 | `<Path>{roots.state}/specdev/changes/{change}/ADR.md</Path>` | 已成为本 change 下游合同的架构决策、原因、后果和替代关系 | 永久项目 ADR 或尚未决定的方案集合 |
16
17
  | Spec | `<Path>{roots.state}/specdev/changes/{change}/spec.md</Path>` | 用户问题、外部行为、范围、验收合同、非功能要求和已锁定实现约束 | 文件级施工步骤 |
17
- | Ticket | `<Path>{roots.state}/specdev/changes/{change}/ticket/{ticket-file}.md</Path>` | 单一垂直切片的行为、决策、范围、路径所有权、执行路线和验证证据 | 跨 Ticket 里程碑治理 |
18
+ | Ticket | `<Path>{roots.state}/specdev/changes/{change}/ticket/{ticket-file}.md</Path>` | 单一垂直切片的行为、决策、范围、路径所有权、执行路线和验证证据 | 跨 Ticket 里程碑治理或远程 Issue 编号 |
18
19
  | Tickets Map | `<Path>{roots.state}/specdev/changes/{change}/tickets-map.md</Path>` | 总体实施背景、项目 Skill 最低调用路由、依赖 DAG、合同覆盖、Ready 投影、并行候选和路径冲突 | 单 Ticket 的完整实现契约 |
19
20
  | Goal Plan | `<Path>{roots.state}/specdev/changes/{change}/goal-plan.md</Path>` | 跨 Ticket 调度、Gate、共享所有权、迁移顺序、集成和偏差治理 | 复制 Ticket 全文 |
20
21
  | Implementation Map | `<Path>{roots.state}/specdev/changes/{change}/implementation-map.md</Path>` | Ready 成员、组合 Ticket inventory、跨 change dependency/serialization 与 revision | 创建或改写子 Spec、Ticket 或实现细节 |
21
22
  | Implementation Plan | `<Path>{roots.state}/specdev/changes/{change}/implementation-plan.md</Path>` | 父 Lead、全局 workspace/实现上限、frontier/Wave/locks/integration queue 和可恢复进度投影 | 改写子 change 权威或伪造完成 |
22
23
  | Implementation Orchestration Evidence | `<Path>{roots.state}/specdev/changes/{change}/evidence/implementation-orchestration.md</Path>` | 成员完成、组合 Ticket 顺序/锁、repository integration、整体验证、漂移和残余风险 | 新产品/架构决定或单 Ticket Evidence 替代品 |
23
- | Evidence | `<Path>{roots.state}/specdev/changes/{change}/evidence/{ticket-id}.md</Path>` | 实际修改、命令、结果、验收映射、偏差、风险和提交引用 | 新的产品或架构决策 |
24
+ | Evidence | `<Path>{roots.state}/specdev/changes/{change}/evidence/{ticket-id}.md</Path>` | 实际修改、命令、结果、验收映射、偏差、风险和提交引用 | 新的产品或架构决策或远程 Issue 正文 |
24
25
  | Change 学习图解 | `<Path>{roots.state}/specdev/changes/{change}/learning/index.md</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/learning/{number}_{topic}.md</Path>` | 面向零专业背景读者解释当前 change 的已验证工件、实现和测试事实;索引按序号持续追加 | 产品决定、架构决定、实现授权或 Learning workflow 知识 |
25
26
  | 代码审查 | `<Path>{roots.state}/specdev/changes/{change}/reviews/CR-###.md</Path>` | 固定点、标准轴和规范轴 finding | 实施修复或合并两轴排名 |
26
27
  | UI 设计包 | `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/design-system.md</Path>`、`<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/comparison/</Path>` 与 `<Path>{roots.state}/specdev/changes/{change}/prototypes/{design-id}/final/</Path>` | 项目 UI 证据、功能风格候选、逐层用户决定、设计 token、交互合同和可运行 HTML/CSS/JS 投影 | 生产 UI 实现或替用户确认高影响偏好 |
@@ -50,7 +51,7 @@ Change CONTEXT/ADR 是 active change 内的执行权威,不是 workflow 级永
50
51
 
51
52
  当前 change 决定与永久知识冲突时,必须在 LOG/ADR 中显式说明替代关系;它只约束当前 change,直到 A 决定是否提升并更新永久版本。
52
53
 
53
- `<Path>{roots.state}/specdev/changes/{change}/source.md</Path>` 只对“原始输入是什么”具有权威;后续用户决定、ADR 和 Spec 可以显式演进该意图。远程来源在摄入后发生变化不会自动改写本地合同,必须重新 Triage。
54
+ `<Path>{roots.state}/specdev/changes/{change}/source.md</Path>` 只对“原始输入是什么”具有权威;后续用户决定、ADR 和 Spec 可以显式演进该意图。远程来源在摄入后发生变化不会自动改写本地合同,必须重新 Triage。GitHub 上由 publish 投影出的 Issue 不是开发权威;发布计数以 `<Path>{roots.state}/specdev/changes/{change}/publish.md</Path>` 为准。
54
55
 
55
56
  代码事实可以证明计划已过时,但不能静默改写用户目标或已接受契约。出现这种情况时,按 `<Path>{roots.workflows}/specdev/common/rules/deviation-control.md</Path>` 退回相应工件修订。
56
57
 
@@ -29,6 +29,6 @@ Owner 原子更新 `<Path>{roots.state}/specdev/changes/{change}/.status.json</P
29
29
 
30
30
  ## 远程来源与归档
31
31
 
32
- 远程动作不参与本地完成判定。Triage 为 `pending-close`/`close-failed` 时先 reconcile;`closed`、`waived` 或 `not-applicable` 才允许 Archive。归档后工件只读。
32
+ 远程动作不参与本地完成判定。Triage `external_action` 为 `pending-close`/`close-failed` 时先 reconcile;`closed`、`waived` 或 `not-applicable` 才允许 Archive。Triage `publish_action` 为 `pending`/`publish-failed` 时先恢复或结束 publish;`not-requested`、`published` 或 `waived` 才允许 Archive。`not-requested` 是默认,未点过 publish 的 change 不被新模式绑架。归档后工件只读。
33
33
 
34
34
  **完成标准**:完成声明可由本地工件、Git 与验证重建;只有一个 owner 命中;失败 candidate 不污染父分支。
@@ -51,3 +51,5 @@ required Ticket Done 必须有 source commit、通过 candidate、父分支 resu
51
51
  Direct Spec Evidence 至少包含:用户批准与轻量合同、Lead、实施前/最终 checkpoint、实际路径、定向/回归/E2E 命令及环境、验收映射、未运行项、偏差、残余风险和提交授权状态。
52
52
 
53
53
  父实现 change 的 Implementation Orchestration Evidence 不能替代子 Evidence。它至少记录最终 Map revision、全部成员最终状态和子证据指针、dependency/serialization 实际顺序、跨 change 合同检查、aggregate 命令/环境/结果、stale candidate 处理、偏差和残余风险。任何成员未 completed 或整体验证未通过时不得形成父完成证据。
54
+
55
+ Evidence 原文不出仓库。对 GitHub 的公共投影由 T-triage publish 按公共投影规则生成,不替代本文件的完整记录。
@@ -5,6 +5,7 @@
5
5
  |---|---|---|
6
6
  | 需要冻结外部来源或审计摄入 | T-triage intake | D / G / W / P / S / C / T |
7
7
  | 本地 change 完成且来源可关闭 | T-triage reconcile | A |
8
+ | 本地 change 完成且要把 Ticket 记到 GitHub | T-triage publish | reconcile / A / 停止 |
8
9
  | 疑难 bug 或性能回归 | D-diagnose-bugs | S / T / I / R / W |
9
10
  | 模糊但可通过决策访谈收敛 | G-grill-with-docs | P / S / T / W |
10
11
  | 大需求包含多个未界定 change 或未知路径 | W-wayfinder | G / P / D / S / T |
@@ -21,4 +22,4 @@
21
22
  同 change 下一阶段需要当前一手推理且上下文健康时继续;切换 repo/person/harness 或旁路时使用 `<Path>{roots.commands}/handoff.md</Path>`;严格限定且可独立派单时使用 Dispatch Packet;其他长上下文以权威工件路径恢复。平台不支持 clear/compact 时不虚构操作。
22
23
 
23
24
 
24
- 本地已清晰请求不强制绕行 TriageG 是单 change Grill,P-prototype 是 UI 设计,不与统一 P-goal-plan 混淆。W 发现候选后逐个交 G/S/T;只有清晰且 Ready 的 child 才交 P。原缺陷诊断 D、风险分诊和远程 reconcile 职责不删除。
25
+ 本地已清晰请求不强制绕行 Triage。完成后要记账才选 publish;publish 与 reconcile 写不同远程对象,可先后执行。G 是单 change Grill,P-prototype 是 UI 设计,不与统一 P-goal-plan 混淆。W 发现候选后逐个交 G/S/T;只有清晰且 Ready 的 child 才交 P。原缺陷诊断 D、风险分诊、远程 reconcile 和完成后发布投影职责不删除。
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:speculo:specdev:publish:v1",
4
+ "title": "SpecDev Publish Ledger Frontmatter",
5
+ "type": "object",
6
+ "required": ["schema_version", "artifact", "change", "mode", "repo", "publish_action", "include_cancelled", "origin", "updated_at"],
7
+ "properties": {
8
+ "schema_version": {"const": 1},
9
+ "artifact": {"const": "publish"},
10
+ "change": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$"},
11
+ "mode": {"const": "publish"},
12
+ "repo": {"type": "string", "pattern": "^.+/.+$"},
13
+ "publish_action": {"enum": ["pending", "published", "publish-failed", "waived"]},
14
+ "include_cancelled": {"type": "boolean"},
15
+ "parent_issue": {"type": ["integer", "null"]},
16
+ "origin": {"enum": ["local", "intake"]},
17
+ "published_at": {"type": ["string", "null"]},
18
+ "updated_at": {"type": "string", "minLength": 1}
19
+ },
20
+ "additionalProperties": true
21
+ }
@@ -54,6 +54,17 @@
54
54
  "cancelled"
55
55
  ]
56
56
  },
57
+ "kind": {
58
+ "enum": [
59
+ "bug",
60
+ "feature",
61
+ "refactor",
62
+ "investigation",
63
+ "operations",
64
+ "documentation",
65
+ "review"
66
+ ]
67
+ },
57
68
  "planning_depth": {
58
69
  "enum": [
59
70
  "lite",
@@ -8,13 +8,15 @@
8
8
  "schema_version": {"const": 1},
9
9
  "artifact": {"const": "triage"},
10
10
  "change": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$"},
11
- "mode": {"enum": ["intake", "reconcile"]},
11
+ "mode": {"enum": ["intake", "reconcile", "publish"]},
12
12
  "source": {"type": "string", "minLength": 1},
13
13
  "classification": {"enum": ["bug", "feature", "refactor", "investigation", "operations", "documentation", "review", "mixed"]},
14
14
  "risk": {"enum": ["low", "medium", "high", "critical"]},
15
15
  "route": {"type": "string", "pattern": "^specdev/[a-z0-9]+(?:-[a-z0-9]+)*$"},
16
16
  "ready_for_implementation": {"type": "boolean"},
17
17
  "external_action": {"enum": ["not-applicable", "pending-close", "closed", "close-failed", "waived"]},
18
+ "publish_action": {"enum": ["not-requested", "pending", "published", "publish-failed", "waived"]},
19
+ "publish": {"type": ["string", "null"]},
18
20
  "updated_at": {"type": "string", "minLength": 1}
19
21
  },
20
22
  "additionalProperties": true