sillyspec 3.25.0 → 3.25.2
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/.claude/skills/sillyspec-doctor/SKILL.md +9 -0
- package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
- package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
- package/.playwright-mcp/prompt-final.png +0 -0
- package/.playwright-mcp/prompt-view.png +0 -0
- package/CLAUDE.md +18 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +60 -12
- package/src/check-primitives.js +65 -0
- package/src/db.js +28 -8
- package/src/hooks/worktree-guard.js +9 -2
- package/src/index.js +140 -11
- package/src/machine-interface.js +13 -7
- package/src/progress/change-registry.js +217 -0
- package/src/progress/consistency-doctor.js +259 -0
- package/src/progress/shared.js +33 -0
- package/src/progress/stage-machine.js +535 -0
- package/src/progress/step-store.js +256 -0
- package/src/progress.js +75 -1191
- package/src/quicklog.js +3 -1
- package/src/run/command.js +1088 -0
- package/src/run/complete-handlers.js +874 -0
- package/src/run/complete.js +725 -0
- package/src/run/gates.js +326 -0
- package/src/run/prompt.js +619 -0
- package/src/run/quick-audit.js +107 -0
- package/src/run/scan-profile.js +238 -0
- package/src/run/shared.js +418 -0
- package/src/run/stage.js +414 -0
- package/src/run.js +23 -4379
- package/src/stage-contract-engine.js +150 -0
- package/src/stage-contract-spec.js +517 -0
- package/src/stage-contract.js +87 -150
- package/src/stage-review.js +63 -0
- package/src/stages/brainstorm.js +3 -1
- package/src/stages/execute.js +5 -2
- package/src/stages/plan-postcheck.js +38 -27
- package/src/stages/plan.js +12 -49
- package/src/stages/propose.js +2 -1
- package/src/stages/scan.js +3 -1
- package/src/workflow.js +9 -17
- package/src/worktree-apply.js +5 -2
- package/src/worktree-deps.js +114 -32
- package/src/worktree.js +23 -11
- package/src/brainstorm-postcheck.js +0 -158
|
@@ -51,9 +51,18 @@ sillyspec progress validate # 校验并修复
|
|
|
51
51
|
sillyspec worktree doctor [--fix] # worktree 健康检查 + 修复
|
|
52
52
|
sillyspec doctor --align-execute-progress --change <name> # 按 plan.md 声明对齐 execute 派生戳(dry-run,只报告将补哪些 step)
|
|
53
53
|
sillyspec doctor --align-execute-progress --change <name> --confirm # 实际落盘:补 step 戳 + 置 execute stage status=completed
|
|
54
|
+
sillyspec doctor --cleanup-remnant # 扫描并报告 0 字节空占位 db(dry-run)
|
|
55
|
+
sillyspec doctor --cleanup-remnant --confirm # 实际删除空占位 db(仅删 0 字节,不动有内容的 db)
|
|
56
|
+
sillyspec doctor --dump-db --path <db 路径> # dump 指定 db 内容到文件(取证,查 stages/steps/changes 表)
|
|
57
|
+
sillyspec doctor --json # 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
|
|
58
|
+
sillyspec runtime list # 枚举 .sillyspec/.runtime/ 下所有运行时产物(只读,看手上有哪些证据/状态文件)
|
|
54
59
|
```
|
|
55
60
|
|
|
56
61
|
> `--align-execute-progress` 仅当 `plan.md` 所有 task checkbox 全勾时才对齐 execute 阶段进度戳。典型用于 worktree 已 cleanup(终态)但 execute 派生戳未盖上的死锁。默认 dry-run,加 `--confirm` 才写盘。doctor 信任 `plan.md` 声明、不复核代码,verify 阶段兜底。`--change` 缺省时按单活跃变更自动兜底。
|
|
62
|
+
>
|
|
63
|
+
> `--cleanup-remnant` 清理 init/平台切换残留的 0 字节空 db(占位但无数据)。默认 dry-run 只报告,`--confirm` 才删。绝不删有内容的 db。
|
|
64
|
+
>
|
|
65
|
+
> `runtime list` 是只读枚举:列出 `.sillyspec/.runtime/` 下的 db、doctor-diagnosis.json、workflow-runs/、gate-status.json、user-inputs.md 等产物及用途。多会话/压缩后想知道「我手上有哪些证据文件」时用。加 `--json` 程序化读取。
|
|
57
66
|
|
|
58
67
|
## 铁律
|
|
59
68
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[ 809ms] [ERROR] Failed to load resource: the server responded with a status of 404 (File not found) @ http://localhost:8848/favicon.ico:0
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
- generic [active] [ref=e1]:
|
|
2
|
+
- banner [ref=e2]:
|
|
3
|
+
- generic [ref=e3]:
|
|
4
|
+
- generic [ref=e4]:
|
|
5
|
+
- text: Silly
|
|
6
|
+
- emphasis [ref=e5]: Spec
|
|
7
|
+
- generic [ref=e6]: Prompt Dossier
|
|
8
|
+
- generic [ref=e7]:
|
|
9
|
+
- text: ⌕
|
|
10
|
+
- textbox "搜索步骤名 / prompt 内容 / 占位符…" [ref=e8]
|
|
11
|
+
- generic [ref=e9]:
|
|
12
|
+
- generic [ref=e10]: 12 stages · 71 steps · 87k
|
|
13
|
+
- button "◐" [ref=e11] [cursor=pointer]
|
|
14
|
+
- generic [ref=e87]:
|
|
15
|
+
- navigation [ref=e88]:
|
|
16
|
+
- generic [ref=e89]:
|
|
17
|
+
- generic [ref=e90]:
|
|
18
|
+
- text: 主流程
|
|
19
|
+
- generic [ref=e91]: "4"
|
|
20
|
+
- generic [ref=e92] [cursor=pointer]:
|
|
21
|
+
- generic [ref=e93]: "01"
|
|
22
|
+
- generic [ref=e94]: 头脑风暴
|
|
23
|
+
- generic [ref=e95]: "8"
|
|
24
|
+
- generic [ref=e96] [cursor=pointer]:
|
|
25
|
+
- generic [ref=e97]: "02"
|
|
26
|
+
- generic [ref=e98]: 实现计划
|
|
27
|
+
- generic [ref=e99]: "5"
|
|
28
|
+
- generic [ref=e100] [cursor=pointer]:
|
|
29
|
+
- generic [ref=e101]: "03"
|
|
30
|
+
- generic [ref=e102]: 波次执行
|
|
31
|
+
- generic [ref=e103]: "12"
|
|
32
|
+
- generic [ref=e104] [cursor=pointer]:
|
|
33
|
+
- generic [ref=e105]: "04"
|
|
34
|
+
- generic [ref=e106]: 验证
|
|
35
|
+
- generic [ref=e107]: "7"
|
|
36
|
+
- generic [ref=e108]:
|
|
37
|
+
- generic [ref=e109]:
|
|
38
|
+
- text: 辅助
|
|
39
|
+
- generic [ref=e110]: "6"
|
|
40
|
+
- generic [ref=e111] [cursor=pointer]:
|
|
41
|
+
- generic [ref=e112]: "05"
|
|
42
|
+
- generic [ref=e113]: 项目扫描
|
|
43
|
+
- generic [ref=e114]: "11"
|
|
44
|
+
- generic [ref=e115] [cursor=pointer]:
|
|
45
|
+
- generic [ref=e116]: "06"
|
|
46
|
+
- generic [ref=e117]: 快速任务
|
|
47
|
+
- generic [ref=e118]: "3"
|
|
48
|
+
- generic [ref=e119] [cursor=pointer]:
|
|
49
|
+
- generic [ref=e120]: "07"
|
|
50
|
+
- generic [ref=e121]: 自由探索
|
|
51
|
+
- generic [ref=e122]: "1"
|
|
52
|
+
- generic [ref=e123] [cursor=pointer]:
|
|
53
|
+
- generic [ref=e124]: "08"
|
|
54
|
+
- generic [ref=e125]: 归档
|
|
55
|
+
- generic [ref=e126]: "5"
|
|
56
|
+
- generic [ref=e127] [cursor=pointer]:
|
|
57
|
+
- generic [ref=e128]: "09"
|
|
58
|
+
- generic [ref=e129]: 状态
|
|
59
|
+
- generic [ref=e130]: "3"
|
|
60
|
+
- generic [ref=e131] [cursor=pointer]:
|
|
61
|
+
- generic [ref=e132]: "10"
|
|
62
|
+
- generic [ref=e133]: 自检
|
|
63
|
+
- generic [ref=e134]: "5"
|
|
64
|
+
- generic [ref=e135]:
|
|
65
|
+
- generic [ref=e136]:
|
|
66
|
+
- text: 变体
|
|
67
|
+
- generic [ref=e137]: "1"
|
|
68
|
+
- generic [ref=e138] [cursor=pointer]:
|
|
69
|
+
- generic [ref=e139]: "11"
|
|
70
|
+
- generic [ref=e140]: 自动模式头脑风暴
|
|
71
|
+
- generic [ref=e141]: "4"
|
|
72
|
+
- generic [ref=e142]:
|
|
73
|
+
- generic [ref=e143]:
|
|
74
|
+
- text: 已废弃
|
|
75
|
+
- generic [ref=e144]: "1"
|
|
76
|
+
- generic [ref=e145] [cursor=pointer]:
|
|
77
|
+
- generic [ref=e146]: "12"
|
|
78
|
+
- generic [ref=e147]: propose · 已废弃
|
|
79
|
+
- generic [ref=e148]: "7"
|
|
80
|
+
- main [ref=e149]:
|
|
81
|
+
- generic [ref=e150]: 主流程 · STAGE 01
|
|
82
|
+
- heading "头脑风暴" [level=1] [ref=e151]
|
|
83
|
+
- paragraph [ref=e152]: 探索需求、分析技术方案、识别风险
|
|
84
|
+
- generic [ref=e153]:
|
|
85
|
+
- generic [ref=e154]: sourcesrc/stages/brainstorm.js
|
|
86
|
+
- generic [ref=e155]: persona资深架构师
|
|
87
|
+
- generic [ref=e156]: steps8
|
|
88
|
+
- generic [ref=e157]:
|
|
89
|
+
- button "全部展开" [ref=e158] [cursor=pointer]
|
|
90
|
+
- button "全部折叠" [ref=e159] [cursor=pointer]
|
|
91
|
+
- generic [ref=e160]: 8 STEPS
|
|
92
|
+
- generic [ref=e161]:
|
|
93
|
+
- article [ref=e162]:
|
|
94
|
+
- generic [ref=e163] [cursor=pointer]:
|
|
95
|
+
- generic [ref=e164]: 01 / 08
|
|
96
|
+
- generic [ref=e165]: 状态检查
|
|
97
|
+
- generic [ref=e166]: ▸
|
|
98
|
+
- article [ref=e167]:
|
|
99
|
+
- generic [ref=e168] [cursor=pointer]:
|
|
100
|
+
- generic [ref=e169]: 02 / 08
|
|
101
|
+
- generic [ref=e170]: 加载项目上下文
|
|
102
|
+
- generic [ref=e171]: ▸
|
|
103
|
+
- article [ref=e172]:
|
|
104
|
+
- generic [ref=e173] [cursor=pointer]:
|
|
105
|
+
- generic [ref=e174]: 03 / 08
|
|
106
|
+
- generic [ref=e175]: 对话式探索与需求澄清
|
|
107
|
+
- generic [ref=e176]:
|
|
108
|
+
- generic [ref=e177]: conditionalWait
|
|
109
|
+
- generic [ref=e178]: repeatable×8
|
|
110
|
+
- generic [ref=e179]: ▸
|
|
111
|
+
- article [ref=e180]:
|
|
112
|
+
- generic [ref=e181] [cursor=pointer]:
|
|
113
|
+
- generic [ref=e182]: 04 / 08
|
|
114
|
+
- generic [ref=e183]: 提出 2-3 种方案
|
|
115
|
+
- generic [ref=e184]: requiresWait
|
|
116
|
+
- generic [ref=e186]: ▸
|
|
117
|
+
- article [ref=e187]:
|
|
118
|
+
- generic [ref=e188] [cursor=pointer]:
|
|
119
|
+
- generic [ref=e189]: 05 / 08
|
|
120
|
+
- generic [ref=e190]: 分段展示设计
|
|
121
|
+
- generic [ref=e191]: requiresWait
|
|
122
|
+
- generic [ref=e193]: ▸
|
|
123
|
+
- article [ref=e194]:
|
|
124
|
+
- generic [ref=e195] [cursor=pointer]:
|
|
125
|
+
- generic [ref=e196]: 06 / 08
|
|
126
|
+
- generic [ref=e197]: 写设计文档并自审
|
|
127
|
+
- generic [ref=e198]: ▸
|
|
128
|
+
- article [ref=e199]:
|
|
129
|
+
- generic [ref=e200] [cursor=pointer]:
|
|
130
|
+
- generic [ref=e201]: 07 / 08
|
|
131
|
+
- generic [ref=e202]: Design Grill 交叉审查
|
|
132
|
+
- generic [ref=e203]: conditionalWait
|
|
133
|
+
- generic [ref=e205]: ▸
|
|
134
|
+
- article [ref=e206]:
|
|
135
|
+
- generic [ref=e207] [cursor=pointer]:
|
|
136
|
+
- generic [ref=e208]: 08 / 08
|
|
137
|
+
- generic [ref=e209]: 用户确认并生成规范文件
|
|
138
|
+
- generic [ref=e210]: requiresWait
|
|
139
|
+
- generic [ref=e212]: ▸
|
|
140
|
+
- contentinfo [ref=e213]:
|
|
141
|
+
- text: 数据机械提取自
|
|
142
|
+
- code [ref=e214]: src/stages/*.js
|
|
143
|
+
- text: · 单文件自包含 · file:// 可直接打开 · 生成命令:
|
|
144
|
+
- code [ref=e215]: node docs/prompt/_build-site.mjs
|
|
Binary file
|
|
Binary file
|
package/CLAUDE.md
CHANGED
|
@@ -20,3 +20,21 @@
|
|
|
20
20
|
- [ ] 归档/清理流程描述与实际代码逻辑一致
|
|
21
21
|
- [ ] 数据库 Schema 描述与 `src/db.js` 一致
|
|
22
22
|
- [ ] 更新文档头部 `updated_at` 时间戳
|
|
23
|
+
|
|
24
|
+
## 提示词文档同步(docs/prompt/)
|
|
25
|
+
`docs/prompt/` 收录各阶段 CLI→Agent 的**逐步提示词原文**(人类可读 md 参考)。详见 `docs/prompt/README.md`。
|
|
26
|
+
|
|
27
|
+
### 数据源关系(重要 — 避免双写漂移)
|
|
28
|
+
- **唯一数据源 = `src/stages/*.js`**(`definition.steps[].prompt`,CLI 运行时读这里)+ `src/run/prompt.js`(注入框架:persona / 铁律 / 占位符替换 / 完成后执行命令模板)。
|
|
29
|
+
- `docs/prompt/*.md` 是源码的**机械提取人类可读镜像**,由 `node docs/prompt/_extract.mjs` 一键再生(源码 → `_extracted.json` → md)。提取脚本对静态阶段直接读 `definition.steps`,对动态阶段(plan/execute)用示例输入跑 `buildPlanSteps`/`buildExecuteSteps` 得到真实 prompt。
|
|
30
|
+
- **禁止手改 md 里的 prompt 原文**(下次提取会被覆盖,且与源码不一致)。要改提示词,改源码后重跑脚本。
|
|
31
|
+
|
|
32
|
+
### 触发同步的改动
|
|
33
|
+
- 修改 `src/stages/*.js`:`definition.steps[].prompt`、步骤名、步骤增删/重排、wait 配置、`_globalGuardrails`、title/description
|
|
34
|
+
- 修改 `src/run/prompt.js`:persona / 铁律 / 占位符替换 / 完成后执行命令模板
|
|
35
|
+
- 修改 `stage-review.js` / `review-tier.js` / `knowledge-match.js`:影响 `{REVIEW_JSON_CONTRACT}` / `{REVIEW_TIER}` / `{KNOWLEDGE_HIT_REPORT}` 等动态块
|
|
36
|
+
|
|
37
|
+
### 同步步骤
|
|
38
|
+
1. 改源码
|
|
39
|
+
2. 运行 `node docs/prompt/_extract.mjs`(刷新 `docs/prompt/_extracted.json`)
|
|
40
|
+
3. 按变动同步对应 `docs/prompt/<stage>.md`(prompt 正文以 `_extracted.json` 为准逐字替换)+ 必要时更新 `docs/prompt/README.md`(占位符总表 / persona 表 / CLI 注入框架)
|
package/package.json
CHANGED
|
@@ -251,6 +251,51 @@ export function detectRiskProfile({ designContent = '', planContent = '', change
|
|
|
251
251
|
* 旧的 detectChangeRisk 接口保持向后兼容
|
|
252
252
|
* @returns {{ level: string, triggers: string[], requiredVerification: string[] }}
|
|
253
253
|
*/
|
|
254
|
+
// ============ 门控可执行化(A:报错说人话) ============
|
|
255
|
+
//
|
|
256
|
+
// 历史教训:integration-critical / deployment-critical 门控只报「缺少真实集成证据 / 需要真实
|
|
257
|
+
// 启动验证证据」,agent 看不出具体缺哪一项、要写/做什么才算过,只能靠改结论文案撞墙。
|
|
258
|
+
// 此处把每一项 requiredVerification 的需求(写什么 / 做什么)和判级原因显式导出,
|
|
259
|
+
// 供 stage-contract 拼成可执行的报错信息。
|
|
260
|
+
|
|
261
|
+
/** 每项 requiredVerification 的需求描述 + 字面期望 */
|
|
262
|
+
export const VERIFICATION_NEEDS = {
|
|
263
|
+
unit_tests: {
|
|
264
|
+
desc: '单元测试(verify 必做;在 verify-result.md 写明测试套件与结果)',
|
|
265
|
+
literals: [],
|
|
266
|
+
},
|
|
267
|
+
contract_tests: {
|
|
268
|
+
desc: '契约测试(前后端/跨进程 API parity 对账,写明结论)',
|
|
269
|
+
literals: [],
|
|
270
|
+
},
|
|
271
|
+
real_daemon_backend_integration: {
|
|
272
|
+
desc: '真实 daemon↔backend 集成验证(非仅 mock 单测)。写「真实集成/端到端」的证据;',
|
|
273
|
+
literals: ['端到端', 'integration test', 'e2e test', 'daemon.*backend', '真实集成', 'runtime evidence', '运行时证据'],
|
|
274
|
+
},
|
|
275
|
+
runtime_log_evidence: {
|
|
276
|
+
desc: 'Runtime Evidence section + 指向真实日志/证据片段。',
|
|
277
|
+
literals: ['Runtime Evidence', '运行时证据', 'daemon log', '日志片段'],
|
|
278
|
+
},
|
|
279
|
+
real_startup_once: {
|
|
280
|
+
desc: '真实启动一次本变更触及的部署/启动入口(服务入口、CLI 主入口、守护进程等——须是本变更实际改动的那一类入口,不能拿无关进程的启动来凑数)。',
|
|
281
|
+
literals: ['启动.*一次', '实际.*启动', 'real startup', 'docker up', 'npm start', 'node server'],
|
|
282
|
+
},
|
|
283
|
+
terminal_state_assertion: {
|
|
284
|
+
desc: '终态断言(建议项,不阻断):AgentRun running→completed/failed、session/lease end 状态同步。',
|
|
285
|
+
literals: ['terminal state', '终态', 'completed failed', 'session end', 'lease end'],
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 风险分级的判定来源(命中关键词来源 = design.md / plan.md 内容,非改动文件本身) */
|
|
290
|
+
export const RISK_LEVEL_CAUSES = {
|
|
291
|
+
'deployment-critical':
|
|
292
|
+
'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
|
|
293
|
+
'注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design 中如实缩小范围,但不建议为绕门控而规避——危险链路该有真实启动证据。',
|
|
294
|
+
'integration-critical':
|
|
295
|
+
'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。',
|
|
296
|
+
'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
|
|
297
|
+
}
|
|
298
|
+
|
|
254
299
|
export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
|
|
255
300
|
const triggers = []
|
|
256
301
|
const combined = [designContent, planContent].join('\n')
|
|
@@ -308,6 +353,16 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification) {
|
|
|
308
353
|
const warnings = []
|
|
309
354
|
const lower = verifyContent.toLowerCase()
|
|
310
355
|
|
|
356
|
+
// 字面证据正则从 VERIFICATION_NEEDS[k].literals 派生——与报错描述、prompt 事前契约严格同源,
|
|
357
|
+
// 杜绝历史上"描述说 A、正则查 B"的分叉(checkIntegrationEvidence 正则曾比 VERIFICATION_NEEDS
|
|
358
|
+
// 描述多匹配 集成测试/backend.*daemon/real.*integration 等)。literals 为空的 need
|
|
359
|
+
// (unit_tests/contract_tests)无字面校验,视为满足。
|
|
360
|
+
const hasEvidence = (k) => {
|
|
361
|
+
const n = VERIFICATION_NEEDS[k]
|
|
362
|
+
if (!n || !n.literals || n.literals.length === 0) return true
|
|
363
|
+
return new RegExp(n.literals.join('|'), 'i').test(lower)
|
|
364
|
+
}
|
|
365
|
+
|
|
311
366
|
const needsIntegration = requiredVerification.includes('real_daemon_backend_integration')
|
|
312
367
|
const needsLogEvidence = requiredVerification.includes('runtime_log_evidence')
|
|
313
368
|
const needsTerminalState = requiredVerification.includes('terminal_state_assertion')
|
|
@@ -315,11 +370,7 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification) {
|
|
|
315
370
|
|
|
316
371
|
if (needsIntegration) {
|
|
317
372
|
const hasMockOnly = /mock.*test.*passed|unit.*test.*passed/i.test(lower)
|
|
318
|
-
const hasIntegrationEvidence =
|
|
319
|
-
/集成测试|integration.*test|e2e.*test|端到端/i.test(lower) ||
|
|
320
|
-
/daemon.*backend|backend.*daemon|真实.*集成|real.*integration/i.test(lower) ||
|
|
321
|
-
/runtime.*evidence|运行时.*证据/i.test(lower)
|
|
322
|
-
|
|
373
|
+
const hasIntegrationEvidence = hasEvidence('real_daemon_backend_integration')
|
|
323
374
|
if (!hasIntegrationEvidence && hasMockOnly) {
|
|
324
375
|
errors.push('integration-critical 变更只提供了 mock 单测证据,缺少真实 daemon↔backend 集成验证')
|
|
325
376
|
} else if (!hasIntegrationEvidence) {
|
|
@@ -328,25 +379,22 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification) {
|
|
|
328
379
|
}
|
|
329
380
|
|
|
330
381
|
if (needsLogEvidence) {
|
|
331
|
-
|
|
332
|
-
if (!hasRuntimeSection) {
|
|
382
|
+
if (!hasEvidence('runtime_log_evidence')) {
|
|
333
383
|
errors.push('integration-critical 变更的 verify-result.md 缺少 Runtime Evidence section')
|
|
334
384
|
}
|
|
335
385
|
}
|
|
336
386
|
|
|
337
387
|
if (needsTerminalState) {
|
|
338
|
-
|
|
339
|
-
if (!hasTerminalState) {
|
|
388
|
+
if (!hasEvidence('terminal_state_assertion')) {
|
|
340
389
|
warnings.push('建议检查终态断言:AgentRun running→completed/failed、session end 状态同步')
|
|
341
390
|
}
|
|
342
391
|
}
|
|
343
392
|
|
|
344
393
|
if (needsRealStartup) {
|
|
345
|
-
|
|
346
|
-
if (!hasStartupEvidence) {
|
|
394
|
+
if (!hasEvidence('real_startup_once')) {
|
|
347
395
|
errors.push('deployment-critical 变更需要真实启动验证证据')
|
|
348
396
|
}
|
|
349
397
|
}
|
|
350
398
|
|
|
351
399
|
return { ok: errors.length === 0, errors, warnings }
|
|
352
|
-
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-primitives.js — 共享的产物字面校验原语(纯函数)
|
|
3
|
+
*
|
|
4
|
+
* 从 workflow.js 的 checkOutput / _checkWorkflow 抽出,作为"何谓 contains_sections / min_lines /
|
|
5
|
+
* no_placeholder / no_empty_files"的**全仓单一语义源**。workflow 引擎与 stage-contract 引擎
|
|
6
|
+
* (stage-contract-engine.js)共用同一组谓词,杜绝"workflow 认 `## X` 子串、stage-contract 认别的"
|
|
7
|
+
* 这类判定分叉(本任务的"事前==事后"同源在原语层就成立)。
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:纯文本判定,不碰 fs。调用方负责读文件 + CRLF 归一化后传入 content——
|
|
10
|
+
* 这样 Windows CRLF 差异在调用方收敛(plan-postcheck 已用 LF-normalizing reader),引擎拿到
|
|
11
|
+
* 什么判什么,不在原语层二次引入 readFileSync 差异(缺陷 windows-python-crlf-taskcard 的根因)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// no_placeholder 的默认占位词。与原 workflow.js checkOutput/_checkWorkflow 逐字一致——
|
|
15
|
+
// 改动会破坏存量扫描文档的占位判定,属契约级常量。
|
|
16
|
+
const DEFAULT_PLACEHOLDER_PATTERNS = [
|
|
17
|
+
'待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/** 内容非空(trim 后有字符)。用于 no_empty_files。 */
|
|
21
|
+
export function contentNonEmpty(content) {
|
|
22
|
+
return typeof content === 'string' && content.trim().length > 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 行数(split('\n'))。用于 min_lines 的 detail 文案与判定。 */
|
|
26
|
+
export function lineCount(content) {
|
|
27
|
+
return typeof content === 'string' ? content.split('\n').length : 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 行数 ≥ min。用于 min_lines 判定。 */
|
|
31
|
+
export function meetsMinLines(content, min) {
|
|
32
|
+
return lineCount(content) >= min
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 返回缺失的章节(sections 中未以 `## <section>` 子串出现的)。
|
|
37
|
+
* 用于 contains_sections——判定与原 workflow.js 逐字一致(`## ${s}` 前缀子串匹配)。
|
|
38
|
+
*/
|
|
39
|
+
export function missingSections(content, sections) {
|
|
40
|
+
if (typeof content !== 'string') return sections.slice()
|
|
41
|
+
return sections.filter(s => !content.includes(`## ${s}`))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 是否含全部章节(missingSections 为空的语法糖)。 */
|
|
45
|
+
export function hasAllSections(content, sections) {
|
|
46
|
+
return missingSections(content, sections).length === 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 返回命中的占位词(patterns 中匹配"独立成行"的)。
|
|
51
|
+
* 用于 no_placeholder——正则 `^\s*[-*]?\s*<p>\s*$`(m flag)与原 workflow.js 逐字一致。
|
|
52
|
+
* patterns 为空/未传时用 DEFAULT_PLACEHOLDER_PATTERNS(等价原 `check.patterns || [defaults]`)。
|
|
53
|
+
*/
|
|
54
|
+
export function placeholderLineMatches(content, patterns) {
|
|
55
|
+
if (typeof content !== 'string') return []
|
|
56
|
+
const pats = (patterns && patterns.length > 0) ? patterns : DEFAULT_PLACEHOLDER_PATTERNS
|
|
57
|
+
return pats.filter(p => new RegExp(`^\\s*[-*]?\\s*${p}\\s*$`, 'm').test(content))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 无占位词命中(placeholderLineMatches 为空的语法糖)。 */
|
|
61
|
+
export function hasNoPlaceholderLine(content, patterns) {
|
|
62
|
+
return placeholderLineMatches(content, patterns).length === 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { DEFAULT_PLACEHOLDER_PATTERNS }
|
package/src/db.js
CHANGED
|
@@ -2,6 +2,11 @@ import initSqlJs from 'sql.js';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
3
3
|
import { basename, dirname, join } from 'path';
|
|
4
4
|
|
|
5
|
+
// DB schema 版本(与 project.schema_version DEFAULT 对齐)。_createSchema 改动(加表/列/migration)时 bump,
|
|
6
|
+
// 触发 .schema-version 戳失效 → 下次 init 重跑 _createSchema + _save(W4-H:高频读路径 gate/derive/status
|
|
7
|
+
// 每次 new ProgressManager 都过 init,原无条件 _createSchema+_save 致每次整库 export+三段原子写)。
|
|
8
|
+
const DB_SCHEMA_VERSION = 3;
|
|
9
|
+
|
|
5
10
|
export class DB {
|
|
6
11
|
constructor(dbPath) {
|
|
7
12
|
this.dbPath = dbPath;
|
|
@@ -25,11 +30,21 @@ export class DB {
|
|
|
25
30
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
26
31
|
this.db.run('PRAGMA synchronous = NORMAL');
|
|
27
32
|
|
|
28
|
-
// 5.
|
|
29
|
-
|
|
33
|
+
// 5. 创建表结构(仅当 schema 版本戳不匹配时——W4-H):
|
|
34
|
+
// schema 已最新则只 load 不写,省掉 _createSchema + _save 的整库 export + 三段原子写。
|
|
35
|
+
// 戳在但 db 缺表属"手动改 db"边角(SillySpec 不支持),_loadDatabase 的损坏检测兜底正常路径。
|
|
36
|
+
const versionPath = `${this.dbPath}.schema-version`;
|
|
37
|
+
let schemaCurrent = false;
|
|
38
|
+
try {
|
|
39
|
+
schemaCurrent = existsSync(versionPath)
|
|
40
|
+
&& readFileSync(versionPath, 'utf8').trim() === String(DB_SCHEMA_VERSION);
|
|
41
|
+
} catch { /* 戳读取失败保守重跑 schema */ }
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
if (!schemaCurrent) {
|
|
44
|
+
this._createSchema();
|
|
45
|
+
this._save();
|
|
46
|
+
try { writeFileSync(versionPath, String(DB_SCHEMA_VERSION)); } catch { /* 戳写入失败不阻断,下次重跑 */ }
|
|
47
|
+
}
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
close() {
|
|
@@ -43,15 +58,20 @@ export class DB {
|
|
|
43
58
|
transaction(fn) {
|
|
44
59
|
if (!this.db) throw new Error('DB not initialized');
|
|
45
60
|
this.db.run('BEGIN');
|
|
61
|
+
let result;
|
|
46
62
|
try {
|
|
47
|
-
|
|
63
|
+
result = fn(this.db);
|
|
48
64
|
this.db.run('COMMIT');
|
|
49
|
-
this._save();
|
|
50
|
-
return result;
|
|
51
65
|
} catch (err) {
|
|
52
|
-
|
|
66
|
+
// fn 抛错或 COMMIT 失败:事务级回滚(此时事务确实活跃,ROLLBACK 有效)
|
|
67
|
+
try { this.db.run('ROLLBACK'); } catch { /* 事务可能已被 fn 内的错自动回滚 */ }
|
|
53
68
|
throw err;
|
|
54
69
|
}
|
|
70
|
+
// COMMIT 成功后才持久化。_save 失败时抛原生错(磁盘满/EPERM),不再被 catch 里
|
|
71
|
+
// 无效的 ROLLBACK 二级异常("no transaction active")掩盖——内存库已 commit,
|
|
72
|
+
// 下次 _save / close() 会落盘,调用方拿到的是准确的持久化错误而非被替换的误报。
|
|
73
|
+
this._save();
|
|
74
|
+
return result;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
/** 获取底层 db 对象(供 progress.js 直接使用) */
|
|
@@ -258,12 +258,19 @@ function queryDbFirstCell(cwd, sql) {
|
|
|
258
258
|
"if(!fs.existsSync(dbPath))process.exit(0);" +
|
|
259
259
|
"import(lib).then(async m=>{const SQL=await m.default();" +
|
|
260
260
|
"const db=new SQL.Database(fs.readFileSync(dbPath));const r=db.exec(sql);" +
|
|
261
|
-
"if(r.length&&r[0].values.length)process.stdout.write(String(r[0].values[0][0]??''));db.close()}).catch(()
|
|
261
|
+
"if(r.length&&r[0].values.length)process.stdout.write(String(r[0].values[0][0]??''));db.close()}).catch(e=>{process.stderr.write('hook db query failed: '+String(e&&e.message||e));process.exit(1)});"
|
|
262
262
|
try {
|
|
263
263
|
return execFileSync(process.execPath, ['-e', script], {
|
|
264
264
|
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
265
|
}).trim() || null
|
|
266
|
-
} catch {
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// DB 查询失败(db 损坏 / sql.js resolve 失败 / 子进程超时):降级返回 null,调用方
|
|
268
|
+
//(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因,
|
|
269
|
+
// 而非完全静默(子进程把诊断写到了 stderr,execFileSync 抛错时挂在 e.stderr 上)。
|
|
270
|
+
const detail = (e && e.stderr ? String(e.stderr).trim() : '') || (e && e.message) || 'timeout/crash'
|
|
271
|
+
console.warn('⚠️ sillyspec.db 查询失败,hook 降级 fail-closed: ' + detail)
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
267
274
|
}
|
|
268
275
|
|
|
269
276
|
export { queryDbFirstCell as _queryDbFirstCellForTest };
|