signoff-cli 0.0.0-stage → 4.0.1

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 (57) hide show
  1. package/.pre-commit-hooks.yaml +27 -0
  2. package/CHANGELOG.md +344 -0
  3. package/LICENSE +21 -0
  4. package/README.md +540 -3
  5. package/action.yml +24 -0
  6. package/bin/signoff.js +218 -0
  7. package/docs/diagrams/README.md +41 -0
  8. package/docs/diagrams/signoff-1-capability-en.png +0 -0
  9. package/docs/diagrams/signoff-1-capability.png +0 -0
  10. package/docs/diagrams/signoff-2-market-en.png +0 -0
  11. package/docs/diagrams/signoff-2-market.png +0 -0
  12. package/docs/diagrams/signoff-3-user-en.png +0 -0
  13. package/docs/diagrams/signoff-3-user.png +0 -0
  14. package/docs/diagrams/signoff-4-flow-en.png +0 -0
  15. package/docs/diagrams/signoff-4-flow.png +0 -0
  16. package/docs/diagrams/signoff-5-competitors-en.png +0 -0
  17. package/docs/diagrams/signoff-5-competitors.png +0 -0
  18. package/docs/diagrams/src/signoff-1-capability-en.mmd +81 -0
  19. package/docs/diagrams/src/signoff-1-capability.mmd +84 -0
  20. package/docs/diagrams/src/signoff-2-market-en.mmd +72 -0
  21. package/docs/diagrams/src/signoff-2-market.mmd +72 -0
  22. package/docs/diagrams/src/signoff-3-user-en.mmd +94 -0
  23. package/docs/diagrams/src/signoff-3-user.mmd +92 -0
  24. package/docs/gates.en.md +105 -0
  25. package/docs/gates.md +88 -0
  26. package/docs/integrations.md +58 -0
  27. package/docs/promotion.md +71 -0
  28. package/docs/signoff.schema.json +108 -0
  29. package/docs/trust.md +72 -0
  30. package/gates/guard.py +474 -0
  31. package/gates/p1_verify.py +111 -0
  32. package/gates/p2_coverage.py +127 -0
  33. package/gates/p3_queue.py +186 -0
  34. package/gates/p4_ledger.py +249 -0
  35. package/gates/p5_slop.py +109 -0
  36. package/lib/audit.js +213 -0
  37. package/lib/bootstrap.js +361 -0
  38. package/lib/checkpoint.js +184 -0
  39. package/lib/ci.js +58 -0
  40. package/lib/core.js +120 -0
  41. package/lib/cov.js +253 -0
  42. package/lib/detect.js +169 -0
  43. package/lib/doctor.js +304 -0
  44. package/lib/editor.js +97 -0
  45. package/lib/explain.js +73 -0
  46. package/lib/guard.js +1229 -0
  47. package/lib/p1.js +469 -0
  48. package/lib/p2.js +323 -0
  49. package/lib/p3.js +633 -0
  50. package/lib/p4.js +733 -0
  51. package/lib/p5.js +438 -0
  52. package/lib/preset.js +66 -0
  53. package/lib/secrets.js +336 -0
  54. package/lib/sign.js +196 -0
  55. package/package.json +56 -6
  56. package/test/selftest.js +1227 -0
  57. package/test/selftest.py +190 -0
