signoff-cli 0.0.0-stage → 4.0.0
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/.pre-commit-hooks.yaml +27 -0
- package/CHANGELOG.md +336 -0
- package/LICENSE +21 -0
- package/README.md +540 -3
- package/action.yml +24 -0
- package/bin/signoff.js +218 -0
- package/docs/diagrams/README.md +41 -0
- package/docs/diagrams/signoff-1-capability-en.png +0 -0
- package/docs/diagrams/signoff-1-capability.png +0 -0
- package/docs/diagrams/signoff-2-market-en.png +0 -0
- package/docs/diagrams/signoff-2-market.png +0 -0
- package/docs/diagrams/signoff-3-user-en.png +0 -0
- package/docs/diagrams/signoff-3-user.png +0 -0
- package/docs/diagrams/signoff-4-flow-en.png +0 -0
- package/docs/diagrams/signoff-4-flow.png +0 -0
- package/docs/diagrams/signoff-5-competitors-en.png +0 -0
- package/docs/diagrams/signoff-5-competitors.png +0 -0
- package/docs/diagrams/src/signoff-1-capability-en.mmd +81 -0
- package/docs/diagrams/src/signoff-1-capability.mmd +84 -0
- package/docs/diagrams/src/signoff-2-market-en.mmd +72 -0
- package/docs/diagrams/src/signoff-2-market.mmd +72 -0
- package/docs/diagrams/src/signoff-3-user-en.mmd +94 -0
- package/docs/diagrams/src/signoff-3-user.mmd +92 -0
- package/docs/gates.en.md +105 -0
- package/docs/gates.md +88 -0
- package/docs/integrations.md +58 -0
- package/docs/promotion.md +71 -0
- package/docs/signoff.schema.json +108 -0
- package/docs/trust.md +72 -0
- package/gates/guard.py +474 -0
- package/gates/p1_verify.py +111 -0
- package/gates/p2_coverage.py +127 -0
- package/gates/p3_queue.py +186 -0
- package/gates/p4_ledger.py +249 -0
- package/gates/p5_slop.py +109 -0
- package/lib/audit.js +213 -0
- package/lib/bootstrap.js +361 -0
- package/lib/checkpoint.js +184 -0
- package/lib/ci.js +58 -0
- package/lib/core.js +120 -0
- package/lib/cov.js +253 -0
- package/lib/detect.js +169 -0
- package/lib/doctor.js +304 -0
- package/lib/editor.js +97 -0
- package/lib/explain.js +73 -0
- package/lib/guard.js +1229 -0
- package/lib/p1.js +469 -0
- package/lib/p2.js +323 -0
- package/lib/p3.js +633 -0
- package/lib/p4.js +733 -0
- package/lib/p5.js +438 -0
- package/lib/preset.js +66 -0
- package/lib/secrets.js +336 -0
- package/lib/sign.js +196 -0
- package/package.json +55 -6
- package/test/selftest.js +1226 -0
- package/test/selftest.py +190 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "User path: install, use, troubleshoot (0 → 5 is the order)"
|
|
3
|
+
---
|
|
4
|
+
flowchart TB
|
|
5
|
+
subgraph ROW1["User path · strictly 0 → 5; red = must do, amber = the places people trip on"]
|
|
6
|
+
direction LR
|
|
7
|
+
subgraph T0["0 · Precheck"]
|
|
8
|
+
direction TB
|
|
9
|
+
T0a["node -v ≥ 18<br/>git --version works"]
|
|
10
|
+
T0b["run at the repo root<br/>(where .git lives)"]
|
|
11
|
+
T0c["Windows / macOS / Linux all fine"]
|
|
12
|
+
end
|
|
13
|
+
subgraph T1["1 · Install"]
|
|
14
|
+
direction TB
|
|
15
|
+
T1a["npm i -g dsh-signoff"]
|
|
16
|
+
T1b["no global install: npx dsh-signoff p1 once"]
|
|
17
|
+
T1c["behind a corporate proxy set npm proxy<br/>or npx cannot fetch the package"]
|
|
18
|
+
end
|
|
19
|
+
subgraph T2["2 · Init"]
|
|
20
|
+
direction TB
|
|
21
|
+
T2a["signoff init"]
|
|
22
|
+
T2b["repo already has husky hooks: add --chain<br/>otherwise yours get replaced"]
|
|
23
|
+
T2c["test command differs per machine: --lock-test"]
|
|
24
|
+
T2d["see what it will write: --dry-run"]
|
|
25
|
+
T2e["leave CI alone: --no-ci"]
|
|
26
|
+
end
|
|
27
|
+
subgraph T3["3 · Daily commits"]
|
|
28
|
+
direction TB
|
|
29
|
+
T3a["git commit<br/>pre-commit runs the five gates"]
|
|
30
|
+
T3b["marker and trailer written only when all five pass"]
|
|
31
|
+
T3c["status: what got installed<br/>doctor: why it does nothing"]
|
|
32
|
+
T3d["explain p2: how this gate decides"]
|
|
33
|
+
T3e["audit --since-install: who really passed"]
|
|
34
|
+
end
|
|
35
|
+
subgraph T4["4 · Red? check in this order"]
|
|
36
|
+
direction TB
|
|
37
|
+
T4a["doctor: are the hooks running at all"]
|
|
38
|
+
T4b["not running → init --chain"]
|
|
39
|
+
T4c["tests really fail → fix code, not thresholds"]
|
|
40
|
+
T4d["coverage short → add assertions, or --strict for the stricter verdict"]
|
|
41
|
+
T4e["queue incomplete → finish the card"]
|
|
42
|
+
T4f["ledger broken / trailer missing → p4 ask/confirm or roll back"]
|
|
43
|
+
T4g["P5 false positive → rewrite the wording, not the word list"]
|
|
44
|
+
end
|
|
45
|
+
subgraph T5["5 · Upgrade and uninstall"]
|
|
46
|
+
direction TB
|
|
47
|
+
T5a["upgrade: npm i -g dsh-signoff@latest<br/>then signoff init to refresh the hooks"]
|
|
48
|
+
T5b["uninstall default: remove hooks, keep config"]
|
|
49
|
+
T5c["--keep-config / --all: two more thorough levels"]
|
|
50
|
+
end
|
|
51
|
+
T0 --> T1 --> T2 --> T3 --> T4 --> T5
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
subgraph ROW2["The three real-world dimensions · they do not change your flow, they change what you see"]
|
|
55
|
+
direction LR
|
|
56
|
+
subgraph PL["Platform differences"]
|
|
57
|
+
direction TB
|
|
58
|
+
PL1["Windows: hooks launch through .ps1 / .cmd<br/>exec bit irrelevant; a GBK console mangles Chinese output"]
|
|
59
|
+
PL2["macOS / Linux: hooks need the exec bit<br/>init sets it, doctor --fix repairs it"]
|
|
60
|
+
PL3["containers / CI: often no user.name or user.email<br/>the commit fails and it is unrelated to the gates"]
|
|
61
|
+
PL4["SMB / NFS: lock atomicity cannot be proven<br/>mkdir beats O_EXCL, still best effort"]
|
|
62
|
+
end
|
|
63
|
+
subgraph EN["Environment differences"]
|
|
64
|
+
direction TB
|
|
65
|
+
EN1["Node 18 / 20 / 22 / 26 all tested in CI"]
|
|
66
|
+
EN2["shallow clone: audit baseline unreachable → red<br/>CI needs fetch-depth 0"]
|
|
67
|
+
EN3["monorepo: the repo root is the unit<br/>per-package init creates two hook sets"]
|
|
68
|
+
EN4["repo without tests: reports 'no test command detected'<br/>it never fakes a pass"]
|
|
69
|
+
EN5["works offline: the gates never touch the network<br/>only npx needs it"]
|
|
70
|
+
end
|
|
71
|
+
subgraph RED["Red lines · do not"]
|
|
72
|
+
direction TB
|
|
73
|
+
RD1["--no-verify → the audit shows missing"]
|
|
74
|
+
RD2["SIGNOFF_SKIP=1 → the marker records skip"]
|
|
75
|
+
RD3["loosening thresholds to turn red green = deleting the evidence"]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
PL ~~~ EN ~~~ RED
|
|
80
|
+
|
|
81
|
+
ROW1 -->|same verdicts, different symptoms| ROW2
|
|
82
|
+
|
|
83
|
+
classDef stage fill:#eef3fb,stroke:#2f6fdd,stroke-width:1.6px,color:#0f2d5c
|
|
84
|
+
classDef must fill:#fee2e2,stroke:#dc2626,stroke-width:1.5px,color:#7f1d1d
|
|
85
|
+
classDef step fill:#fbfcfe,stroke:#9aa4b2,stroke-width:1.1px,color:#1f2937
|
|
86
|
+
classDef warn fill:#fef3c7,stroke:#d97706,stroke-width:1.2px,color:#7c2d12
|
|
87
|
+
classDef env fill:#eef2ff,stroke:#4f46e5,stroke-width:1.1px,color:#312e81
|
|
88
|
+
classDef bad fill:#fee2e2,stroke:#b91c1c,stroke-width:1.3px,color:#7f1d1d
|
|
89
|
+
class T0a,T1a,T2a,T3a,T3b stage
|
|
90
|
+
class T1c,T2b,T2c,T4a,T4b,T4c must
|
|
91
|
+
class T0b,T0c,T1b,T2d,T2e,T3c,T3d,T3e,T4d,T4e,T4f,T4g,T5a,T5b,T5c step
|
|
92
|
+
class PL1,PL2,PL3,PL4 warn
|
|
93
|
+
class EN1,EN2,EN3,EN4,EN5 env
|
|
94
|
+
class RD1,RD2,RD3 bad
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 用户路径:安装、使用、排错(0→5 是先后顺序)
|
|
3
|
+
---
|
|
4
|
+
flowchart TB
|
|
5
|
+
subgraph ROW1["用户路径 · 严格按 0 → 5 的先后;红色是必做,黄色是你容易踩的"]
|
|
6
|
+
direction LR
|
|
7
|
+
subgraph T0["0 · 前置检查"]
|
|
8
|
+
direction TB
|
|
9
|
+
T0a["node -v ≥ 18<br/>git --version 可用"]
|
|
10
|
+
T0b["在仓库根执行<br/>(.git 所在目录)"]
|
|
11
|
+
T0c["Windows / macOS / Linux 都行"]
|
|
12
|
+
end
|
|
13
|
+
subgraph T1["1 · 安装"]
|
|
14
|
+
direction TB
|
|
15
|
+
T1a["npm i -g dsh-signoff"]
|
|
16
|
+
T1b["不想全局装:npx dsh-signoff p1 试一次"]
|
|
17
|
+
T1c["公司网络要先配 npm proxy<br/>否则 npx 取不到包"]
|
|
18
|
+
end
|
|
19
|
+
subgraph T2["2 · 初始化"]
|
|
20
|
+
direction TB
|
|
21
|
+
T2a["signoff init"]
|
|
22
|
+
T2b["仓库已有 husky 等钩子:加 --chain<br/>否则你的钩子会被顶掉"]
|
|
23
|
+
T2c["测试命令因机而异:加 --lock-test"]
|
|
24
|
+
T2d["先看会写什么:--dry-run"]
|
|
25
|
+
T2e["不想动 CI:--no-ci"]
|
|
26
|
+
end
|
|
27
|
+
subgraph T3["3 · 日常提交"]
|
|
28
|
+
direction TB
|
|
29
|
+
T3a["git commit<br/>pre-commit 自动跑五关"]
|
|
30
|
+
T3b["五关全过才写 marker 与 trailer"]
|
|
31
|
+
T3c["status:装了什么<br/>doctor:为什么没生效"]
|
|
32
|
+
T3d["explain p2:这一关到底怎么判"]
|
|
33
|
+
T3e["audit --since-install:谁真的过了"]
|
|
34
|
+
end
|
|
35
|
+
subgraph T4["4 · 红了怎么办(按这个顺序查)"]
|
|
36
|
+
direction TB
|
|
37
|
+
T4a["doctor:钩子真的在跑吗"]
|
|
38
|
+
T4b["没在跑 → init --chain"]
|
|
39
|
+
T4c["测试真失败 → 修代码,别调阈值"]
|
|
40
|
+
T4d["覆盖不足 → 补断言,或 --strict 看更严结论"]
|
|
41
|
+
T4e["队列未完成 → 把卡片做完"]
|
|
42
|
+
T4f["账本断链 / 缺 trailer → p4 ask/confirm 或回滚"]
|
|
43
|
+
T4g["P5 误报 → 改写措辞,不改词表"]
|
|
44
|
+
end
|
|
45
|
+
subgraph T5["5 · 升级与卸载"]
|
|
46
|
+
direction TB
|
|
47
|
+
T5a["升级:npm i -g dsh-signoff@latest<br/>再跑 signoff init 刷新钩子"]
|
|
48
|
+
T5b["uninstall 默认:摘钩子、留配置"]
|
|
49
|
+
T5c["--keep-config / --all:更彻底两档"]
|
|
50
|
+
end
|
|
51
|
+
T0 --> T1 --> T2 --> T3 --> T4 --> T5
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
subgraph ROW2["现实世界的三个维度 · 不改你的流程,但会改变你看到的现象"]
|
|
55
|
+
direction LR
|
|
56
|
+
subgraph PL["平台差异"]
|
|
57
|
+
direction TB
|
|
58
|
+
PL1["Windows:钩子用 .ps1 / .cmd 启动器<br/>执行位不生效;控制台 GBK 会把中文显示成乱码"]
|
|
59
|
+
PL2["macOS / Linux:钩子必须有执行位<br/>init 会设,doctor --fix 能补"]
|
|
60
|
+
PL3["容器 / CI:常无 user.name、user.email<br/>提交失败与闸门无关"]
|
|
61
|
+
PL4["SMB / NFS:锁的原子性无法被证明<br/>mkdir 比 O_EXCL 可靠,仍属尽力而为"]
|
|
62
|
+
end
|
|
63
|
+
subgraph EN["环境差异"]
|
|
64
|
+
direction TB
|
|
65
|
+
EN1["Node 18 / 20 / 22 / 26 都在 CI 测过"]
|
|
66
|
+
EN2["浅克隆:审计基线不可达 → 判红<br/>CI 必须 fetch-depth 0"]
|
|
67
|
+
EN3["monorepo:以仓库根为准<br/>子包各自 init 会出现两套钩子"]
|
|
68
|
+
EN4["无测试仓库:如实报“未识别到测试命令”<br/>不假装通过"]
|
|
69
|
+
EN5["离线可用:闸门不联网,只有 npx 取包要网"]
|
|
70
|
+
end
|
|
71
|
+
subgraph RED["红线 · 不要碰"]
|
|
72
|
+
direction TB
|
|
73
|
+
RD1["--no-verify 绕过 → 审计显示 missing"]
|
|
74
|
+
RD2["SIGNOFF_SKIP=1 → marker 记 skip,留痕"]
|
|
75
|
+
RD3["改阈值让红变绿 = 把证据删掉"]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
ROW1 -->|同一套判定,换环境会换现象| ROW2
|
|
80
|
+
|
|
81
|
+
classDef stage fill:#eef3fb,stroke:#2f6fdd,stroke-width:1.6px,color:#0f2d5c
|
|
82
|
+
classDef must fill:#fee2e2,stroke:#dc2626,stroke-width:1.5px,color:#7f1d1d
|
|
83
|
+
classDef step fill:#fbfcfe,stroke:#9aa4b2,stroke-width:1.1px,color:#1f2937
|
|
84
|
+
classDef warn fill:#fef3c7,stroke:#d97706,stroke-width:1.2px,color:#7c2d12
|
|
85
|
+
classDef env fill:#eef2ff,stroke:#4f46e5,stroke-width:1.1px,color:#312e81
|
|
86
|
+
classDef bad fill:#fee2e2,stroke:#b91c1c,stroke-width:1.3px,color:#7f1d1d
|
|
87
|
+
class T0a,T1a,T2a,T3a,T3b stage
|
|
88
|
+
class T1c,T2b,T2c,T4a,T4b,T4c must
|
|
89
|
+
class T0b,T0c,T1b,T2d,T2e,T3c,T3d,T3e,T4d,T4e,T4f,T4g,T5a,T5b,T5c step
|
|
90
|
+
class PL1,PL2,PL3,PL4 warn
|
|
91
|
+
class EN1,EN2,EN3,EN4,EN5 env
|
|
92
|
+
class RD1,RD2,RD3 bad
|
package/docs/gates.en.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Gates — SignOff
|
|
2
|
+
|
|
3
|
+
> 中文: [gates.md](gates.md)
|
|
4
|
+
|
|
5
|
+
One command per gate. Each writes `.signoff/pN.json`; exit 0 passes, 1 fails, 2 is a usage or precondition problem.
|
|
6
|
+
|
|
7
|
+
## P1 `gates/p1_verify.py`
|
|
8
|
+
|
|
9
|
+
| flag | meaning |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `--run CMD` (repeatable) | command to run; when omitted, `signoff guard` supplies the detected runner |
|
|
12
|
+
| `--budget SEC` | budget for all commands together, default 180 s |
|
|
13
|
+
| `--install-hook` | write `.git/hooks/pre-commit` containing `exec signoff p1` |
|
|
14
|
+
| `--json` / `--quiet` | machine-readable output / errors only |
|
|
15
|
+
|
|
16
|
+
Output: `{"gate":"p1","ok":true,"elapsedMs":39,"budgetMs":180000,"cases":3,...}`. The case count is the largest number found in `N passed` / `N tests` / `N checks` / `N assertions`.
|
|
17
|
+
|
|
18
|
+
## Runner detection (`lib/detect.js`, 3.7.0)
|
|
19
|
+
|
|
20
|
+
`signoff guard` never guesses silently. It resolves a command in this order and prints where the answer came from:
|
|
21
|
+
|
|
22
|
+
1. `test_cmd` in `.signoff/config.json` (set with `--test-cmd`);
|
|
23
|
+
2. `package.json` → `scripts.test`, unless it says `no test specified`;
|
|
24
|
+
3. `package.json` dependencies → `vitest` / `jest` / `mocha` (`npx --no-install …`);
|
|
25
|
+
4. `Cargo.toml` → `cargo test --quiet`;
|
|
26
|
+
5. `go.mod` → `go test ./...`;
|
|
27
|
+
6. `Makefile` with a `test:` target → `make test`;
|
|
28
|
+
7. python tests (`tests/test_*.py` or `test_*.py` anywhere) → `python -m pytest -q`, else `python -m unittest discover -q`, else a hint that no interpreter exists.
|
|
29
|
+
|
|
30
|
+
An empty `tests/` directory is not a suite. When tests exist but nothing can run them, `init` / `status` / `doctor` say so instead of reporting a silent skip.
|
|
31
|
+
|
|
32
|
+
## P2 `gates/p2_coverage.py`
|
|
33
|
+
|
|
34
|
+
Scans the root plus `packages/` and `apps/` for directories with a `package.json` or `verify/`. Levels:
|
|
35
|
+
|
|
36
|
+
- `scenario`: `verify/scenarios/*.json` all parse and at least one has a `checks` array;
|
|
37
|
+
- `harness`: only pages under `verify/`;
|
|
38
|
+
- `smoke`: only a `package.json`;
|
|
39
|
+
- `missing`: none of the above.
|
|
40
|
+
|
|
41
|
+
| flag | meaning |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `--strict` | fail unless every directory reaches `scenario` |
|
|
44
|
+
| `--min-scenario N` / `--min-assertions N` | thresholds |
|
|
45
|
+
| `--md FILE` | write a Markdown coverage table |
|
|
46
|
+
|
|
47
|
+
## P3 `gates/p3_queue.py`
|
|
48
|
+
|
|
49
|
+
Cards live in `tasks/<id>/card.json` by default:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"id":"t1","goal":"fix the menu","scope":["client.js"],"acceptance":["menu 11/11"],"rollback":"v0.9.34"}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| subcommand | behaviour |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `validate` | five fields present, ids unique, scope is an array of strings |
|
|
58
|
+
| `plan` | cards whose scopes overlap go into different waves; prints the wave list |
|
|
59
|
+
| `collect --changed FILES` | every changed file must be claimed by a card; also lists cards that claimed work but shipped nothing |
|
|
60
|
+
|
|
61
|
+
## P4 `gates/p4_ledger.py`
|
|
62
|
+
|
|
63
|
+
Actions are limited to `config` / `profile` / `publish`. The ledger is scoped per repository: `.signoff/audit.log` inside a repo (falling back to `~/.signoff/audit.log` outside one; `SIGNOFF_HOME` forces a directory). Each record carries a `repo` field, and `verify` only checks rollback points for the current repo.
|
|
64
|
+
|
|
65
|
+
| subcommand | behaviour |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `ask --action --target [--ttl]` | issue a single-use ticket, 600 s by default |
|
|
68
|
+
| `confirm --ticket --decision allow\|deny [--detail] [--rollback]` | consume the ticket and append a hash-chained entry; `deny` is recorded too and exits 1 |
|
|
69
|
+
| `verify` | recompute the chain (`prev` + content hash) and check that the rollback point is an existing git ref; free text fails |
|
|
70
|
+
| `export --out FILE [--merge OTHER...] [--format {md,json,csv}]` | export the ledger; each `--merge` file is re-verified independently and a broken chain is reported with exit 1 |
|
|
71
|
+
|
|
72
|
+
An entry looks like `{"ts":...,"action":"publish","target":"x@1.0.0","detail":"...","rollback":"v1.0.0","decision":"allow","prev":"<sha256>","hash":"<sha256>"}`.
|
|
73
|
+
|
|
74
|
+
## P5 `gates/p5_slop.py`
|
|
75
|
+
|
|
76
|
+
Six rules: `placeholder` (`TODO`, `TBD`, `FIXME`, `待补`, `待定`), `ai-word` (`赋能`/`闭环`/`抓手`/`颗粒度`/`全链路` …), `absolute-claim` (`100%`/`零缺陷`/`永不`/`绝对`/`完美`/`彻底`/`极致`/`无缝`), `empty-adverbs` (two or more in one line), `claim-without-number` (`已验证`/`已覆盖`/`全部通过`/`全绿` on a line with no number), and missing rollback (P4 only).
|
|
77
|
+
|
|
78
|
+
Fenced code and table rows starting with `|` are skipped so examples are not flagged. `--allow <rule>` relaxes one rule.
|
|
79
|
+
|
|
80
|
+
## Auto-guard (2.0.0)
|
|
81
|
+
|
|
82
|
+
After `signoff init`, `pre-commit` / `pre-push` / CI all call `signoff guard`:
|
|
83
|
+
|
|
84
|
+
| what is staged | gates that actually run |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| docs (README, `docs/`) | P5 |
|
|
87
|
+
| a detected test runner | P1 |
|
|
88
|
+
| `verify/scenarios/` | P2 `--strict` |
|
|
89
|
+
| a ledger (`.signoff/audit.log` or `~/.signoff/audit.log`) | P4 `verify` |
|
|
90
|
+
|
|
91
|
+
`.signoff/cache.json` is keyed by `git write-tree`, so the same staged tree is judged once. `SIGNOFF_SKIP=1` skips and appends to `.signoff/skips.log`; `git commit --no-verify` is git's own hole and leaves no record, which is why CI is the boundary that cannot be bypassed.
|
|
92
|
+
|
|
93
|
+
A chained (`--chain`) hook keeps its language: `.py`/`.js`/`.ps1`/`.cmd` suffixes pick the interpreter, and the plain `.local` name still runs first (3.9.0).
|
|
94
|
+
|
|
95
|
+
Since 3.9.1 the lock is a directory holding `owner.json`, taken with `mkdir` (atomic on SMB/NFS, unlike O_EXCL create); a 3.9.0 lock file is still recognised.
|
|
96
|
+
|
|
97
|
+
Since 3.8.0 one hook run at a time holds `.signoff/lock`; a live holder makes the second run exit 1 with the holder's pid, and a lock from a dead pid or older than 10 minutes is broken.
|
|
98
|
+
|
|
99
|
+
## Audit boundary
|
|
100
|
+
|
|
101
|
+
`audit --since-install` first checks that the install point is an ancestor of HEAD. After a rebase, a force-push or a fresh clone it is not, so the command exits 2 instead of reporting a window that covers the wrong commits; `signoff init --reset-install` moves the boundary to HEAD.
|
|
102
|
+
|
|
103
|
+
## Runtime (3.0.0 and later)
|
|
104
|
+
|
|
105
|
+
All five gates and the guard are Node (`lib/p1..p5.js`, `lib/guard.js`); Node 18+ is the only requirement, with no Python fallback. `gates/*.py` is a parity reference only (`npm run parity`).
|
package/docs/gates.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Gates — SignOff / 落印
|
|
2
|
+
|
|
3
|
+
> English: [gates.en.md](gates.en.md)
|
|
4
|
+
|
|
5
|
+
每关一条命令,结果写 `.signoff/pN.json`,退出码 0 通过、1 失败、2 用法或前置条件问题。
|
|
6
|
+
|
|
7
|
+
## P1 `gates/p1_verify.py`
|
|
8
|
+
|
|
9
|
+
| 参数 | 含义 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `--run CMD`(可重复) | 要执行的命令;缺省:有 `tests/` 用 `python -m pytest tests -q`,否则 `npm test --silent` |
|
|
12
|
+
| `--budget SEC` | 全部命令合计预算,默认 180 秒 |
|
|
13
|
+
| `--install-hook` | 写 `.git/hooks/pre-commit`,内容为 `exec signoff p1` |
|
|
14
|
+
| `--json` / `--quiet` | 机器可读输出 / 只报错 |
|
|
15
|
+
|
|
16
|
+
输出:`{"gate":"p1","ok":true,"elapsedMs":39,"budgetMs":180000,"cases":3,...}`。用例数从 `N passed` / `N tests` / `N checks` / `N assertions` 里取最大值。
|
|
17
|
+
|
|
18
|
+
## P2 `gates/p2_coverage.py`
|
|
19
|
+
|
|
20
|
+
扫描根目录与 `packages/`、`apps/` 下带 `package.json` 或 `verify/` 的目录。级别:
|
|
21
|
+
|
|
22
|
+
- `scenario`:`verify/scenarios/*.json` 全部可解析且至少一个 `checks` 数组;
|
|
23
|
+
- `harness`:只有 `verify/` 下的页面;
|
|
24
|
+
- `smoke`:只有 `package.json`;
|
|
25
|
+
- `missing`:都没有。
|
|
26
|
+
|
|
27
|
+
| 参数 | 含义 |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `--strict` | 任一目录没到 `scenario` 即失败 |
|
|
30
|
+
| `--min-scenario N` / `--min-assertions N` | 阈值 |
|
|
31
|
+
| `--md FILE` | 写 Markdown 覆盖表 |
|
|
32
|
+
|
|
33
|
+
## P3 `gates/p3_queue.py`
|
|
34
|
+
|
|
35
|
+
卡片默认在 `tasks/<id>/card.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{"id":"t1","goal":"修菜单","scope":["dsh-session-fork-tree/client.js"],"acceptance":["menu 11/11"],"rollback":"v0.9.34"}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| 子命令 | 行为 |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `validate` | 五个字段齐全、id 不重复、scope 为字符串数组 |
|
|
44
|
+
| `plan` | scope 前缀或通配相交的卡不同波,输出 wave 列表 |
|
|
45
|
+
| `collect --changed FILES` | 每个改动文件必须被某张卡认领;同时列出“改了却没交东西”的卡 |
|
|
46
|
+
|
|
47
|
+
## P4 `gates/p4_ledger.py`
|
|
48
|
+
|
|
49
|
+
动作类别固定为 `config` / `profile` / `publish`,账本按仓库隔离:仓库内 `.signoff/audit.log`(非仓库目录退回 `~/.signoff/audit.log`,`SIGNOFF_HOME` 可强制指定)。每条记录带 `repo` 字段,`verify` 只校验本仓库的回滚点。
|
|
50
|
+
|
|
51
|
+
| 子命令 | 行为 |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `ask --action --target [--ttl]` | 发票据,单次使用,默认 600 秒过期 |
|
|
54
|
+
| `confirm --ticket --decision allow\|deny [--detail] [--rollback]` | 消费票据并追加哈希链条目;`deny` 也记账,退出码 1 |
|
|
55
|
+
| `verify` | 复算链(`prev` + 内容哈希),并校验回滚点是存在的 git ref;自由文本回滚点判失败 |
|
|
56
|
+
| `export --out FILE [--format {md,json,csv}]` | 导出账本;md 是表格,json 是记录数组,csv 是一行一条 |
|
|
57
|
+
|
|
58
|
+
条目形如 `{"ts":...,"action":"publish","target":"x@1.0.0","detail":"...","rollback":"v1.0.0","decision":"allow","prev":"<sha256>","hash":"<sha256>"}`。
|
|
59
|
+
|
|
60
|
+
## P5 `gates/p5_slop.py`
|
|
61
|
+
|
|
62
|
+
六条规则:`placeholder`(未完成的占位标记:`TODO`、`TBD`、`FIXME`、`待补`、`待定`)、`ai-word`(`赋能`/`闭环`/`抓手`/`颗粒度`/`全链路`…)、`absolute-claim`(`100%`/`零缺陷`/`永不`/`绝对`/`完美`/`彻底`/`极致`/`无缝`)、`empty-adverbs`(一行两个及以上空副词)、`claim-without-number`(`已验证`/`已覆盖`/`全部通过`/`全绿` 所在行没有数字)、缺回滚(仅 P4 使用)。
|
|
63
|
+
|
|
64
|
+
跳过 fenced code 与以 `|` 开头的表格行,避免把示例当空话。`--allow <rule>` 可放行某条规则。
|
|
65
|
+
|
|
66
|
+
## 自动守护(2.0.0)
|
|
67
|
+
|
|
68
|
+
`signoff init` 之后,`pre-commit` / `pre-push` / CI 都会调 `signoff guard`:
|
|
69
|
+
|
|
70
|
+
| 场景 | 实际跑的关 |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| 暂存的文档(README、docs/ 下) | P5 |
|
|
73
|
+
| 能探到测试命令(package.json 的 test,或 pytest) | P1 |
|
|
74
|
+
| 存在 `verify/scenarios/` | P2 `--strict` |
|
|
75
|
+
| 存在账本(仓库内 `.signoff/audit.log` 或 `~/.signoff/audit.log`) | P4 `verify` |
|
|
76
|
+
|
|
77
|
+
缓存文件 `.signoff/cache.json` 以 `git write-tree` 的结果为键:同一份暂存内容重复提交只判一次。
|
|
78
|
+
`SIGNOFF_SKIP=1` 跳过并写 `.signoff/skips.log`;`git commit --no-verify` 是 git 自身口子,无记录,因此 CI 是不可绕过的边界。
|
|
79
|
+
|
|
80
|
+
> 自检记录(2026-11-06):装好 `signoff init` 后,本条修正在 `pre-commit` 里跑过 P1(30 用例)与 P5(3 份文档 0 命中)。
|
|
81
|
+
|
|
82
|
+
## 运行环境(3.0.0 起)
|
|
83
|
+
|
|
84
|
+
## 审计边界
|
|
85
|
+
|
|
86
|
+
`audit --since-install` 先确认安装点是 HEAD 的祖先;rebase、强推、换克隆后对不上会退 2,用 `signoff init --reset-install` 把边界挪到当前 HEAD。
|
|
87
|
+
|
|
88
|
+
五关与守卫都是 Node 实现(`lib/p1..p5.js`、`lib/guard.js`),只需要 Node 18+;不再依赖 Python,也没有降级模式。`gates/*.py` 仅作为对拍参考(`npm run parity`)。
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 接入方式(同一套判决,六种入口)
|
|
2
|
+
|
|
3
|
+
落印不绑定平台:任何一个能跑命令的地方都能接入,判决与退出码一致(0 通过、1 红、2 用法/环境)。
|
|
4
|
+
|
|
5
|
+
## 1. 本机零接触(最快)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g signoff-cli && cd your-repo && signoff init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
之后 `git commit` 自动过闸。
|
|
12
|
+
|
|
13
|
+
## 2. GitHub Actions
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
- uses: ayuanxx-max/dsh-signoff@v3.15.0
|
|
17
|
+
with:
|
|
18
|
+
args: all
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
红关会输出 `::error file=...,line=...` 注解并写步骤摘要。
|
|
22
|
+
|
|
23
|
+
## 3. pre-commit
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
repos:
|
|
27
|
+
- repo: https://github.com/ayuanxx-max/dsh-signoff
|
|
28
|
+
rev: v3.15.0
|
|
29
|
+
hooks:
|
|
30
|
+
- id: signoff-guard
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 4. lefthook
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
pre-commit:
|
|
37
|
+
commands:
|
|
38
|
+
signoff:
|
|
39
|
+
run: signoff guard
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 5. husky
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
echo 'signoff guard' > .husky/pre-commit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 6. VS Code
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
signoff init --editor vscode
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
生成 6 个任务与问题匹配器,红关直接落到 Problems 面板。
|
|
55
|
+
|
|
56
|
+
## MCP
|
|
57
|
+
|
|
58
|
+
`io.github.ayuanxx-max/signoff` 已在 MCP Registry,可被支持 MCP 的客户端直接调用。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 推广:GitHub 上没有浏览时怎么办
|
|
2
|
+
|
|
3
|
+
前提:这是一个**工具型**仓库,不是内容型。工具靠"被引用"传播,不靠刷。
|
|
4
|
+
所以顺序是:先让仓库在 GitHub 内部可被检索到,再让外部渠道有人替你说一句话,最后才是发帖。
|
|
5
|
+
|
|
6
|
+
## 第 0 层:GitHub 内部可发现性(今天就能做完,收益最确定)
|
|
7
|
+
|
|
8
|
+
| 项 | 现状 | 应做 |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| 仓库 description | mojibake(`SignOff 4e00 8bfa...`) | 改成一句英文 + 一句中文,含关键词 |
|
|
11
|
+
| topics | **空** | 至少 8 个:`git-hooks` `pre-commit` `code-quality` `audit` `cli` `nodejs` `testing` `llm-agents` `ai-agents` `mcp` |
|
|
12
|
+
| README 首屏 | 中文长文,无英文 | 顶部三行:一句话 + 安装 + 一张门禁输出;`README.en.md` 供非中文读者 |
|
|
13
|
+
| Releases | 有 tarball,但说明只有几行 | 每个 Release 写安装 + 用法 + 变更(已按此改造) |
|
|
14
|
+
| License | 确认存在 | 无 LICENSE 的仓库会被企业用户直接跳过 |
|
|
15
|
+
|
|
16
|
+
GitHub 的搜索会索引 description、topics、README 正文;topics 为空等于不参与任何主题页。
|
|
17
|
+
|
|
18
|
+
## 第 1 层:进入别人的清单(一次性的、长期有效的流量)
|
|
19
|
+
|
|
20
|
+
优先级从高到低:
|
|
21
|
+
|
|
22
|
+
1. **awesome-mcp-servers** —— 我们有真正的 MCP server,这类清单审核看"能不能跑",通过率高。
|
|
23
|
+
2. **awesome-git-hooks / awesome-pre-commit / awesome-static-analysis** —— 与本工具的定位完全对口。
|
|
24
|
+
3. **awesome-claude-code / awesome-ai-agents** —— agent 工作流清单,signoff 的"证明 done"是它们的空白格。
|
|
25
|
+
4. **awesome-nodejs**(门槛高、竞争大)——最后再试。
|
|
26
|
+
|
|
27
|
+
提交方式:fork → 在对应分类加一行(一句话 + 链接)→ PR。**一行就够,别写广告词**,
|
|
28
|
+
这类清单的维护者会直接关掉营销文案。
|
|
29
|
+
|
|
30
|
+
## 第 2 层:内容(发一次,长期被搜到)
|
|
31
|
+
|
|
32
|
+
英文主战场(选一个发,别群发):
|
|
33
|
+
|
|
34
|
+
- **Show HN**:标题不要写 "SignOff — a tool for X"。写结论:
|
|
35
|
+
`Show HN: A git hook that refuses to let an agent say "done" without proof`。
|
|
36
|
+
正文三段:问题(agent 说完成了,凭什么信)、做法(五关 + 账本 + 退出码即判决)、
|
|
37
|
+
一条真实输出。HN 会问"和 husky/lint-staged 有什么区别",答案必须一句话说清。
|
|
38
|
+
- **dev.to / r/git**:偏教程,`How to make "done" verifiable in a repo`。
|
|
39
|
+
|
|
40
|
+
中文:
|
|
41
|
+
|
|
42
|
+
- **V2EX / 掘金 / 知乎**:标题带痛点——"让 AI 写完代码说'完成'这件事,怎么验?"
|
|
43
|
+
- 内容结构同上,附中文 README 链接。
|
|
44
|
+
|
|
45
|
+
## 第 3 层:定位(决定别人要不要点开)
|
|
46
|
+
|
|
47
|
+
不要和 lint 工具比,它们不判"完成"。可用的对比句:
|
|
48
|
+
|
|
49
|
+
- husky / lefthook:**装钩子**,不决定钩子里跑什么、失败算什么;
|
|
50
|
+
- pre-commit:**跑检查**,不产账本,也没有"这次提交真的过闸门了吗"的审计;
|
|
51
|
+
- SignOff:把"完成"变成一个**可验证的判决**(退出码)+ 一条**可审计的链**(账本)。
|
|
52
|
+
|
|
53
|
+
## 度量(别猜,读数据)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 近 14 天浏览量/克隆量(需要 push 权限的 token)
|
|
57
|
+
curl -s -H "Authorization: token $TOKEN" \
|
|
58
|
+
https://api.github.com/repos/ayuanxx-max/dsh-signoff/traffic/views
|
|
59
|
+
# 每周 npm 下载
|
|
60
|
+
curl -s https://api.npmjs.org/downloads/point/last-week/dsh-signoff
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
现实预期:这类工具前两周能到 20–50 star 就算不错;单个 awesome-list 收录常带来 10–30 star 的长尾。
|
|
64
|
+
**不做**:买赞、互 star、群发私信——GitHub 会判定为 spam,收益为负。
|
|
65
|
+
|
|
66
|
+
## 顺序建议
|
|
67
|
+
|
|
68
|
+
1. 今天:description + topics + README.en.md(**这一步的投入产出比最高**)。
|
|
69
|
+
2. 本周:三个 awesome-list PR。
|
|
70
|
+
3. 有第一波 star 后:Show HN(先有社会证明再发帖,转化率差 2–3 倍)。
|
|
71
|
+
4. 之后每月一次:Release 说明写成"这次解决了谁的什么问题",Release 页面本身也会被搜到。
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "signoff.json",
|
|
4
|
+
"description": "Repo-level opt-in for SignOff: a committed declaration makes every clone run the five gates without a local install step.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"gates": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": [
|
|
12
|
+
"p1",
|
|
13
|
+
"p2",
|
|
14
|
+
"p3",
|
|
15
|
+
"p4",
|
|
16
|
+
"p5"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"description": "Documentation only; the guard always runs P1-P5."
|
|
20
|
+
},
|
|
21
|
+
"test_cmd": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Pinned test command. Omit it unless every clone has that runner."
|
|
24
|
+
},
|
|
25
|
+
"budget": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"description": "Seconds a gate may spend before it counts as a failure."
|
|
28
|
+
},
|
|
29
|
+
"docs": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
34
|
+
"description": "Docs paths P5 should read."
|
|
35
|
+
},
|
|
36
|
+
"pre_push": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"description": "Run the full set before a push too."
|
|
39
|
+
},
|
|
40
|
+
"secrets": {
|
|
41
|
+
"type": [
|
|
42
|
+
"boolean",
|
|
43
|
+
"object"
|
|
44
|
+
],
|
|
45
|
+
"description": "Secret scanning before P1. false disables it; an object accepts {allow: [rule or rule:path]}."
|
|
46
|
+
},
|
|
47
|
+
"cache": {
|
|
48
|
+
"type": "boolean",
|
|
49
|
+
"description": "Cache the P1 verdict per staged tree (default true)."
|
|
50
|
+
},
|
|
51
|
+
"docsFast": {
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"description": "Skip the suite when every staged file is a document (default true)."
|
|
54
|
+
},
|
|
55
|
+
"coverage": {
|
|
56
|
+
"type": [
|
|
57
|
+
"boolean",
|
|
58
|
+
"object"
|
|
59
|
+
],
|
|
60
|
+
"description": "Run signoff cov in the guard. true means min 80; {min: 90} sets the threshold.",
|
|
61
|
+
"properties": {
|
|
62
|
+
"min": {
|
|
63
|
+
"type": "number"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"sign": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"description": "Sign the commit trailer (needs a key)."
|
|
70
|
+
},
|
|
71
|
+
"sign_key": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Path to the private key used for trailers."
|
|
74
|
+
},
|
|
75
|
+
"p5": {
|
|
76
|
+
"type": [
|
|
77
|
+
"string",
|
|
78
|
+
"object"
|
|
79
|
+
],
|
|
80
|
+
"description": "Rule pack for gate 5: \"zh\" (default), \"en\", \"both\", or {pack, rules, allow}.",
|
|
81
|
+
"properties": {
|
|
82
|
+
"pack": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"enum": [
|
|
85
|
+
"zh",
|
|
86
|
+
"en",
|
|
87
|
+
"both"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"rules": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Path to an extra rule JSON."
|
|
93
|
+
},
|
|
94
|
+
"allow": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"checkpoint": {
|
|
103
|
+
"type": "boolean",
|
|
104
|
+
"description": "advance a signed ledger checkpoint after every passing run"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"additionalProperties": false
|
|
108
|
+
}
|