sillyspec 3.28.3 → 3.28.4
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +7 -2
- package/src/change-risk-profile.js +77 -6
- package/src/config-cat.js +12 -1
- package/src/constants.js +3 -1
- package/src/docs-check.js +34 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +181 -6
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +16 -0
- package/src/progress.js +62 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +30 -9
- package/src/run/complete-handlers.js +152 -6
- package/src/run/complete.js +45 -0
- package/src/run/gates.js +94 -6
- package/src/run/prompt.js +96 -5
- package/src/run/shared.js +74 -6
- package/src/run/stage.js +35 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review.js +3 -0
- package/src/stages/archive.js +4 -4
- package/src/stages/brainstorm.js +2 -8
- package/src/stages/doctor.js +41 -515
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +5 -26
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +10 -6
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +25 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +257 -19
- package/src/verify-probes.js +142 -13
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +51 -13
- package/src/worktree.js +5 -1
package/src/index.js
CHANGED
|
@@ -98,11 +98,13 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
98
98
|
单 task review.json 命令式写入:verdict/notes 你给,base/head/changedFiles/diffPaths CLI 代算
|
|
99
99
|
sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
|
|
100
100
|
sillyspec design-init --change <name> [--force] 从 decisions.md 生成 design.md 十三章节骨架(决策追踪表预填;已存在不覆盖)
|
|
101
|
+
sillyspec fourpiece-init --change <name> 生成 proposal/requirements/decisions 骨架(frontmatter+章节+模板占位;已存在不覆盖)
|
|
101
102
|
sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
|
|
102
103
|
sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
|
|
103
104
|
sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
|
|
104
105
|
sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
|
|
105
106
|
sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
|
|
107
|
+
sillyspec module-docs-sync --change <name> [--note ...] diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等)
|
|
106
108
|
sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
|
|
107
109
|
sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json] 拍变更前端点基线(幂等不覆盖;worktree 内跑自动锚主仓;归档 delta 端点增删 before 侧)
|
|
108
110
|
sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
|
|
@@ -117,6 +119,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
117
119
|
(无子命令) 跑诊断,列出问题
|
|
118
120
|
--align-execute-progress [--change <名>] [--confirm] 基于 plan.md 对齐 execute 派生戳(默认 dry-run)
|
|
119
121
|
--cleanup-remnant [--confirm] 删除 0 字节空占位 db(默认 dry-run,仅 --confirm 落盘)
|
|
122
|
+
--gc-unstamped-runs [--confirm] 回收已归档变更留下的无 change 戳 execute-runs(默认 dry-run)
|
|
120
123
|
--dump-db --path <db 路径> dump 指定 db 内容到文件(取证用)
|
|
121
124
|
--json 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
|
|
122
125
|
sillyspec modules <rebuild | status | migrate>
|
|
@@ -942,8 +945,51 @@ async function main() {
|
|
|
942
945
|
writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
|
|
943
946
|
console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
|
|
944
947
|
}
|
|
945
|
-
writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init
|
|
948
|
+
writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
|
|
949
|
+
// 2026-09-08-ir-verify-facts R-02:已存在 md 缺证据账/回执槽段时仅追加骨架(幂等二跑
|
|
950
|
+
// 零改动);task 行按 verify-required-evidence.json 预填(存在时)。
|
|
951
|
+
if (existsSync(vpReportPath)) {
|
|
952
|
+
let vpReqItems = []
|
|
953
|
+
try {
|
|
954
|
+
const vpReqPath = join(vpSpecBase, 'changes', vpChange, 'verify-required-evidence.json')
|
|
955
|
+
if (existsSync(vpReqPath)) {
|
|
956
|
+
const vpReq = JSON.parse(readFileSync(vpReqPath, 'utf8'))
|
|
957
|
+
vpReqItems = Array.isArray(vpReq.items) ? vpReq.items.map(it => ({ task: it.task, evidence: it.evidence || [] })) : []
|
|
958
|
+
}
|
|
959
|
+
} catch { /* 读取失败 → 空预填,不阻断 */ }
|
|
960
|
+
const { backfillMissingEvidenceSlots } = await import('./verify-probes.js')
|
|
961
|
+
const vpAdded = backfillMissingEvidenceSlots(vpReportPath, vpReqItems)
|
|
962
|
+
if (vpAdded.added.length > 0) {
|
|
963
|
+
console.log(`📄 已补齐缺失槽段(不触碰既有正文): ${vpAdded.added.join('、')}`)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
case 'module-docs-sync': {
|
|
970
|
+
// 模块文档 sidecar 同步命令化(2026-09-09 刀批):diff 归属模块 → sidecar 追加变更索引行 +
|
|
971
|
+
// 卡 updated_at 戳——机械部分 CLI 代算,卡片正文语义更新仍归 agent/归档流程。幂等。
|
|
972
|
+
const mdsChangeIdx = args.indexOf('--change');
|
|
973
|
+
const mdsChange = mdsChangeIdx >= 0 && args[mdsChangeIdx + 1] ? args[mdsChangeIdx + 1] : null;
|
|
974
|
+
const mdsNoteIdx = args.indexOf('--note');
|
|
975
|
+
const mdsNote = mdsNoteIdx >= 0 && args[mdsNoteIdx + 1] ? args[mdsNoteIdx + 1] : null;
|
|
976
|
+
if (!mdsChange) {
|
|
977
|
+
console.error('用法: sillyspec module-docs-sync --change <name> [--note "一句话"] [--json]\n diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等;卡片正文语义更新仍归 agent)');
|
|
978
|
+
process.exit(2);
|
|
979
|
+
}
|
|
980
|
+
const { syncModuleDocSidecars } = await import('./module-impact.js');
|
|
981
|
+
const mdsResult = syncModuleDocSidecars({ cwd: dir, changeName: mdsChange, note: mdsNote, specDir });
|
|
982
|
+
if (json) {
|
|
983
|
+
console.log(JSON.stringify({ command: 'module-docs-sync', change: mdsChange, ...mdsResult }, null, 2));
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
if (mdsResult.reason) console.log(mdsResult.reason);
|
|
987
|
+
if (mdsResult.synced.length > 0) {
|
|
988
|
+
console.log(String.fromCharCode(9989) + ' sidecar 已同步 ' + mdsResult.synced.length + ' 个模块: ' + mdsResult.synced.join(', '));
|
|
989
|
+
console.log(' 卡 updated_at 已戳;卡片正文语义更新(如需要)仍归归档流程/agent。');
|
|
946
990
|
}
|
|
991
|
+
if (mdsResult.skipped.length > 0) console.log(String.fromCharCode(8505) + ' 已含本变更行跳过: ' + mdsResult.skipped.join(', '));
|
|
992
|
+
if (mdsResult.unmatched.length > 0) console.log(String.fromCharCode(9888) + ' 未匹配模块的文件 ' + mdsResult.unmatched.length + ' 个(不入 sidecar): ' + mdsResult.unmatched.slice(0, 5).join(', '));
|
|
947
993
|
break;
|
|
948
994
|
}
|
|
949
995
|
case 'plan-adopt-waves': {
|
|
@@ -1313,6 +1359,94 @@ async function main() {
|
|
|
1313
1359
|
console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
|
|
1314
1360
|
break;
|
|
1315
1361
|
}
|
|
1362
|
+
case 'fourpiece-init': {
|
|
1363
|
+
// 四件套骨架预生成(2026-09-09 ql-20260909-003,轮次经济学 §3.2):brainstorm 期 agent
|
|
1364
|
+
// 手写 proposal/requirements/decisions 的 frontmatter/章节骨架是格式返工根源——本命令
|
|
1365
|
+
// 逐文件生成骨架(已存在不覆盖),agent 只填语义。与 design-init 同族(幂等)。
|
|
1366
|
+
const fpChangeIdx = args.indexOf('--change');
|
|
1367
|
+
const fpChange = fpChangeIdx >= 0 && args[fpChangeIdx + 1] ? args[fpChangeIdx + 1] : null;
|
|
1368
|
+
if (!fpChange) {
|
|
1369
|
+
console.error('用法: sillyspec fourpiece-init --change <name> [--json] [--spec-dir <path>]\n 生成 proposal.md/requirements.md/decisions.md 骨架(frontmatter+章节标题+模板占位;已存在不覆盖;design.md 归 design-init)');
|
|
1370
|
+
process.exit(2);
|
|
1371
|
+
}
|
|
1372
|
+
const fpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
1373
|
+
const fpDir = join(fpSpecBase, 'changes', fpChange);
|
|
1374
|
+
mkdirSync(fpDir, { recursive: true });
|
|
1375
|
+
const fpNow = new Date();
|
|
1376
|
+
const fpDate = fpNow.toISOString().slice(0, 10);
|
|
1377
|
+
const fpStamp = fpNow.toISOString().slice(0, 19).replace('T', ' ');
|
|
1378
|
+
const generated = [];
|
|
1379
|
+
const mk = (fname, body) => {
|
|
1380
|
+
const p = join(fpDir, fname);
|
|
1381
|
+
if (existsSync(p)) { console.log(`ℹ️ ${fname} 已存在,不覆盖`); return; }
|
|
1382
|
+
writeFileSync(p, body);
|
|
1383
|
+
generated.push(fname);
|
|
1384
|
+
console.log(`✅ 已生成 ${fname} 骨架: ${p}`);
|
|
1385
|
+
};
|
|
1386
|
+
mk('proposal.md', `---
|
|
1387
|
+
author: qinyi
|
|
1388
|
+
created_at: ${fpStamp}
|
|
1389
|
+
---
|
|
1390
|
+
# 提案书(Proposal)
|
|
1391
|
+
|
|
1392
|
+
## 动机
|
|
1393
|
+
<!--TODO: 为什么做、解决什么核心问题-->
|
|
1394
|
+
|
|
1395
|
+
## 关键问题
|
|
1396
|
+
<!--TODO: 为什么现有方案不够(2-3 个痛点)-->
|
|
1397
|
+
|
|
1398
|
+
## 变更范围
|
|
1399
|
+
<!--TODO: 本次做什么-->
|
|
1400
|
+
|
|
1401
|
+
## 不在范围内(显式清单)
|
|
1402
|
+
- <!--TODO: 不做 X-->
|
|
1403
|
+
|
|
1404
|
+
## 成功标准(可验证)
|
|
1405
|
+
- <!--TODO: 可验证条目-->
|
|
1406
|
+
`);
|
|
1407
|
+
mk('requirements.md', `---
|
|
1408
|
+
author: qinyi
|
|
1409
|
+
created_at: ${fpStamp}
|
|
1410
|
+
---
|
|
1411
|
+
# 需求规格(Requirements)
|
|
1412
|
+
|
|
1413
|
+
## 角色
|
|
1414
|
+
| 角色 | 说明 |
|
|
1415
|
+
|---|---|
|
|
1416
|
+
| <!--TODO--> | <!--TODO--> |
|
|
1417
|
+
|
|
1418
|
+
## 功能需求
|
|
1419
|
+
|
|
1420
|
+
### FR-01: <!--TODO-->
|
|
1421
|
+
Given <!--TODO-->
|
|
1422
|
+
When <!--TODO-->
|
|
1423
|
+
Then <!--TODO-->
|
|
1424
|
+
|
|
1425
|
+
## 非功能需求
|
|
1426
|
+
- 兼容性:<!--TODO-->
|
|
1427
|
+
|
|
1428
|
+
## 决策覆盖矩阵(如存在 decisions.md)
|
|
1429
|
+
| 决策 ID | 覆盖的 FR | 说明 |
|
|
1430
|
+
|---|---|---|
|
|
1431
|
+
`);
|
|
1432
|
+
mk('decisions.md', `---
|
|
1433
|
+
author: qinyi
|
|
1434
|
+
created_at: ${fpStamp}
|
|
1435
|
+
change: ${fpChange}
|
|
1436
|
+
---
|
|
1437
|
+
|
|
1438
|
+
# 决策记录(Decisions)
|
|
1439
|
+
|
|
1440
|
+
<!-- 增量落盘:每解决一个有实现影响的问题当场追加一条(格式见 brainstorm Step 3 模板);幂等按 D-xxx@vN 判重 -->
|
|
1441
|
+
`);
|
|
1442
|
+
if (json) {
|
|
1443
|
+
console.log(JSON.stringify({ command: 'fourpiece-init', change: fpChange, generated }, null, 2));
|
|
1444
|
+
} else if (generated.length > 0) {
|
|
1445
|
+
console.log(`
|
|
1446
|
+
${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语义内容(骨架勿手删章节);design.md 用 sillyspec design-init。`);
|
|
1447
|
+
}
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1316
1450
|
case 'design-init': {
|
|
1317
1451
|
// design.md 十三章节骨架生成(change: 2026-09-07-ir-stage-p3c,task-02,FR-03):agent 此前
|
|
1318
1452
|
// 从零手写整份设计文档,章节漏项到 stage-contract/plan 门禁才暴露。本命令从 decisions.md
|
|
@@ -1810,9 +1944,10 @@ async function main() {
|
|
|
1810
1944
|
// slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
|
|
1811
1945
|
case 'doctor': {
|
|
1812
1946
|
const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir);
|
|
1813
|
-
// 执行流:--cleanup-remnant / --cleanup-ghosts / --dump-db(结构化诊断之外的修复/取证动作)
|
|
1947
|
+
// 执行流:--cleanup-remnant / --cleanup-ghosts / --gc-unstamped-runs / --dump-db(结构化诊断之外的修复/取证动作)
|
|
1814
1948
|
const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
|
|
1815
1949
|
const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
|
|
1950
|
+
const gcUnstampedRuns = filteredArgs.includes('--gc-unstamped-runs');
|
|
1816
1951
|
const dumpDbFlag = filteredArgs.includes('--dump-db');
|
|
1817
1952
|
const doctorConfirm = filteredArgs.includes('--confirm');
|
|
1818
1953
|
const pathIdx = filteredArgs.indexOf('--path');
|
|
@@ -1918,6 +2053,29 @@ async function main() {
|
|
|
1918
2053
|
process.exitCode = r.errors.length > 0 ? 1 : 0;
|
|
1919
2054
|
break;
|
|
1920
2055
|
}
|
|
2056
|
+
if (gcUnstampedRuns) {
|
|
2057
|
+
// ql-20260908-006-5f04:无戳 execute-runs 一次性清扫。默认 dry-run,--confirm 才删。
|
|
2058
|
+
// 不进 archive 热路径(pruneArchivedChangeRuntime 故意不猜无戳 run)。
|
|
2059
|
+
const { gcUnstampedExecuteRuns } = await import('./doctor-diagnostics.js');
|
|
2060
|
+
const r = await gcUnstampedExecuteRuns({ cwd: doctorEffectiveDir, specDir, confirm: doctorConfirm });
|
|
2061
|
+
if (json) {
|
|
2062
|
+
console.log(JSON.stringify(r, null, 2));
|
|
2063
|
+
} else {
|
|
2064
|
+
const verb = doctorConfirm ? '已删除' : '待删除(dry-run)';
|
|
2065
|
+
console.log(`🧹 无戳 execute-runs(仅已归档变更、交叉核验通过)${verb}:${r.count} 个 [runtime: ${r.runtime_root || '(无)'}]`);
|
|
2066
|
+
for (const c of r.candidates || []) {
|
|
2067
|
+
console.log(` ${doctorConfirm ? '✅' : '-'} ${c.runId} ← ${c.owner}(via ${c.via})`);
|
|
2068
|
+
}
|
|
2069
|
+
for (const s of r.skipped || []) console.log(` ⏭️ ${s.runId}(${s.reason})`);
|
|
2070
|
+
for (const e of r.errors || []) console.log(` ❌ ${e.runId || ''}: ${e.error}`);
|
|
2071
|
+
if (!doctorConfirm && r.count > 0) {
|
|
2072
|
+
console.log(`\n加 --confirm 执行删除(只删上面列出的无戳 run,有 change 戳 / 命中活跃变更 / 归属不明一律不动)。`);
|
|
2073
|
+
}
|
|
2074
|
+
if (r.reason) console.log(`ℹ️ ${r.reason}`);
|
|
2075
|
+
}
|
|
2076
|
+
process.exitCode = (r.errors && r.errors.length > 0) ? 1 : 0;
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
1921
2079
|
if (dumpDbFlag) {
|
|
1922
2080
|
if (!dbPath) { console.error('❌ --dump-db 需要 --path <db 路径>'); process.exitCode = 2; break; }
|
|
1923
2081
|
const { dumpDb } = await import('./doctor-diagnostics.js');
|
|
@@ -1944,9 +2102,21 @@ async function main() {
|
|
|
1944
2102
|
process.exitCode = output.overall_status === 'pass' ? 0 : 1;
|
|
1945
2103
|
break;
|
|
1946
2104
|
}
|
|
1947
|
-
//
|
|
1948
|
-
|
|
1949
|
-
|
|
2105
|
+
// 2026-09-09-doctor-noai:--status 拦截(审查 B-03:等价只读渲染——阶段进度视图,
|
|
2106
|
+
// 零副作用);非 --json 其余形态改道直跑诊断 + renderDoctorSummary(不走
|
|
2107
|
+
// runCommand/initChange/不刷 lastActive——只读零副作用承诺保真)。
|
|
2108
|
+
if (filteredArgs.includes('--status')) {
|
|
2109
|
+
// 等价只读渲染:复用 progress show(cwd 无活跃变更时提示无——与原只读短路视图同语义级)
|
|
2110
|
+
const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
|
|
2111
|
+
pm.show(doctorEffectiveDir);
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
{
|
|
2115
|
+
const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('./doctor-diagnostics.js')
|
|
2116
|
+
const diag = await runDoctorDiagnostics({ cwd: doctorEffectiveDir })
|
|
2117
|
+
console.log(renderDoctorSummary(diag))
|
|
2118
|
+
try { writeDoctorDiagnosis(diag, resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec')) } catch { /* fail-soft */ }
|
|
2119
|
+
}
|
|
1950
2120
|
break;
|
|
1951
2121
|
}
|
|
1952
2122
|
case 'scan': {
|
|
@@ -2920,7 +3090,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
2920
3090
|
|
|
2921
3091
|
const { cmdTaskcard } = await import('./taskcard.js');
|
|
2922
3092
|
try {
|
|
2923
|
-
|
|
3093
|
+
// 坑 platform-sync-progress-rollback-and-db-corruption 坑4:taskcard 此前只走
|
|
3094
|
+
// resolveSpecDir(祖先遍历本地 .sillyspec),不感知平台接管指针——平台模式下
|
|
3095
|
+
// 仓库根裸跑报「变更目录不存在」,需显式 --spec-dir(与 run/plan 系列不一致)。
|
|
3096
|
+
// 对齐 endpoints 等命令:先 resolvePlatformSpecDir(指针 fail-closed 语义同源)。
|
|
3097
|
+
const tcPlatformSpecDir = resolvePlatformSpecDir(dir, specDir) || specDir;
|
|
3098
|
+
const result = cmdTaskcard(tcName, { cwd: dir, specDir: tcPlatformSpecDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
|
|
2924
3099
|
for (const f of result.created) console.log(`✅ 已生成: ${f}`);
|
|
2925
3100
|
for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
|
|
2926
3101
|
if (result.created.length > 0) {
|
package/src/module-impact.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* diff 源:resolveVerifyChangedFiles(worktree-aware,与 task-review 同源);
|
|
11
11
|
* module-map:.sillyspec/docs/<p>/modules/_module-map.yaml 的 modules.<id>.paths
|
|
12
12
|
* (目录条目以 / 结尾按前缀匹配,与模块上下文索引读侧口径一致)。
|
|
13
|
+
* 声明源(plan --done 首版,2026-09-08 刀②):design 文件变更清单——代码未写、无 diff,
|
|
14
|
+
* 归属分类同一套 module-map 前缀匹配,仅输入来源不同。
|
|
13
15
|
*/
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
15
17
|
import { join } from 'path'
|
|
16
18
|
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
17
19
|
import { resolveSpecDir } from './run/shared.js'
|
|
@@ -68,11 +70,67 @@ function classifyFile(posixPath, modulePaths) {
|
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* 生成 module-impact.md 骨架。
|
|
71
|
-
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
73
|
+
* @param {{ cwd: string, changeName: string, specDir?: string|null, sourceFiles?: string[]|null, origin?: string|null }} opts
|
|
74
|
+
* sourceFiles:声明来源(plan --done 首版用 design 文件变更清单——此时代码未写、无 diff 可取);
|
|
75
|
+
* 缺省走 diff 来源(resolveVerifyChangedFiles,archive/execute 后的真实改动)。
|
|
76
|
+
* origin:骨架首行的生成方说明(区分 plan 首版与 CLI 命令生成,追溯来源用)。
|
|
72
77
|
* @returns {{ markdown: string, matchedCount: number, unmatchedCount: number }|null}
|
|
73
|
-
* 无 module-map /
|
|
78
|
+
* 无 module-map / 无可归类文件 → null(无可归类输入,agent 全手写)
|
|
74
79
|
*/
|
|
75
|
-
|
|
80
|
+
/**
|
|
81
|
+
* 模块文档 sidecar 同步命令化(2026-09-09 刀批,轮次经济学 §3.1 模块文档同步项):
|
|
82
|
+
* quick/archive 两处 prompt 让 agent 手写 sidecar 追加行——机械部分(变更归属模块、
|
|
83
|
+
* sidecar 追加、卡 updated_at 戳)CLI 代算,agent 只留卡片正文语义更新。
|
|
84
|
+
* 幂等:sidecar 已含 `- <change> |` 行则跳过该模块;卡不存在只建 sidecar 不建卡(卡内容是语义)。
|
|
85
|
+
* @returns {{ synced: string[], unmatched: string[], skipped: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null }) {
|
|
88
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
89
|
+
// 复用 generateModuleImpactSkeleton 的 map 发现 + 归类(diff 来源)
|
|
90
|
+
const skel = generateModuleImpactSkeleton({ cwd, changeName, specDir })
|
|
91
|
+
if (!skel) return { synced: [], unmatched: [], skipped: [], reason: '无 module-map 或无 diff 可归类' }
|
|
92
|
+
const modulesDir = findModulesDir(specBase)
|
|
93
|
+
if (!modulesDir) return { synced: [], unmatched: [], skipped: [], reason: 'modules 目录不存在' }
|
|
94
|
+
// 从骨架 markdown 反解归类结果(命中模块集合 + 未匹配清单)
|
|
95
|
+
const hitModules = [...skel.markdown.matchAll(/^\| (\S+) \| `([^`]+)`/gm)].map(m => m[1])
|
|
96
|
+
const unmatched = [...skel.markdown.matchAll(/^- `([^`]+)` /gm)].map(m => m[1])
|
|
97
|
+
const synced = []
|
|
98
|
+
const skipped = []
|
|
99
|
+
for (const mod of [...new Set(hitModules)]) {
|
|
100
|
+
const sidecarPath = join(modulesDir, `${mod}.changelog.md`)
|
|
101
|
+
let sidecar = ''
|
|
102
|
+
try { sidecar = readFileSync(sidecarPath, 'utf8') } catch { sidecar = '' }
|
|
103
|
+
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(mod); continue }
|
|
104
|
+
const NL = String.fromCharCode(10)
|
|
105
|
+
const line = `- ${changeName} | ${note || '(见该变更 quicklog/归档)'}`
|
|
106
|
+
const base = sidecar
|
|
107
|
+
? sidecar.replace(/\n*$/, NL)
|
|
108
|
+
: `# ${mod} 变更索引` + NL + NL
|
|
109
|
+
writeFileSync(sidecarPath, base + line + NL)
|
|
110
|
+
// 卡 updated_at 戳(存在才戳)
|
|
111
|
+
const cardPath = join(modulesDir, `${mod}.md`)
|
|
112
|
+
try {
|
|
113
|
+
const card = readFileSync(cardPath, 'utf8')
|
|
114
|
+
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString().slice(0, 19) + '+08:00'}`)
|
|
115
|
+
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
116
|
+
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
117
|
+
synced.push(mod)
|
|
118
|
+
}
|
|
119
|
+
return { synced, unmatched, skipped }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findModulesDir(specBase) {
|
|
123
|
+
try {
|
|
124
|
+
for (const p of readdirSync(join(specBase, 'docs'), { withFileTypes: true })) {
|
|
125
|
+
if (!p.isDirectory()) continue
|
|
126
|
+
const cand = join(specBase, 'docs', p.name, 'modules')
|
|
127
|
+
if (existsSync(join(cand, '_module-map.yaml'))) return cand
|
|
128
|
+
}
|
|
129
|
+
} catch { /* 无 docs/ */ }
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null, sourceFiles = null, origin = null }) {
|
|
76
134
|
const specBase = resolveSpecDir(cwd, { specDir })
|
|
77
135
|
|
|
78
136
|
// 找 _module-map.yaml(docs/<p>/modules/;多项目取首个含 modules 索引的)
|
|
@@ -92,15 +150,20 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
92
150
|
const modulePaths = parseModuleMapPaths(readFileSync(moduleMapPath, 'utf8'))
|
|
93
151
|
if (modulePaths.size === 0) return null
|
|
94
152
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
let sourceFileList
|
|
154
|
+
if (sourceFiles) {
|
|
155
|
+
sourceFileList = sourceFiles.map(f => f.split('\\').join('/')).filter(f => !f.startsWith('.sillyspec/'))
|
|
156
|
+
} else {
|
|
157
|
+
// includeWorkingTree 同 verify module 子集(坑 module-subset-zero-hit-uncommitted):
|
|
158
|
+
// worktree 未提交改动也计入模块影响面(module-impact 按实际 diff 归属模块)
|
|
159
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || []
|
|
160
|
+
sourceFileList = diffFiles.map(f => f.split('\\').join('/')).filter(f => !f.startsWith('.sillyspec/'))
|
|
161
|
+
}
|
|
162
|
+
if (sourceFileList.length === 0) return null
|
|
100
163
|
|
|
101
164
|
const byModule = new Map()
|
|
102
165
|
const unmatched = []
|
|
103
|
-
for (const f of
|
|
166
|
+
for (const f of sourceFileList) {
|
|
104
167
|
const mod = classifyFile(f, modulePaths)
|
|
105
168
|
if (mod) {
|
|
106
169
|
if (!byModule.has(mod)) byModule.set(mod, [])
|
|
@@ -111,7 +174,9 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
111
174
|
}
|
|
112
175
|
|
|
113
176
|
const L = [
|
|
114
|
-
|
|
177
|
+
origin
|
|
178
|
+
? `# 模块影响分析(骨架由 ${origin} 生成)`
|
|
179
|
+
: '# 模块影响分析(骨架由 `sillyspec module-impact --change <变更名>` 生成)',
|
|
115
180
|
'',
|
|
116
181
|
'> 文件×模块归属由 CLI 按 _module-map.yaml paths 前缀匹配预填;',
|
|
117
182
|
'> **影响类型**(逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增)与 review 标记是语义判断,',
|
|
@@ -162,5 +227,5 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
162
227
|
L.push('规则:execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。')
|
|
163
228
|
L.push('')
|
|
164
229
|
|
|
165
|
-
return { markdown: L.join('\n'), matchedCount:
|
|
230
|
+
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length }
|
|
166
231
|
}
|
package/src/plan-adopt-waves.js
CHANGED
|
@@ -60,12 +60,17 @@ function updateSummaryWaveColumn(lines, waveOf) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* @param {{ changeDir: string, dryRun?: boolean }} opts
|
|
63
|
+
* @param {{ changeDir: string, dryRun?: boolean, mode?: 'write'|'proposal' }} opts
|
|
64
|
+
* mode='proposal'(2026-09-09-plan-derived):只读产拓扑布局提案——渲染 Wave 段草稿与
|
|
65
|
+
* W 列同步结果但不落盘,供 postcheck「提案-验证-落盘」自动修复(FR-01)验证后决断写否。
|
|
64
66
|
* @returns {{ ok: boolean, error?: string, waves?: string[][], planPath: string, dryRun: boolean,
|
|
65
67
|
* waveBlock?: string[], tableRowsUpdated?: number, tableRowsSkipped?: number,
|
|
68
|
+
* planMdDraft?: string, rewritten?: boolean,
|
|
66
69
|
* postcheck?: { errors: string[], warnings: string[] } }}
|
|
70
|
+
* planMdDraft:proposal 档下的完整 plan.md 草稿文本;rewritten:与原文比较是否有实际变更
|
|
71
|
+
* (幂等重排零变更 → false,调用方静默);write 档沿用既有 postcheck 字段。
|
|
67
72
|
*/
|
|
68
|
-
export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
73
|
+
export function adoptPlanWaves({ changeDir, dryRun = false, mode = 'write' }) {
|
|
69
74
|
const planPath = join(changeDir, 'plan.md')
|
|
70
75
|
if (!existsSync(planPath)) return { ok: false, error: `plan.md 不存在: ${planPath}`, planPath, dryRun }
|
|
71
76
|
const tasksDir = join(changeDir, 'tasks')
|
|
@@ -108,9 +113,11 @@ export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
|
108
113
|
|
|
109
114
|
const { updated, skipped } = updateSummaryWaveColumn(lines, waveOf)
|
|
110
115
|
const content = lines.join('\n').replace(/\n*$/, '\n')
|
|
116
|
+
const rewritten = content !== original.replace(/\r\n/g, '\n').replace(/\n*$/, '\n')
|
|
111
117
|
|
|
112
|
-
if (dryRun) {
|
|
113
|
-
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped
|
|
118
|
+
if (dryRun || mode === 'proposal') {
|
|
119
|
+
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
120
|
+
planMdDraft: content, rewritten }
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
writeFileSync(planPath, content, 'utf8')
|
|
@@ -122,5 +129,6 @@ export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
|
122
129
|
} catch (e) {
|
|
123
130
|
postcheck = { errors: [`复跑校验异常: ${e?.message || e}`], warnings: [] }
|
|
124
131
|
}
|
|
125
|
-
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
132
|
+
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
133
|
+
rewritten, postcheck }
|
|
126
134
|
}
|
|
@@ -46,6 +46,22 @@ export class ChangeRegistry {
|
|
|
46
46
|
* @param {string} changeName - 变更名
|
|
47
47
|
* @returns {{ current_stage: string, status: string }|null} 无该行返回 null(未注册目录桩)
|
|
48
48
|
*/
|
|
49
|
+
/**
|
|
50
|
+
* 读某变更某阶段的 completed_at(2026-09-08-ir-verify-facts FR-02:verifyStartAt 基准——
|
|
51
|
+
* evidence 分类核验的 mtime 窗口下界,取 execute 行 completed_at)。只读不抛:无行/读失败
|
|
52
|
+
* 返回 null(调用方走 R-05 fallback,与 getChangeCreatedAt 同容错风格)。
|
|
53
|
+
*/
|
|
54
|
+
getStageCompletedAt(cwd, changeName, stage) {
|
|
55
|
+
try {
|
|
56
|
+
const db = this.pm._ensureDB(cwd);
|
|
57
|
+
const row = db.getDb().prepare(
|
|
58
|
+
`SELECT s.completed_at FROM stages s JOIN changes c ON s.change_id = c.id
|
|
59
|
+
WHERE c.name = ? AND s.stage = ? ORDER BY s.completed_at DESC LIMIT 1`
|
|
60
|
+
).get(changeName, stage);
|
|
61
|
+
return row && row.completed_at ? row.completed_at : null;
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
getChangeStage(cwd, changeName) {
|
|
50
66
|
const db = this.pm._ensureDB(cwd);
|
|
51
67
|
const sqlDb = db.getDb();
|
package/src/progress.js
CHANGED
|
@@ -25,13 +25,19 @@ import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME
|
|
|
25
25
|
// resolveSpecDir 单一真相源在 src/run/shared.js(含 home 拒绝守卫,坑 cwd-correction-home-collision
|
|
26
26
|
// 根治:home 下 .sillyspec 恒不命中,防 smoke/临时目录污染自我延续)。此处 re-export 保持
|
|
27
27
|
// 既有 import 路径兼容;run/shared.js 对 progress.js 只有动态 import,无静态循环。
|
|
28
|
-
import { resolveSpecDir, checkPlatformManaged, PLATFORM_MANAGED_FILENAME, isSelfReferentialSpecRoot } from './run/shared.js';
|
|
28
|
+
import { resolveSpecDir, checkPlatformManaged, PLATFORM_MANAGED_FILENAME, isSelfReferentialSpecRoot, isTempResidueSpecRoot } from './run/shared.js';
|
|
29
29
|
export { resolveSpecDir };
|
|
30
30
|
|
|
31
31
|
// 默认规范目录名(相对于 cwd)
|
|
32
32
|
// SPEC_DIR_NAME → ./progress/shared.js(W6 Step9d)
|
|
33
33
|
const RUNTIME_SUBDIR = '.runtime';
|
|
34
34
|
|
|
35
|
+
// import() 的 pre-import 快照保留份数。这类快照只服务「刚 pull 完发现平台盖错、要退回
|
|
36
|
+
// pull 前」这一个场景,最新一份即可覆盖;越旧的快照 schema 可能已 bump,恢复价值随时间衰减。
|
|
37
|
+
// 可用 SILLYSPEC_PREIMPORT_BAK_KEEP 覆盖(灾难排查期想多留几份时)。
|
|
38
|
+
const PRE_IMPORT_BAK_KEEP_DEFAULT = 1;
|
|
39
|
+
const PRE_IMPORT_BAK_RE = /^sillyspec\.db\.pre-import-.*\.bak$/;
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* 向上查找含 .sillyspec 目录的祖先目录——实现已收敛至 src/run/shared.js(单一真相源,
|
|
37
43
|
* 含 home 拒绝守卫),此处 re-export。见顶部 import 说明。
|
|
@@ -95,6 +101,18 @@ export function resolvePlatformSpecDir(cwd, explicitSpecDir = null) {
|
|
|
95
101
|
// 指针丢失不该静默当纯本地项目处理(会建本地进度库 → 状态分裂)。
|
|
96
102
|
const decl = checkPlatformManaged(cwd);
|
|
97
103
|
if (decl) {
|
|
104
|
+
// temp 残留降级(2026-09-08 temp 投毒治理):声明 specRoot 在系统 temp 且 cwd 不在
|
|
105
|
+
// temp → 可证伪的测试/联调残留(真实平台 spec 根永不在 temp),与指针路径的 temp
|
|
106
|
+
// 警告(下方 resolvedTmp 分支)及自指声明降级(run/command.js 恢复链)同类。warn +
|
|
107
|
+
// 自动清理声明后按本地模式继续——不清理则每次命令重复 warn,且 disconnect 会连带
|
|
108
|
+
// 清 local.yaml platform 段误伤真实平台连接,不适合作为残留清理手段。
|
|
109
|
+
// 删除失败仍按本地继续(best effort;下次命令重复 warn 提示手动清理)。
|
|
110
|
+
if (isTempResidueSpecRoot(cwd, decl.specRoot)) {
|
|
111
|
+
const declarationPath = join(resolve(cwd), PLATFORM_MANAGED_FILENAME);
|
|
112
|
+
console.warn(`⚠️ 检测到 temp 残留平台接管声明(specRoot 指向系统 temp 目录:${decl.specRoot},测试/联调投毒特征),已自动清理并按本地模式运行: ${declarationPath}`);
|
|
113
|
+
try { unlinkSync(declarationPath) } catch { /* best effort */ }
|
|
114
|
+
return resolveSpecDir(cwd);
|
|
115
|
+
}
|
|
98
116
|
throw new PlatformManagedError({
|
|
99
117
|
declarationPath: join(resolve(cwd), PLATFORM_MANAGED_FILENAME),
|
|
100
118
|
specRoot: decl.specRoot,
|
|
@@ -613,6 +631,14 @@ export class ProgressManager {
|
|
|
613
631
|
if (existsSync(walSidecar)) {
|
|
614
632
|
try { copyFileSync(walSidecar, `${bakPath}-wal`); } catch { /* 侧车消失=已 checkpoint,无碍 */ }
|
|
615
633
|
}
|
|
634
|
+
// 写完即回收旧快照:每次 pull / resolve --take-platform 都新增一份全库快照,无上限时
|
|
635
|
+
// 按同步次数无限累积(实证单仓 292 份 / 317MB)。裁剪放在写入侧而非独立 GC 命令——
|
|
636
|
+
// 靠人工记得跑 GC 等于没有回收。fail-open:卫生动作不能阻断 import 主流程。
|
|
637
|
+
try {
|
|
638
|
+
this._pruneImportBaks(cwd, bakPath);
|
|
639
|
+
} catch (e) {
|
|
640
|
+
console.warn(`⚠️ pre-import 快照裁剪失败(不阻断 import): ${e.message}`);
|
|
641
|
+
}
|
|
616
642
|
|
|
617
643
|
const pushedAt = progressObj.pushed_at ?? null;
|
|
618
644
|
const now = new Date().toISOString();
|
|
@@ -707,6 +733,38 @@ export class ProgressManager {
|
|
|
707
733
|
return { ok: true, imported: cn, reason: undefined, bakPath };
|
|
708
734
|
}
|
|
709
735
|
|
|
736
|
+
/**
|
|
737
|
+
* 滚动裁剪 .runtime/ 下的 pre-import 快照,只留最新 N 份(含各自 -wal 侧车)。
|
|
738
|
+
*
|
|
739
|
+
* 排序依据:文件名内嵌 ISO 时间戳(冒号/点已替换为 '-',见 import() 的 bakTs),
|
|
740
|
+
* 定宽且无进位歧义,字典序 == 时间序,无需 statSync(mtime 会被 copy/同步工具改写)。
|
|
741
|
+
*
|
|
742
|
+
* @param {string} cwd
|
|
743
|
+
* @param {string|null} keepPath - 本次刚写的快照路径,绝不裁(并发 import 互删护栏)
|
|
744
|
+
* @returns {number} 实际删除的快照份数
|
|
745
|
+
*/
|
|
746
|
+
_pruneImportBaks(cwd, keepPath) {
|
|
747
|
+
let keep = Number(process.env.SILLYSPEC_PREIMPORT_BAK_KEEP);
|
|
748
|
+
if (!Number.isInteger(keep) || keep < 1) keep = PRE_IMPORT_BAK_KEEP_DEFAULT;
|
|
749
|
+
|
|
750
|
+
const runtimeDir = this._runtimePath(cwd);
|
|
751
|
+
const baks = readdirSync(runtimeDir).filter(f => PRE_IMPORT_BAK_RE.test(f)).sort();
|
|
752
|
+
const keepName = keepPath ? basename(keepPath) : null;
|
|
753
|
+
const doomed = baks.slice(0, Math.max(0, baks.length - keep)).filter(f => f !== keepName);
|
|
754
|
+
|
|
755
|
+
let removed = 0;
|
|
756
|
+
for (const f of doomed) {
|
|
757
|
+
// 单份删除失败(他进程持句柄 / 权限)不连坐其余份:跳过即可,下次 import 再收。
|
|
758
|
+
try { unlinkSync(join(runtimeDir, f)); } catch { continue; }
|
|
759
|
+
removed++;
|
|
760
|
+
const wal = join(runtimeDir, `${f}-wal`);
|
|
761
|
+
if (existsSync(wal)) {
|
|
762
|
+
try { unlinkSync(wal); } catch { /* 侧车残留无害:不参与恢复链,下次裁剪再收 */ }
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return removed;
|
|
766
|
+
}
|
|
767
|
+
|
|
710
768
|
/**
|
|
711
769
|
* 本地脏度标记(D-013 / task-04):写入路径末尾更新 changes.last_local_modified_ts。
|
|
712
770
|
* 在写方法的事务内调用则属于该事务(原子,失败整体回滚);import 是例外不调本方法
|
|
@@ -839,6 +897,9 @@ export class ProgressManager {
|
|
|
839
897
|
// 单变更阶段查询(quick 轻量归档阶段闸):无行 null,读失败抛(调用方 fail-closed)
|
|
840
898
|
getChangeStage(cwd, changeName) { return this._changeRegistry.getChangeStage(cwd, changeName); }
|
|
841
899
|
|
|
900
|
+
// verifyStartAt 基准只读(2026-09-08-ir-verify-facts):execute 行 completed_at;无行 null 走 R-05 fallback
|
|
901
|
+
getStageCompletedAt(cwd, changeName, stage) { return this._changeRegistry.getStageCompletedAt(cwd, changeName, stage); }
|
|
902
|
+
|
|
842
903
|
registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
|
|
843
904
|
|
|
844
905
|
updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
|
package/src/quick-recommend.js
CHANGED
|
@@ -84,12 +84,17 @@ export function recommendChanges({
|
|
|
84
84
|
taskDescription = '',
|
|
85
85
|
}) {
|
|
86
86
|
if (!activeChanges || activeChanges.length === 0) return []
|
|
87
|
+
// 活跃 quick 会话过滤(2026-09-09 §7-3):quick-<hex8> 是会话不是完整流程变更——无
|
|
88
|
+
// design/proposal 语义可关联(冒烟实证:新 quick 被自动挂上另一活跃 quick 当关联变更)。
|
|
89
|
+
// 形态单源 QUICK_SID_RE(run/shared.js);此处不 import 防把 fs 纯函数拖进 shared 依赖链。
|
|
90
|
+
const QUICK_SESSION_ID_RE = /^quick-[0-9a-f]{8}$/
|
|
91
|
+
const candidateChanges = activeChanges.filter((name) => !QUICK_SESSION_ID_RE.test(name))
|
|
87
92
|
|
|
88
93
|
const dirtyFiles = [...new Set([...baselineFiles, ...quickFiles])]
|
|
89
94
|
.filter(f => f && !f.startsWith('.sillyspec/'))
|
|
90
95
|
const descTokens = [...new Set(tokenizeDescription(taskDescription))]
|
|
91
96
|
|
|
92
|
-
const results =
|
|
97
|
+
const results = candidateChanges.map(name => {
|
|
93
98
|
const reasons = []
|
|
94
99
|
|
|
95
100
|
// 信号 1:脏文件命中 design.md 文件变更清单
|
package/src/quicklog.js
CHANGED
|
@@ -294,9 +294,17 @@ export function extractTitleFromResult(result) {
|
|
|
294
294
|
if (!result) return ''
|
|
295
295
|
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
296
296
|
if (!m) return ''
|
|
297
|
-
|
|
297
|
+
// 不再截到首个标点(2026-09-08 用户反馈②):旧口径迫使 agent 刻意避开标点写标题,
|
|
298
|
+
// 「A,B」被截成「A」反而降低可读性。--req 的模板指引本就要求一句话短标题,
|
|
299
|
+
// 标点由写者自主掌控;CLI 只兜底超长截断。
|
|
300
|
+
let t = m[1].trim()
|
|
298
301
|
if (!t) return ''
|
|
299
|
-
if (t.length > 80)
|
|
302
|
+
if (t.length > 80) {
|
|
303
|
+
// 超长截断优先在标点/空格处断(标题残句可读),找不到就近边界才硬截。
|
|
304
|
+
const cut = t.slice(0, 80)
|
|
305
|
+
const m2 = cut.match(/^[\s\S]*[,。;:,;:]\s*/)
|
|
306
|
+
t = (m2 && m2[0].length >= 40 ? m2[0] : cut).replace(/[,。;:,;:\s]+$/, '') + '…'
|
|
307
|
+
}
|
|
300
308
|
return t
|
|
301
309
|
}
|
|
302
310
|
|