package/bin/signoff.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // SignOff (落印) CLI — dispatches to the five gates.
5
+ // The gates are pure Node (lib/p1..p5.js). SIGNOFF_ENGINE=python falls back to
6
+ // the reference implementation in gates/*.py, which is used only for parity tests.
7
+
8
+ const { spawnSync } = require("child_process");
9
+ const path = require("path");
10
+ const fs = require("fs");
11
+
12
+ // npm >= 11 gates postinstall behind an allowlist, so the install cannot be the only
13
+ // way in: the first gate command in a repo finishes the job itself. Read-only commands
14
+ // (doctor/status/explain/audit) and the hook path stay untouched - the hook path is what
15
+ // the machine-wide guard uses, and there a light P1 is the whole point.
16
+ const BOOTSTRAP_CMDS = new Set(["p1", "p2", "p3", "p4", "p5", "all", "guard", "secrets", "cov"]);
17
+ function maybeBootstrap(cmd) {
18
+ if (!BOOTSTRAP_CMDS.has(cmd)) return;
19
+ if (process.env.SIGNOFF_NO_BOOTSTRAP === "1") return;
20
+ try {
21
+ const r = require("../lib/bootstrap").ensureRepo({ quiet: true });
22
+ if (r.installed && !process.argv.includes("--json")) {
23
+ console.error(`signoff: guarded ${require("path").basename(r.root)} automatically (${r.changed.join(", ")})`);
24
+ }
25
+ } catch (e) {
26
+ /* a convenience, never a verdict */
27
+ }
28
+ }
29
+
30
+ const GATES = {
31
+ p1: "p1_verify.py",
32
+ p2: "p2_coverage.py",
33
+ p3: "p3_queue.py",
34
+ p4: "p4_ledger.py",
35
+ p5: "p5_slop.py",
36
+ };
37
+
38
+ const HELP = `SignOff / 落印 — make "done" provable
39
+
40
+ signoff p1 [--run CMD]... [--budget SEC] gate 1 verify before you speak
41
+ signoff p2 [--min-scenario N] [--md FILE] gate 2 coverage has a level, not a vibe
42
+ signoff p3 [--cards DIR] plan|collect ... gate 3 parallel work, no collisions
43
+ signoff p4 ask|confirm|verify|export ... gate 4 risky actions leave a ledger
44
+ signoff p4 export --merge OTHER... merge another repo's ledger into one table
45
+ signoff p4 export --format {md,json,csv} pick the export shape
46
+ signoff p5 [FILES...] gate 5 empty words are a failure
47
+
48
+ signoff p1 --install-hook drop a git pre-commit hook
49
+ signoff all p1, p2, p5, then p4 verify
50
+ signoff doctor [--fix] show what is present; --fix repairs what it can prove
51
+
52
+ signoff init [--no-ci] [--prepare] [--quiet] install hooks + CI so the gates run themselves
53
+ signoff init --share also write signoff.json, so every clone opts in
54
+ signoff init --chain | --lock-test | --reset-install keep existing hooks / pin the
55
+ detected runner / move the install point to HEAD
56
+ signoff ensure [--global] [--json] idempotent install; gates also self-install
57
+ (--global = guard every repo on this machine)
58
+ signoff guard [--full] run the gate set the hooks use (right now)
59
+ signoff status is this repo guarded?
60
+ signoff doctor [--json] [--fix] why is (or isn't) it guarded? --fix repairs it
61
+ signoff secrets [--staged|--all] [--json] scan staged files for leaked credentials
62
+ signoff cov [--diff] [--min 80] [--json] how much of this change the suite executed\n signoff init --preset node|python|go|java start from a curated gate config\n signoff init --editor vscode .vscode/tasks.json + problem matchers
63
+ signoff explain <p1..p5> what a gate checks and how it decides
64
+ signoff mcp serve the gates over stdio (MCP clients)
65
+ signoff audit [--range A..B] [--strict] did the commits that landed carry a trailer?
66
+ signoff audit --since-install only commits after the install point (refuses
67
+ a point that is not an ancestor of HEAD)
68
+ signoff uninstall [--keep-config] [--all] remove hooks + CI workflow
69
+
70
+ Requires Node >= 18 only; no Python. Options common to every gate: --json, -h.
71
+ `;
72
+
73
+ function python() {
74
+ return process.env.SIGNOFF_PYTHON || (process.platform === "win32" ? "python" : "python3");
75
+ }
76
+
77
+ function jsGate(name) {
78
+ const file = path.join(__dirname, "..", "lib", `${name}.js`);
79
+ return fs.existsSync(file) ? file : null;
80
+ }
81
+
82
+ function runGate(name, args) {
83
+ if (process.env.SIGNOFF_ENGINE !== "python" && jsGate(name)) {
84
+ try {
85
+ return require(jsGate(name)).run(args) || 0;
86
+ } catch (e) {
87
+ console.error(`signoff: ${name} failed: ${e && e.message}`);
88
+ return 1;
89
+ }
90
+ }
91
+ const file = path.join(__dirname, "..", "gates", GATES[name]);
92
+ if (!fs.existsSync(file)) {
93
+ console.error(`signoff: missing gate script ${file}`);
94
+ return 127;
95
+ }
96
+ const r = spawnSync(python(), [file, ...args], { stdio: "inherit" });
97
+ if (r.error) {
98
+ console.error(`signoff: cannot start ${python()} (${r.error.message}). Set SIGNOFF_PYTHON.`);
99
+ return 127;
100
+ }
101
+ return r.status === null ? 1 : r.status;
102
+ }
103
+
104
+ // Secrets run first in `all`: an unstaged key is worth more than a style report,
105
+ // and a non-git directory has nothing to scan (exit 2 from secrets is not a failure).
106
+ function runSecrets(args) {
107
+ try {
108
+ const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
109
+ cwd: process.cwd(),
110
+ encoding: "utf8",
111
+ windowsHide: true,
112
+ });
113
+ if (inside.status !== 0) return 0;
114
+ return require("../lib/secrets.js").run(["--json"]) || 0;
115
+ } catch (e) {
116
+ console.error(`signoff: secrets failed: ${e && e.message}`);
117
+ return 1;
118
+ }
119
+ }
120
+
121
+ function runAll(args) {
122
+ const steps = [
123
+ ["p1", args],
124
+ ["p2", args],
125
+ ["p5", []],
126
+ ["p4", ["verify"]],
127
+ // Coverage is opt-in on data: no coverage file means "not measured", not "failed".
128
+ ["cov", ["--allow-missing", "--diff"]],
129
+ ];
130
+ let code = 0;
131
+ console.log("\n== signoff secrets ==");
132
+ const sc = runSecrets(args);
133
+ if (sc !== 0) code = sc;
134
+ for (const [name, a] of steps) {
135
+ console.log(`\n== signoff ${name} ==`);
136
+ const c = runGate(name, a);
137
+ if (c !== 0) code = c;
138
+ }
139
+ return code;
140
+ }
141
+
142
+ function guard(sub, args) {
143
+ const file = path.join(__dirname, "..", "lib", "guard.js");
144
+ if (fs.existsSync(file)) {
145
+ try {
146
+ return require(file).main([sub, ...args]) || 0;
147
+ } catch (e) {
148
+ console.error(`signoff: guard failed: ${e && e.message}`);
149
+ return 1;
150
+ }
151
+ }
152
+ const py = path.join(__dirname, "..", "gates", "guard.py");
153
+ const r = spawnSync(python(), [py, sub, ...args], { stdio: "inherit" });
154
+ if (r.error) {
155
+ console.error(`signoff: cannot start ${python()} (${r.error.message}). Set SIGNOFF_PYTHON.`);
156
+ return 127;
157
+ }
158
+ return r.status === null ? 1 : r.status;
159
+ }
160
+
161
+ const [cmd, ...rest] = process.argv.slice(2);
162
+
163
+ if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
164
+ console.log(HELP);
165
+ process.exit(0);
166
+ }
167
+ if (cmd === "-v" || cmd === "--version" || cmd === "version") {
168
+ console.log(require("../package.json").version);
169
+ process.exit(0);
170
+ }
171
+ if (cmd === "doctor") process.exit(require("../lib/doctor").run(rest));
172
+ if (cmd === "ensure") process.exit(require("../lib/bootstrap").run(rest));
173
+ if (cmd === "all") {
174
+ maybeBootstrap("all");
175
+ process.exit(runAll(rest));
176
+ }
177
+ if (cmd === "init") process.exit(guard("init", rest));
178
+ if (cmd === "guard") {
179
+ maybeBootstrap("guard");
180
+ process.exit(guard("guard", rest));
181
+ }
182
+ if (cmd === "hook") process.exit(guard("hook", rest));
183
+ if (cmd === "status") process.exit(guard("status", rest));
184
+ if (cmd === "uninstall") {
185
+ if (rest.includes("--global")) {
186
+ const r = require("../lib/bootstrap").uninstallGlobal();
187
+ console.log(r.removed ? `signoff: machine layer removed (${r.dir})` : "signoff: machine layer was not installed");
188
+ process.exit(0);
189
+ }
190
+ process.exit(guard("uninstall", rest));
191
+ }
192
+ if (cmd === "audit") {
193
+ try {
194
+ process.exit(require(path.join(__dirname, "..", "lib", "audit.js")).run(rest) || 0);
195
+ } catch (e) {
196
+ console.error(`signoff: audit failed: ${e && e.message}`);
197
+ process.exit(1);
198
+ }
199
+ }
200
+ if (cmd === "explain") process.exit(require("../lib/explain.js").run(rest));
201
+ if (cmd === "cov") process.exit(require("../lib/cov.js").run(rest));
202
+ if (cmd === "secrets") {
203
+ maybeBootstrap("secrets");
204
+ process.exit(require("../lib/secrets.js").run(rest));
205
+ }
206
+ if (cmd === "mcp") {
207
+ // stdio JSON-RPC; the module wires stdin and stays alive until the client closes it.
208
+ require("../integrations/mcp/server.js");
209
+ return;
210
+ }
211
+ if (GATES[cmd]) {
212
+ maybeBootstrap(cmd);
213
+ process.exit(runGate(cmd, rest));
214
+ }
215
+
216
+ console.error(`signoff: unknown command "${cmd}"\n`);
217
+ console.error(HELP);
218
+ process.exit(2);
@@ -0,0 +1,41 @@
1
+ # SignOff 架构与生态图
2
+
3
+ 三张图全部由 **Mermaid** 生成(`mermaid-js/mermaid` + `mermaid-cli`),源文件在 `src/`,
4
+ 改文案只要改 `.mmd` 再渲染一次——不手画、不拼版、可 diff。
5
+
6
+ | 源文件 | 产物 | 回答的问题 |
7
+ | --- | --- | --- |
8
+ | `src/signoff-1-capability.mmd` | `signoff-1-capability.png` | 由哪些部分组成、判定发生在哪一层、什么会留在仓库里 |
9
+ | `src/signoff-2-market.mmd` | `signoff-2-market.png` | 已出现在哪些市场、哪些等审核、哪些物料备好未提交 |
10
+ | `src/signoff-3-user.mmd` | `signoff-3-user.png` | 用户怎么装怎么用,以及平台/环境差异与已知坑 |
11
+
12
+ ## 重新生成
13
+
14
+ ```powershell
15
+ $py = "$env:USERPROFILE\.openviking\venv\Scripts\python.exe"
16
+ & $py "$env:USERPROFILE\.dsh\tools\diagram.py" doctor # 检查 mmdc + Brave
17
+ & $py "$env:USERPROFILE\.dsh\tools\diagram.py" render src --out . --size 2200 --theme neutral
18
+ ```
19
+
20
+ 这台机器上的全局规矩(`~/.dsh/AGENTS.md`「框架图」):画图一律走这条链路。
21
+ 选 Mermaid 的理由是它是**可代码驱动**的图工具里星标最多的(91 028);
22
+ `excalidraw/excalidraw` 星更多(135 255)但它是画布应用,没有受支持的 headless CLI,结果不可复现。
23
+
24
+ ## 图 1 的读法
25
+
26
+ 从左到右:谁在调用 → 谁在判定 → 结果落在哪 → 依赖什么。要点:
27
+
28
+ - 四个入口(CLI、git 钩子、CI、MCP/技能)**共用同一个 `guard.js`**,结论不会互相矛盾;
29
+ - 五关并列,任一判红整体退出码就是 1,**没有“部分通过”**;
30
+ - 提交信息里的 `Signoff-Pass: <tree>` 是审计的唯一线索,`--no-verify` 会让它消失(审计里显示 `missing`)。
31
+
32
+ ## 图 2 的读法
33
+
34
+ 绿色=已上线可被搜索/安装;黄色=已提交等人工审核;灰色=物料已备未提交。
35
+ 底部那句话是关键:**Registry 只是索引,不是流量**;真正带来访问的是人工清单与被引用。
36
+
37
+ ## 图 3 的读法
38
+
39
+ 上半部分是正常路径(安装 → 初始化 → 日常 → 排错 → 退出/边界),下半部分三支是
40
+ 平台差异、环境差异、已知坑。排错那一支刻意写成链条:`doctor → doctor --fix → explain <gate> → 改配置`,
41
+ 改阈值永远放最后——90% 的“没反应”是钩子没生效,不是阈值。
Binary file
Binary file
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: SignOff capabilities and relations (v3.9.7)
3
+ ---
4
+ flowchart LR
5
+ subgraph L1["① Entry · who calls it"]
6
+ direction TB
7
+ A1["CLI<br/>run · p1..p5 · init · status · doctor<br/>audit · explain · mcp · uninstall<br/>--json · --strict · --dry-run · --chain"]
8
+ A2["Git hooks<br/>pre-commit runs the gates<br/>commit-msg checks the trailer<br/>pre-push checks the audit window<br/>existing hooks chained as .local"]
9
+ A3["CI · signoff.yml<br/>same gates, second run<br/>fetch-depth 0 required for audit"]
10
+ A4["Agents<br/>5 MCP tools · one skill<br/>isError mirrors the exit code"]
11
+ A1 ~~~ A2 ~~~ A3 ~~~ A4
12
+ end
13
+
14
+ subgraph L2["② Dispatcher · guard.js decides what runs and what counts as pass"]
15
+ direction TB
16
+ D1["Parse<br/>cwd normalised to repo root<br/>SIGNOFF_* env · piped runs filtered"]
17
+ D2["Lock<br/>.signoff/lock/ dir + owner.json<br/>atomic mkdir; dead pid or >10 min reclaimed<br/>wait cap SIGNOFF_LOCK_WAIT_MS=8s"]
18
+ D3["Select gates<br/>five by default<br/>name p1..p5; a hook runs only its stage"]
19
+ D4["Verdict<br/>0 all pass · 1 any gate red · 2 usage or env<br/>no partial pass; --json emits the result"]
20
+ D1 ~~~ D2 ~~~ D3 ~~~ D4
21
+ end
22
+
23
+ subgraph L3["③ Five gates · each answers: read → decide → fix"]
24
+ direction TB
25
+ P1["P1 Tests<br/>read: config.test_cmd, else auto-detect<br/>decide: test exit code ≠ 0<br/>fix: pin the command in config"]
26
+ P2["P2 Coverage<br/>read: scenario files, assertion count<br/>decide: below threshold, or no assertions<br/>fix: add assertions; --strict is stricter"]
27
+ P3["P3 Queue<br/>read: status in tasks/id/card.json<br/>decide: not done, or fields missing<br/>fix: finish the card's acceptance criteria"]
28
+ P4["P4 Ledger<br/>read: audit.log hash chain + rollback commit<br/>decide: chain broken, rollback gone, trailer missing<br/>fix: p4 ask/confirm, or roll back"]
29
+ P5["P5 Anti-slop<br/>read: docs added by this change<br/>decide: placeholders · claims without evidence<br/>fix: add evidence or delete it"]
30
+ P1 ~~~ P2 ~~~ P3 ~~~ P4 ~~~ P5
31
+ end
32
+
33
+ subgraph L4["④ State machine · what happens inside one commit"]
34
+ direction TB
35
+ S1["pre-commit<br/>all five pass → marker {tree, mode} written"]
36
+ S2["commit-msg<br/>marker.tree == HEAD tree ?<br/>yes → append the Signoff-Pass trailer"]
37
+ S3["pre-push<br/>every commit in the audit window checked<br/>missing trailer = red"]
38
+ S4["CI<br/>gates + audit again<br/>no git identity means no fake pass"]
39
+ S1 ~~~ S2 ~~~ S3 ~~~ S4
40
+ end
41
+
42
+ subgraph L5["⑤ Artifacts · judgment leaves evidence"]
43
+ direction TB
44
+ F1["passed<br/>tree + mode(pass|skip) + ts<br/>invalidated when tree changes"]
45
+ F2["audit.log<br/>append-only hash chain: prevHash · action · rollback"]
46
+ F3["installed.json<br/>install-point sha / time / version<br/>left edge of the audit window"]
47
+ F4["commit message<br/>Signoff-Pass: tree<br/>--no-verify is the only hole"]
48
+ F5["config.json<br/>thresholds · test command · P5 word list"]
49
+ F1 ~~~ F2 ~~~ F3 ~~~ F4 ~~~ F5
50
+ end
51
+
52
+ subgraph L6["⑥ External dependencies · only two are hard requirements"]
53
+ direction TB
54
+ X1["Node ≥ 18<br/>pure JS · no build step · no native modules"]
55
+ X2["git<br/>hook location · history · ancestor checks"]
56
+ X3["Test runner<br/>whatever the repo uses; falls back when absent"]
57
+ X4["Python · peer tests only<br/>never invoked at runtime"]
58
+ X5["GitHub Actions<br/>optional, --no-ci skips it"]
59
+ X1 ~~~ X2 ~~~ X3 ~~~ X4 ~~~ X5
60
+ end
61
+
62
+ L1 -->|calls| L2
63
+ L2 -->|picks gates per stage| L3
64
+ L3 -->|writes marker / trailer| L4
65
+ L4 -->|persists| L5
66
+ L5 -->|audit comparison| L6
67
+
68
+
69
+ classDef entry fill:#eef3fb,stroke:#2f6fdd,stroke-width:1.4px,color:#0f2d5c
70
+ classDef sched fill:#fdf3e7,stroke:#b45309,stroke-width:1.4px,color:#5c2c05
71
+ classDef gate fill:#e8f7f0,stroke:#0e9f6e,stroke-width:1.4px,color:#0b4a33
72
+ classDef state fill:#eef2ff,stroke:#4f46e5,stroke-width:1.4px,color:#312e81
73
+ classDef data fill:#f3ecfd,stroke:#7c3aed,stroke-width:1.4px,color:#3d1e6b
74
+ classDef ext fill:#f4f5f7,stroke:#6b7280,stroke-width:1.2px,color:#374151
75
+ classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
76
+ class A1,A2,A3,A4 entry
77
+ class D1,D2,D3,D4 sched
78
+ class P1,P2,P3,P4,P5 gate
79
+ class S1,S2,S3,S4 state
80
+ class F1,F2,F3,F4,F5 data
81
+ class X1,X2,X3,X4,X5 ext
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: SignOff 完整能力与关系(v3.9.7)
3
+ ---
4
+ flowchart LR
5
+ subgraph L1["① 入口层 · 谁在调用它"]
6
+ direction TB
7
+ A1["CLI<br/>run · p1..p5 · init · status · doctor<br/>audit · explain · mcp · uninstall<br/>--json · --strict · --dry-run · --chain"]
8
+ A2["Git 钩子<br/>pre-commit 跑闸门<br/>commit-msg 查 trailer<br/>pre-push 查审计窗口<br/>已有钩子链式为 .local"]
9
+ A3["CI · signoff.yml<br/>同一套闸门再跑一遍<br/>fetch-depth 0 才能审计"]
10
+ A4["Agent 面<br/>MCP 五工具 · 技能<br/>isError 跟随退出码"]
11
+ A1 ~~~ A2 ~~~ A3 ~~~ A4
12
+ end
13
+
14
+ subgraph L2["② 调度层 · guard.js 决定跑什么、谁算通过"]
15
+ direction TB
16
+ D1["解析<br/>cwd 归一到仓库根<br/>SIGNOFF_* 环境 · 管道点 filtered"]
17
+ D2["取锁<br/>.signoff/lock/ 目录 + owner.json<br/>mkdir 原子;死进程或超 10 分钟回收<br/>等待上限 SIGNOFF_LOCK_WAIT_MS=8s"]
18
+ D3["选关<br/>默认五关全跑<br/>可点名 p1..p5;钩子只跑本阶段该跑的"]
19
+ D4["定判决<br/>0 全过 / 1 任一关红 / 2 用法或环境错<br/>--json 输出结构化结果"]
20
+ D1 ~~~ D2 ~~~ D3 ~~~ D4
21
+ end
22
+
23
+ subgraph L3["③ 五关 · 每关都回答:读什么 → 判什么 → 怎么修"]
24
+ direction TB
25
+ P1["P1 测试<br/>读:config.test_cmd,否则自动识别<br/>判:测试命令退出码 ≠ 0<br/>修:把命令固化进 config"]
26
+ P2["P2 覆盖<br/>读:场景文件数与断言数<br/>判:低于阈值,或声称成功却无断言<br/>修:补断言;--strict 更严"]
27
+ P3["P3 队列<br/>读:tasks/id/card.json 的 status<br/>判:不是 done,或字段缺失<br/>修:把卡片验收条件做完"]
28
+ P4["P4 账本<br/>读:audit.log 哈希链 + 回滚点提交<br/>判:链断、回滚点消失、审计缺 trailer<br/>修:p4 ask/confirm 补记或回滚"]
29
+ P5["P5 反空话<br/>读:本次新增的文档<br/>判:占位符(词边界)· 无证据结论 · 空话词<br/>修:给证据或删句子"]
30
+ P1 ~~~ P2 ~~~ P3 ~~~ P4 ~~~ P5
31
+ end
32
+
33
+ subgraph L4["④ 状态机 · 一次提交里发生了什么"]
34
+ direction TB
35
+ S1["pre-commit<br/>五关全过 → 写 marker {tree, mode}"]
36
+ S2["commit-msg<br/>marker.tree == HEAD tree ?<br/>是 → 追加 Signoff-Pass trailer"]
37
+ S3["pre-push<br/>审计窗口内提交逐个查<br/>缺 trailer 即红"]
38
+ S4["CI<br/>再跑五关 + 审计<br/>无 git 身份时不冒充通过"]
39
+ S1 ~~~ S2 ~~~ S3 ~~~ S4
40
+ end
41
+
42
+ subgraph L5["⑤ 产物与记录 · 判断留下证据"]
43
+ direction TB
44
+ F1["passed<br/>tree + mode(pass|skip) + ts<br/>tree 变了自动失效"]
45
+ F2["audit.log<br/>只追加哈希链:prevHash · action · rollback"]
46
+ F3["installed.json<br/>安装点 sha / 时间 / 版本<br/>审计窗口左边界"]
47
+ F4["提交信息<br/>Signoff-Pass: tree<br/>--no-verify 的唯一破绽"]
48
+ F5["config.json<br/>阈值 · 测试命令 · P5 词表"]
49
+ F1 ~~~ F2 ~~~ F3 ~~~ F4 ~~~ F5
50
+ end
51
+
52
+ subgraph L6["⑥ 外部依赖 · 只有两样是硬要求"]
53
+ direction TB
54
+ X1["Node ≥ 18<br/>纯 JS · 无构建 · 无原生模块"]
55
+ X2["git<br/>钩子位置 · 提交历史 · 祖先校验"]
56
+ X3["测试运行器<br/>仓库自己那一套;缺失则退化后备命令"]
57
+ X4["Python · 仅对等测试<br/>运行时永不调用"]
58
+ X5["GitHub Actions<br/>可选,--no-ci 跳过"]
59
+ X1 ~~~ X2 ~~~ X3 ~~~ X4 ~~~ X5
60
+ end
61
+
62
+ L1 -->|调用| L2
63
+ L2 -->|按阶段选关| L3
64
+ L3 -->|写 marker / trailer| L4
65
+ L4 -->|落盘| L5
66
+ L5 -->|审计比对| L6
67
+
68
+ N["退出码即判决:0 通过 · 1 判红 · 2 用法与环境<br/>没有“部分通过”这回事"]
69
+ L2 -.-> N
70
+
71
+ classDef entry fill:#eef3fb,stroke:#2f6fdd,stroke-width:1.4px,color:#0f2d5c
72
+ classDef sched fill:#fdf3e7,stroke:#b45309,stroke-width:1.4px,color:#5c2c05
73
+ classDef gate fill:#e8f7f0,stroke:#0e9f6e,stroke-width:1.4px,color:#0b4a33
74
+ classDef state fill:#eef2ff,stroke:#4f46e5,stroke-width:1.4px,color:#312e81
75
+ classDef data fill:#f3ecfd,stroke:#7c3aed,stroke-width:1.4px,color:#3d1e6b
76
+ classDef ext fill:#f4f5f7,stroke:#6b7280,stroke-width:1.2px,color:#374151
77
+ classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
78
+ class A1,A2,A3,A4 entry
79
+ class D1,D2,D3,D4 sched
80
+ class P1,P2,P3,P4,P5 gate
81
+ class S1,S2,S3,S4 state
82
+ class F1,F2,F3,F4,F5 data
83
+ class X1,X2,X3,X4,X5 ext
84
+ class N note
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: SignOff publishing and market distribution (2026-11-07)
3
+ ---
4
+ flowchart LR
5
+ subgraph SRC["⓪ Single source of truth · everything else is a projection"]
6
+ direction TB
7
+ R1["GitHub master<br/>source · hook templates · CI · docs"]
8
+ R2["npm tarball dsh-signoff@3.9.7<br/>package.json carries mcpName"]
9
+ R3["server.json + smithery.yaml<br/>raw material for listings"]
10
+ R4["docs/diagrams/src/*.mmd<br/>the diagrams are sources too"]
11
+ R1 ~~~ R2 ~~~ R3 ~~~ R4
12
+ end
13
+
14
+ subgraph PUB["① Publish channels · where content is actually downloadable"]
15
+ direction TB
16
+ C1["npm registry<br/>dsh-signoff · latest 3.9.7<br/>the only install path"]
17
+ C2["GitHub Releases<br/>v3.9.7 bilingual notes + tgz"]
18
+ C3["Official MCP Registry<br/>io.github.ayuanxx-max/signoff@3.9.7<br/>mcpName verified"]
19
+ C4["OpenViking docs<br/>viking://resources/dsh/signoff-*"]
20
+ C1 ~~~ C2 ~~~ C3 ~~~ C4
21
+ end
22
+
23
+ subgraph IDX["② Indexes and lists · whether anyone can find you"]
24
+ direction TB
25
+ subgraph IDXP["submitted / happens automatically"]
26
+ direction TB
27
+ I1["awesome-mcp-servers · PR #18813<br/>Developer Tools · human review 1–3 days"]
28
+ I2["Glama · auto-indexes the repo and scores it<br/>the listing carries its badge"]
29
+ I1 ~~~ I2
30
+ end
31
+ subgraph IDXR["not submitted yet · material ready"]
32
+ direction TB
33
+ I3["Smithery · smithery.yaml ready<br/>needs GitHub OAuth on the web"]
34
+ I4["mcp.so · PulseMCP<br/>mostly pull the official registry, usually automatic"]
35
+ I5["ClawHub skills market<br/>skill written, not published"]
36
+ I6["other awesome-* lists<br/>git-hooks / pre-commit / ai-agents"]
37
+ I3 ~~~ I4 ~~~ I5 ~~~ I6
38
+ end
39
+ end
40
+
41
+ subgraph SEE["③ Discovery · where users actually come from"]
42
+ direction TB
43
+ V1["GitHub search and topics<br/>11 topics set"]
44
+ V2["click from a curated list"]
45
+ V3["word of mouth / posts / comparisons"]
46
+ V4["npm search: dsh-signoff"]
47
+ V1 ~~~ V2 ~~~ V3 ~~~ V4
48
+ end
49
+
50
+ N1["Key distinction: registry and lists are indexes, they generate no traffic<br/>traffic comes from curated lists, citations and search hits"]
51
+ N2["Progress: channels 4/4 done<br/>indexes = 1 in review + 1 automatic + 1 submittable + 1 synced + 2 not submitted"]
52
+
53
+ SRC -->|publish| PUB
54
+ PUB -->|register as discoverable| IDX
55
+ IDX -->|someone sees it| SEE
56
+ C3 -.->|downstream sync| I4
57
+ C2 -.->|auto-indexed| I2
58
+ IDX -.-> N1
59
+ IDX -.-> N2
60
+
61
+ classDef src fill:#0f172a,stroke:#0f172a,color:#ffffff
62
+ classDef pub fill:#dcfce7,stroke:#0e9f6e,stroke-width:1.4px,color:#14532d
63
+ classDef pending fill:#fef3c7,stroke:#d97706,stroke-width:1.4px,color:#7c2d12
64
+ classDef todo fill:#f1f5f9,stroke:#94a3b8,stroke-width:1.2px,color:#334155
65
+ classDef see fill:#eef2ff,stroke:#4f46e5,stroke-width:1.3px,color:#312e81
66
+ classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
67
+ class R1,R2,R3,R4 src
68
+ class C1,C2,C3,C4 pub
69
+ class I1,I2 pending
70
+ class I3,I4,I5,I6 todo
71
+ class V1,V2,V3,V4 see
72
+ class N1,N2 note
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: SignOff 发布与市场分布(2026-11-07)
3
+ ---
4
+ flowchart LR
5
+ subgraph SRC["⓪ 唯一事实源 · 其余都是它的投影"]
6
+ direction TB
7
+ R1["GitHub 仓库 master<br/>源码 · 钩子模板 · CI · 文档"]
8
+ R2["npm tarball dsh-signoff@3.9.4<br/>package.json 带 mcpName"]
9
+ R3["server.json + smithery.yaml<br/>市场清单的原料"]
10
+ R4["docs/diagrams/src/*.mmd<br/>图也是源文件"]
11
+ R1 ~~~ R2 ~~~ R3 ~~~ R4
12
+ end
13
+
14
+ subgraph PUB["① 发布渠道 · 内容在这里真正可下载"]
15
+ direction TB
16
+ C1["npm registry<br/>dsh-signoff · latest 3.9.4<br/>唯一安装入口"]
17
+ C2["GitHub Releases<br/>v3.9.4 中英双语 + tgz"]
18
+ C3["官方 MCP Registry<br/>io.github.ayuanxx-max/signoff@3.9.4<br/>已过 mcpName 校验"]
19
+ C4["OpenViking 文档库<br/>viking://resources/dsh/signoff-*"]
20
+ C1 ~~~ C2 ~~~ C3 ~~~ C4
21
+ end
22
+
23
+ subgraph IDX["② 索引与目录 · 决定别人能不能找到你"]
24
+ direction TB
25
+ subgraph IDXP["已提交 / 会自动发生"]
26
+ direction TB
27
+ I1["awesome-mcp-servers · PR #18813<br/>Developer Tools 分类 · 人工审核 1–3 天"]
28
+ I2["Glama · 按仓库自动建索引并出分<br/>列表条目自带它的徽章"]
29
+ I1 ~~~ I2
30
+ end
31
+ subgraph IDXR["还没提交 · 物料已备"]
32
+ direction TB
33
+ I3["Smithery · smithery.yaml 已就绪<br/>需 GitHub OAuth 在网页提交"]
34
+ I4["mcp.so · PulseMCP<br/>多拉官方 registry,通常自动收录"]
35
+ I5["ClawHub 技能市场<br/>技能已写好,未发布"]
36
+ I6["其余 awesome-* 清单<br/>git-hooks / pre-commit / ai-agents"]
37
+ I3 ~~~ I4 ~~~ I5 ~~~ I6
38
+ end
39
+ end
40
+
41
+ subgraph SEE["③ 发现入口 · 用户实际从哪里进来"]
42
+ direction TB
43
+ V1["GitHub 搜索与 topics<br/>11 个 topic 已配"]
44
+ V2["清单页点击"]
45
+ V3["别人转述 / 文章 / 对比表"]
46
+ V4["npm 站内搜索 dsh-signoff"]
47
+ V1 ~~~ V2 ~~~ V3 ~~~ V4
48
+ end
49
+
50
+ N1["关键区分:Registry 与目录是索引,不产生流量<br/>流量来自人工清单、被引用、被搜索命中"]
51
+ N2["进度:发布渠道 4/4 完成<br/>索引 = 1 待审 + 1 自动 + 1 可提交 + 1 同步 + 2 未提交"]
52
+
53
+ SRC -->|发布| PUB
54
+ PUB -->|登记为可发现| IDX
55
+ IDX -->|被人看到| SEE
56
+ C3 -.->|下游同步| I4
57
+ C2 -.->|自动建索引| I2
58
+ IDX -.-> N1
59
+ IDX -.-> N2
60
+
61
+ classDef src fill:#0f172a,stroke:#0f172a,color:#ffffff
62
+ classDef pub fill:#dcfce7,stroke:#0e9f6e,stroke-width:1.4px,color:#14532d
63
+ classDef pending fill:#fef3c7,stroke:#d97706,stroke-width:1.4px,color:#7c2d12
64
+ classDef todo fill:#f1f5f9,stroke:#94a3b8,stroke-width:1.2px,color:#334155
65
+ classDef see fill:#eef2ff,stroke:#4f46e5,stroke-width:1.3px,color:#312e81
66
+ classDef note fill:#ffffff,stroke:#0f172a,stroke-dasharray:4 3,color:#0f172a
67
+ class R1,R2,R3,R4 src
68
+ class C1,C2,C3,C4 pub
69
+ class I1,I2 pending
70
+ class I3,I4,I5,I6 todo
71
+ class V1,V2,V3,V4 see
72
+ class N1,N2 note