sillyspec 3.28.0 → 3.28.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/package.json +1 -1
- package/src/archive-delta.js +419 -0
- package/src/design-facts.js +354 -0
- package/src/docs-check.js +79 -8
- package/src/doctor-diagnostics.js +11 -2
- package/src/endpoint-baseline.js +109 -0
- package/src/git-helper.js +31 -0
- package/src/index.js +213 -4
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +20 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +3 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +70 -1
- package/src/run/complete-handlers.js +53 -4
- package/src/run/complete.js +46 -1
- package/src/run/gates.js +333 -2
- package/src/run/prompt.js +70 -0
- package/src/run/scan-profile.js +98 -6
- package/src/run/shared.js +12 -1
- package/src/run/stage.js +7 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +110 -3
- package/src/spec-sync.js +19 -2
- package/src/stage-review.js +62 -3
- package/src/stages/archive.js +5 -5
- package/src/stages/brainstorm.js +10 -10
- package/src/stages/execute.js +15 -13
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +6 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +9 -7
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +610 -2
- package/src/verify-probes.js +89 -10
- package/src/workflow.js +30 -0
- package/templates/agents-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +5 -0
- package/templates/workflows/scan-docs.yaml +6 -0
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
10
10
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
11
|
-
import { basename, dirname, extname, join, resolve, isAbsolute } from 'path';
|
|
11
|
+
import { basename, dirname, extname, join, resolve, isAbsolute, sep } from 'path';
|
|
12
12
|
import { safeGit, git } from './git-helper.js';
|
|
13
13
|
import { getVersion } from './version.js';
|
|
14
14
|
|
|
@@ -94,11 +94,14 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
94
94
|
sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
|
|
95
95
|
sillyspec backfill-reviews --change <name> [--adopt] [--json] 缺 review.json 的 task 生成草稿;--adopt 重算代填已有 review 的 base/head 等机械字段(verdict 保留)
|
|
96
96
|
sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
|
|
97
|
+
sillyspec design-init --change <name> [--force] 从 decisions.md 生成 design.md 十三章节骨架(决策追踪表预填;已存在不覆盖)
|
|
98
|
+
sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
|
|
97
99
|
sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
|
|
98
100
|
sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
|
|
99
101
|
sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
|
|
100
102
|
sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
|
|
101
103
|
sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
|
|
104
|
+
sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json] 拍变更前端点基线(幂等不覆盖;worktree 内跑自动锚主仓;归档 delta 端点增删 before 侧)
|
|
102
105
|
sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
|
|
103
106
|
sillyspec plan-adopt-waves --change <名> [--dry-run] plan.md Wave 段一键重排为 depends_on 拓扑分组(同步任务总表 W 列,幂等)
|
|
104
107
|
sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
|
|
@@ -833,7 +836,7 @@ async function main() {
|
|
|
833
836
|
process.exit(2);
|
|
834
837
|
}
|
|
835
838
|
assertSafeChangeName(vpChange, '--change 变更名');
|
|
836
|
-
const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase } = await import('./verify-probes.js');
|
|
839
|
+
const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts } = await import('./verify-probes.js');
|
|
837
840
|
// spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
|
|
838
841
|
// 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
|
|
839
842
|
// resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
|
|
@@ -851,11 +854,26 @@ async function main() {
|
|
|
851
854
|
if (vpInit) {
|
|
852
855
|
const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
|
|
853
856
|
if (existsSync(vpReportPath)) {
|
|
854
|
-
|
|
857
|
+
const vpExisting = readFileSync(vpReportPath, 'utf8');
|
|
858
|
+
// 检测正则与提取侧(parseProbePrefillAnchors 的 /^#### 探针 (\d+)[::]/)对齐要求冒号——
|
|
859
|
+
// 对抗复审 BUG-1(2026-09-07):agent 手写无冒号走样标题 `#### 探针 1` 会被宽松检测误判
|
|
860
|
+
// 「已有子节」跳过注入,而提取侧不认 → 死路复活(--init no-op + gate error 无解)。
|
|
861
|
+
if (!/^#### 探针 \d+[::]/m.test(vpExisting)) {
|
|
862
|
+
// P1 修复(2026-09-07):存量旧格式(九章节)报告 + --init 曾是死路——facts 无条件刷新
|
|
863
|
+
// 而正文不补,checkProbeConsistency 判别子判「疑似 agent 删除预填段」error 阻断,而修复
|
|
864
|
+
// 指引「重跑 --init」对已存在报告是 no-op。修法:探针段是 CLI 拥有的机械段,检测缺失时
|
|
865
|
+
// 尾部补注入(正文其余不动),判别子自此有子节可对账。
|
|
866
|
+
const vpSection = `\n## 探针结果(CLI 机械预填,--init 补注入) [层:可复跑探针——gate 抽查防篡改]\n${renderVerifyProbesReport(vpResult)}\n`;
|
|
867
|
+
writeFileSync(vpReportPath, vpExisting.replace(/\s*$/, '\n') + vpSection);
|
|
868
|
+
console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)`);
|
|
869
|
+
} else {
|
|
870
|
+
console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
|
|
871
|
+
}
|
|
855
872
|
} else {
|
|
856
873
|
writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
|
|
857
874
|
console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
|
|
858
875
|
}
|
|
876
|
+
writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新,最近快照语义)
|
|
859
877
|
}
|
|
860
878
|
break;
|
|
861
879
|
}
|
|
@@ -945,8 +963,78 @@ async function main() {
|
|
|
945
963
|
// 产生大量假 missing):聚合模式——逐 task 卡提取,各自落 contract-artifacts/<change>/<task>/,
|
|
946
964
|
// 与 verifyApiParity 的聚合读侧(contract-artifacts/<change>/*/endpoints.json 全量并集)对齐。
|
|
947
965
|
const epSub = filteredArgs[1];
|
|
966
|
+
// 子分发(变更 2026-09-07-endpoint-baseline task-02):baseline(变更前基线)+ extract(原样)。
|
|
967
|
+
if (epSub === 'baseline') {
|
|
968
|
+
// 端点基线(D-002 gap-1):变更起点全仓现扫端点集快照 → .runtime/endpoint-baselines/
|
|
969
|
+
// <change>.json(幂等,已拍过不覆盖——首次跑即变更前状态,重跑保持原快照)。归档 delta
|
|
970
|
+
// 端点增删的 before 侧;扫描/落盘在 endpoint-baseline.js 纯函数,此处只做参数解析 +
|
|
971
|
+
// worktree 主仓锚定 + 输出。
|
|
972
|
+
const ebChangeIdx = args.indexOf('--change');
|
|
973
|
+
const ebChange = ebChangeIdx >= 0 && args[ebChangeIdx + 1] ? args[ebChangeIdx + 1] : null;
|
|
974
|
+
if (!ebChange) {
|
|
975
|
+
console.error('用法: sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json]\n 拍变更前端点基线(全仓现扫,幂等已拍过不覆盖)→ .runtime/endpoint-baselines/<change>.json;归档 delta 端点增删的 before 侧。\n execute worktree 内跑:扫描根与落点自动锚定主仓(变更前代码在主仓;worktree 内是交付态且其 .runtime 随清理丢失)');
|
|
976
|
+
process.exit(2);
|
|
977
|
+
}
|
|
978
|
+
assertSafeChangeName(ebChange, '--change 变更名');
|
|
979
|
+
// worktree 主仓锚定(D-002 gap-1 关键):cwd 在 execute worktree 内时——
|
|
980
|
+
// ① 扫描 cwd 若保持 worktree:扫到的是 baseline 检出 + 本变更改动(非「变更前」端点集);
|
|
981
|
+
// ② runtimeRoot 若随 cwd 落 worktree 副本 .runtime:基线随 worktree 清理静默丢失。
|
|
982
|
+
// 判据同 resolveEffectiveDir(P1-1)/ worktree.js _resolveMainRepoRoot:--git-dir ≠
|
|
983
|
+
// --git-common-dir 且非 submodule = linked worktree,common-dir(可能是相对路径,须相对
|
|
984
|
+
// dir 绝对化)父目录即主仓根 → 扫描根与 spec 解析都锚过去(--spec-dir 显式指定仍优先)。
|
|
985
|
+
let ebScanCwd = dir;
|
|
986
|
+
let ebSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
987
|
+
// --json 纪律(design §8):机器输出模式下人类提示走 stderr,stdout 只留最终 JSON
|
|
988
|
+
const ebNote = (msg) => { (json ? console.error : console.log)(msg); };
|
|
989
|
+
try {
|
|
990
|
+
const gd = git(dir, ['rev-parse', '--git-dir']);
|
|
991
|
+
const gcd = git(dir, ['rev-parse', '--git-common-dir']);
|
|
992
|
+
const superProj = git(dir, ['rev-parse', '--show-superproject-working-tree']);
|
|
993
|
+
if (gd && gcd && gd !== gcd && !superProj) {
|
|
994
|
+
const ebMainRoot = dirname(resolve(dir, gcd));
|
|
995
|
+
if (ebMainRoot !== dir && existsSync(ebMainRoot)) {
|
|
996
|
+
ebNote(`⚠️ cwd 在 linked worktree 内,基线扫描根与落点已锚定主仓: ${ebMainRoot}(变更前代码在主仓;worktree 内是交付态且其 .runtime 随清理丢失,防基线静默失效)`);
|
|
997
|
+
ebScanCwd = ebMainRoot;
|
|
998
|
+
ebSpecBase = resolvePlatformSpecDir(ebMainRoot, specDir) || join(ebMainRoot, '.sillyspec');
|
|
999
|
+
// 显式 --spec-dir 指向 worktree 副本(任意形态——手建目录/检出副本;下方 drift 只认
|
|
1000
|
+
// .runtime/worktrees 检出形态,覆盖不了手建副本)→ 重算结果仍在 worktree 内时锚回主仓
|
|
1001
|
+
if (!resolve(ebSpecBase).toLowerCase().startsWith(resolve(ebMainRoot).toLowerCase() + sep)) {
|
|
1002
|
+
ebNote(`⚠️ 显式 spec 命中 worktree 副本(${ebSpecBase}),基线落点一并锚定主仓: ${join(ebMainRoot, '.sillyspec')}`);
|
|
1003
|
+
ebSpecBase = join(ebMainRoot, '.sillyspec');
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
} catch { /* git 不可用/非仓库 → 不锚定,按原 cwd 继续 */ }
|
|
1008
|
+
// 终局兜底(同下方 extract 分支 detectWorktreeSpecDrift 用法,无论上面是否已锚定都跑:
|
|
1009
|
+
// --spec-dir 显式指向 worktree 副本 spec 时,上面重算 resolvePlatformSpecDir 仍原样
|
|
1010
|
+
// 返回该显式路径)——spec 命中副本 → 锚回主仓,防 runtimeRoot 落副本 .runtime。
|
|
1011
|
+
const ebDrift = detectWorktreeSpecDrift(ebSpecBase);
|
|
1012
|
+
if (ebDrift) {
|
|
1013
|
+
ebNote(`⚠️ 已自动锚定主仓 spec: ${ebDrift.mainSpecBase}(基线落主仓;当前 spec 命中 worktree 副本 ${ebDrift.changeName},流程继续)`);
|
|
1014
|
+
ebSpecBase = ebDrift.mainSpecBase;
|
|
1015
|
+
}
|
|
1016
|
+
const ebRuntimeRoot = (await import('./run/shared.js')).resolveRuntimeRoot({ specRoot: specDir }, ebSpecBase);
|
|
1017
|
+
const { captureEndpointBaseline } = await import('./endpoint-baseline.js');
|
|
1018
|
+
const ebResult = captureEndpointBaseline({ cwd: ebScanCwd, changeName: ebChange, runtimeRoot: ebRuntimeRoot });
|
|
1019
|
+
if (json) {
|
|
1020
|
+
console.log(JSON.stringify({ command: 'endpoints-baseline', change: ebChange, ok: !ebResult.error, ...ebResult }, null, 2));
|
|
1021
|
+
if (ebResult.error) process.exit(1);
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
if (ebResult.error) {
|
|
1025
|
+
console.error(`❌ 端点基线拍摄失败: ${ebResult.error}`);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
if (!ebResult.written) {
|
|
1029
|
+
console.log(`ℹ️ 端点基线已存在,跳过(幂等不覆盖): ${join(ebRuntimeRoot, 'endpoint-baselines', `${ebChange}.json`)}`);
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
console.log(`✅ 已拍变更前端点基线(${ebResult.count} 个端点)→ ${ebResult.path}`);
|
|
1033
|
+
console.log(' 幂等:已拍过不覆盖(重跑保持首次快照);归档 delta 用它计算端点增删(before 侧)。');
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
948
1036
|
if (epSub !== 'extract') {
|
|
949
|
-
console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5 聚合读侧一致,推荐)。\n execute worktree 模式:allowed_paths 自动按 worktree 根解析,产物恒落主仓 spec(坑 endpoints-extract-worktree-pitfalls
|
|
1037
|
+
console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费。\n --all-tasks:聚合逐 task 卡 allowed_paths 各自提取(对账口径与探针 5 聚合读侧一致,推荐)。\n execute worktree 模式:allowed_paths 自动按 worktree 根解析,产物恒落主仓 spec(坑 endpoints-extract-worktree-pitfalls)\nsillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json]\n 拍变更前端点基线(全仓现扫,幂等已拍过不覆盖)→ .runtime/endpoint-baselines/<change>.json;归档 delta 端点增删的 before 侧。worktree 内跑自动锚定主仓。');
|
|
950
1038
|
process.exit(2);
|
|
951
1039
|
}
|
|
952
1040
|
const epChangeIdx = args.indexOf('--change');
|
|
@@ -1156,6 +1244,108 @@ async function main() {
|
|
|
1156
1244
|
console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
|
|
1157
1245
|
break;
|
|
1158
1246
|
}
|
|
1247
|
+
case 'design-init': {
|
|
1248
|
+
// design.md 十三章节骨架生成(change: 2026-09-07-ir-stage-p3c,task-02,FR-03):agent 此前
|
|
1249
|
+
// 从零手写整份设计文档,章节漏项到 stage-contract/plan 门禁才暴露。本命令从 decisions.md
|
|
1250
|
+
// 当前版本 D 条目预填决策追踪表,章节标题逐字取自 brainstorm Step 6 模板(R-03,渲染本体在
|
|
1251
|
+
// design-facts.js generateDesignSkeleton——本 case 只负责读盘/幂等保护/落盘)。已存在不覆盖
|
|
1252
|
+
// (幂等,agent 产出优先),--force 覆盖重生成。
|
|
1253
|
+
const diChangeIdx = args.indexOf('--change');
|
|
1254
|
+
const diChange = diChangeIdx >= 0 && args[diChangeIdx + 1] ? args[diChangeIdx + 1] : null;
|
|
1255
|
+
const diForce = args.includes('--force');
|
|
1256
|
+
if (!diChange) {
|
|
1257
|
+
console.error('用法: sillyspec design-init --change <name> [--force] [--json] [--spec-dir <path>]\n 从变更 decisions.md 生成 design.md 十三章节骨架(决策追踪表按当前版本 D 条目预填;已存在不覆盖,--force 覆盖)');
|
|
1258
|
+
process.exit(2);
|
|
1259
|
+
}
|
|
1260
|
+
assertSafeChangeName(diChange, '--change 变更名');
|
|
1261
|
+
const diSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
1262
|
+
const diChangeDir = join(diSpecBase, 'changes', diChange);
|
|
1263
|
+
const diDecisionsPath = join(diChangeDir, 'decisions.md');
|
|
1264
|
+
if (!existsSync(diDecisionsPath)) {
|
|
1265
|
+
console.error(`❌ decisions.md 不存在: ${diDecisionsPath}`);
|
|
1266
|
+
console.error(' 先跑 sillyspec run brainstorm 推进决策环节(形成技术决策落盘 decisions.md),再生成 design 骨架。');
|
|
1267
|
+
if (json) console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: false, error: `decisions.md 不存在: ${diDecisionsPath}` }, null, 2));
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
const { generateDesignSkeleton } = await import('./design-facts.js');
|
|
1271
|
+
// author/created_at 与 taskcard/scan-postcheck 同口径:git user.name(回退 unknown)+ 本地墙钟
|
|
1272
|
+
let diAuthor = 'unknown';
|
|
1273
|
+
try { diAuthor = git(dir, ['config', 'user.name']) || 'unknown' } catch { /* git 不可用 → unknown */ }
|
|
1274
|
+
const { nowWallClock } = await import('./datetime.js');
|
|
1275
|
+
const diSkeleton = generateDesignSkeleton({
|
|
1276
|
+
changeName: diChange,
|
|
1277
|
+
decisionsText: readFileSync(diDecisionsPath, 'utf8'),
|
|
1278
|
+
author: diAuthor,
|
|
1279
|
+
now: nowWallClock(),
|
|
1280
|
+
});
|
|
1281
|
+
const diDesignPath = join(diChangeDir, 'design.md');
|
|
1282
|
+
const diExisted = existsSync(diDesignPath);
|
|
1283
|
+
if (diExisted && !diForce) {
|
|
1284
|
+
if (json) {
|
|
1285
|
+
console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: true, path: diDesignPath, written: false, reason: 'exists' }, null, 2));
|
|
1286
|
+
} else {
|
|
1287
|
+
console.log(`ℹ️ design.md 已存在,不覆盖: ${diDesignPath}(确要重新生成骨架加 --force)`);
|
|
1288
|
+
}
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
writeFileSync(diDesignPath, diSkeleton);
|
|
1292
|
+
if (json) {
|
|
1293
|
+
console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: true, path: diDesignPath, written: true, overwritten: diExisted }, null, 2));
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
console.log(`✅ 已生成 design.md 骨架: ${diDesignPath}${diExisted ? '(--force 覆盖既有文件)' : ''}`);
|
|
1297
|
+
console.log(' 决策追踪表已按 decisions.md 当前版本 D 条目预填;回到 brainstorm Step 6(写设计文档并自审)逐节填散文,填毕删除生成注释。');
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
case 'delta': {
|
|
1301
|
+
// delta.md 生成(change: 2026-09-07-ir-stage-p3d,task-02):task-01 buildDeltaReport
|
|
1302
|
+
// 四源 fail-soft 聚合(reconcile/verify-facts/module-map/decisions)的 CLI 落盘入口——
|
|
1303
|
+
// 归档确认步(handleArchiveConfirmStep)自动生成失败时的手动补跑通道。幂等覆盖
|
|
1304
|
+
// (重跑即刷新):与 design-init「agent 产出优先不覆盖」语义相反——delta 是源数据
|
|
1305
|
+
// (verify-runs/module-map)时点快照,旧报告天然过期,重新生成即对齐。
|
|
1306
|
+
const dlChangeIdx = args.indexOf('--change');
|
|
1307
|
+
const dlChange = dlChangeIdx >= 0 && args[dlChangeIdx + 1] ? args[dlChangeIdx + 1] : null;
|
|
1308
|
+
if (!dlChange) {
|
|
1309
|
+
console.error('用法: sillyspec delta --change <name> [--spec-dir <path>] [--json]\n 生成变更 delta.md(Before/Delta/After 三段式;四源 fail-soft,缺源逐段降级注记;幂等覆盖重跑即刷新)');
|
|
1310
|
+
process.exit(2);
|
|
1311
|
+
}
|
|
1312
|
+
assertSafeChangeName(dlChange, '--change 变更名');
|
|
1313
|
+
const dlSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
1314
|
+
const dlChangeDir = join(dlSpecBase, 'changes', dlChange);
|
|
1315
|
+
if (!existsSync(dlChangeDir)) {
|
|
1316
|
+
console.error(`❌ 变更目录不存在: ${dlChangeDir}`);
|
|
1317
|
+
if (json) console.log(JSON.stringify({ command: 'delta', change: dlChange, ok: false, error: `变更目录不存在: ${dlChangeDir}` }, null, 2));
|
|
1318
|
+
process.exit(1);
|
|
1319
|
+
}
|
|
1320
|
+
// runtimeRoot 解析同 register-stage-review 先例(resolvePlatformOpts > resolveRuntimeRoot):
|
|
1321
|
+
// reconcile(verify-runs/<ts>/reconcile-result.json)与 apply-pathspec 兜底清单都在 runtime 下。
|
|
1322
|
+
const dlPlatformOpts = {};
|
|
1323
|
+
const dlResolved = resolvePlatformOpts(dir, specDir);
|
|
1324
|
+
if (dlResolved) {
|
|
1325
|
+
dlPlatformOpts.specRoot = dlResolved.specRoot;
|
|
1326
|
+
if (dlResolved.runtimeRoot) dlPlatformOpts.runtimeRoot = dlResolved.runtimeRoot;
|
|
1327
|
+
}
|
|
1328
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js');
|
|
1329
|
+
const dlRuntimeRoot = resolveRuntimeRoot(dlPlatformOpts, dlSpecBase);
|
|
1330
|
+
const { buildDeltaReport } = await import('./archive-delta.js');
|
|
1331
|
+
// CLI 不收 --project:project=null 走 loadModuleMap 降级路径(报告内「无 module-map」注记),
|
|
1332
|
+
// 与归档确认步自动生成(progress.project 有值时命中 module-map)形成口径互补。
|
|
1333
|
+
const dlMarkdown = buildDeltaReport({
|
|
1334
|
+
changeDir: dlChangeDir,
|
|
1335
|
+
specRoot: dlSpecBase,
|
|
1336
|
+
project: null,
|
|
1337
|
+
runtimeRoot: dlRuntimeRoot,
|
|
1338
|
+
});
|
|
1339
|
+
const dlPath = join(dlChangeDir, 'delta.md');
|
|
1340
|
+
writeFileSync(dlPath, dlMarkdown);
|
|
1341
|
+
if (json) {
|
|
1342
|
+
console.log(JSON.stringify({ command: 'delta', change: dlChange, ok: true, path: dlPath, written: true }, null, 2));
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
console.log(`✅ 已生成 delta.md: ${dlPath}(幂等覆盖,重跑即刷新)`);
|
|
1346
|
+
console.log(' Before/Delta/After 三段式快照;归档确认步自动生成失败时可用本命令手动补。');
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1159
1349
|
case 'register-stage-review': {
|
|
1160
1350
|
// 手动注册 stage 级 review(brainstorm/plan/execute-acceptance)的确定性 writer ——
|
|
1161
1351
|
// 生成 run 目录 + review.json 骨架(cannot_verify,待独立审查子代理填 verdict)或 adopt
|
|
@@ -1610,6 +1800,25 @@ async function main() {
|
|
|
1610
1800
|
break;
|
|
1611
1801
|
}
|
|
1612
1802
|
case 'scan': {
|
|
1803
|
+
// scan facts 子命令(archify 借鉴:机械事实预咀嚼,2026-09-05)。
|
|
1804
|
+
// CLI 确定性抽取端点/依赖/规模/git 基线 → docs/<project>/scan/_facts.md,
|
|
1805
|
+
// 供 scan 步骤 3/5 的子代理共享(禁止各自 grep 重复发现)。与 diff 同为纯只读旁路。
|
|
1806
|
+
if (filteredArgs[1] === 'facts') {
|
|
1807
|
+
const { buildAndWriteScanFacts } = await import('./scan-facts.js')
|
|
1808
|
+
const factsEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
1809
|
+
const projIdx = filteredArgs.indexOf('--project')
|
|
1810
|
+
const pathIdx = filteredArgs.indexOf('--path')
|
|
1811
|
+
const r = buildAndWriteScanFacts({
|
|
1812
|
+
cwd: factsEffectiveDir,
|
|
1813
|
+
specDir: resolvePlatformSpecDir(dir, specDir) || join(factsEffectiveDir, '.sillyspec'),
|
|
1814
|
+
projectName: projIdx >= 0 && filteredArgs[projIdx + 1] ? filteredArgs[projIdx + 1] : basename(factsEffectiveDir),
|
|
1815
|
+
projectPath: pathIdx >= 0 && filteredArgs[pathIdx + 1] ? filteredArgs[pathIdx + 1] : null,
|
|
1816
|
+
})
|
|
1817
|
+
const f = r.facts
|
|
1818
|
+
console.log(`scan facts 已写入: ${r.outPath}`)
|
|
1819
|
+
console.log(` 类型 ${f.type} · 依赖 ${f.deps.length} · 端点 ${f.endpoints.backendTotal}(后端)/ ${f.endpoints.frontendTotal}(前端调用)· 源文件 ${f.size.fileCount}`)
|
|
1820
|
+
break
|
|
1821
|
+
}
|
|
1613
1822
|
// scan diff 子命令(D-001@v1:接线唯一入口 index.js,非 command.js 裸 token 静默吞)。
|
|
1614
1823
|
// 纯只读比较命令:跳过下方共享块的 triggerPullActiveChange(不触发网络 pull),
|
|
1615
1824
|
// 也不走 scan 主流程(--standard/--deep 分支不动)。解析 --base/--full/--report 后转发 scan-diff。
|
package/src/modules.js
CHANGED
|
@@ -212,21 +212,42 @@ export function splitChangelog(cwd, { force = false } = {}) {
|
|
|
212
212
|
for (const f of cards) {
|
|
213
213
|
const cardPath = join(modulesDir, f);
|
|
214
214
|
const content = readFileSync(cardPath, 'utf8');
|
|
215
|
-
// 提取「## 变更索引」节(CRLF 归一后行级匹配,与 modules.js
|
|
215
|
+
// 提取「## 变更索引」节(CRLF 归一后行级匹配,与 modules.js 其余解析同防线)。
|
|
216
|
+
// 标题正则允许后缀(如「(表格,初始为空)」——2026-09-07 实证 core-engine 形态漏迁);
|
|
217
|
+
// miss 时再试 MANUAL_NOTES 区(stages/runtime 形态:<!-- MANUAL_NOTES_END --> 前的尾部
|
|
218
|
+
// 行块——全或无:块内行须全部为「- <变更名> | 摘要」形态才整体迁出,防误迁散文)
|
|
216
219
|
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
217
|
-
const startIdx = lines.findIndex(l => /^##\s
|
|
218
|
-
|
|
219
|
-
let
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
const startIdx = lines.findIndex(l => /^##\s*变更索引/.test(l));
|
|
221
|
+
let kind = 'index';
|
|
222
|
+
let segStart = -1, segEnd = -1, body = '';
|
|
223
|
+
if (startIdx >= 0) {
|
|
224
|
+
let endIdx = lines.length;
|
|
225
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
226
|
+
if (/^##\s/.test(lines[i])) { endIdx = i; break; }
|
|
227
|
+
}
|
|
228
|
+
const b = lines.slice(startIdx + 1, endIdx).join('\n').trim();
|
|
229
|
+
// 已迁出(只剩指针行)或本就为空 → 跳过
|
|
230
|
+
if (!b || /^见\s/.test(b) || b.includes('.changelog.md')) { skipped++; continue; }
|
|
231
|
+
kind = 'index'; segStart = startIdx + 1; segEnd = endIdx; body = b;
|
|
232
|
+
} else {
|
|
233
|
+
const endMark = lines.findIndex(l => l.includes('<!-- MANUAL_NOTES_END -->'));
|
|
234
|
+
if (endMark < 0) { skipped++; continue; }
|
|
235
|
+
// 块首 = END 标记向上、以空行为界的连续内容块起点
|
|
236
|
+
let bs = endMark - 1;
|
|
237
|
+
while (bs >= 0 && lines[bs].trim() !== '') bs--;
|
|
238
|
+
// 块内剔除 HTML 注释行(区开标记 <!-- MANUAL_NOTES ... --> 等)后须全为变更行形态
|
|
239
|
+
const block = lines.slice(bs + 1, endMark).filter(l => {
|
|
240
|
+
const t = l.trim()
|
|
241
|
+
return t !== '' && !/^<!--.*-->$/.test(t)
|
|
242
|
+
});
|
|
243
|
+
const CHG_LINE = /^- [A-Za-z0-9][\w\-./]* \| /;
|
|
244
|
+
if (block.length === 0 || !block.every(l => CHG_LINE.test(l))) { skipped++; continue; }
|
|
245
|
+
kind = 'manual-notes'; segStart = bs + 1; segEnd = endMark; body = block.join('\n');
|
|
222
246
|
}
|
|
223
|
-
const body = lines.slice(startIdx + 1, endIdx).join('\n').trim();
|
|
224
|
-
// 已迁出(只剩指针行)或本就为空 → 跳过
|
|
225
|
-
if (!body || /^见\s/.test(body) || body.includes('.changelog.md')) { skipped++; continue; }
|
|
226
247
|
const bytesMoved = Buffer.byteLength(body, 'utf8');
|
|
227
248
|
const stem = f.replace(/\.md$/, '');
|
|
228
249
|
const sidecarPath = join(modulesDir, `${stem}.changelog.md`);
|
|
229
|
-
results.push({ card: cardPath, sidecar: sidecarPath, bytesMoved, _lines: lines,
|
|
250
|
+
results.push({ card: cardPath, sidecar: sidecarPath, bytesMoved, kind, _lines: lines, _segStart: segStart, _segEnd: segEnd, _body: body });
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
253
|
|
|
@@ -253,9 +274,17 @@ export function splitChangelog(cwd, { force = false } = {}) {
|
|
|
253
274
|
sidecarContent = header + '\n' + r._body + '\n';
|
|
254
275
|
}
|
|
255
276
|
writeFileSync(r.sidecar, sidecarContent, 'utf8');
|
|
256
|
-
//
|
|
277
|
+
// 卡内替换(只动该段,其余逐字保留;_lines 已是 LF 归一):
|
|
278
|
+
// index → 节体换指针行;manual-notes → 尾部块清空 + END 标记前留迁移指针注释
|
|
257
279
|
const stem = r.card.split(/[\\/]/).pop().replace(/\.md$/, '');
|
|
258
|
-
|
|
280
|
+
let newLines;
|
|
281
|
+
if (r.kind === 'manual-notes') {
|
|
282
|
+
newLines = [...r._lines.slice(0, r._segStart),
|
|
283
|
+
`<!-- MANUAL_NOTES 区变更条目已迁出至 ${stem}.changelog.md(split-changelog);新条目追加 sidecar,勿写回本卡 -->`,
|
|
284
|
+
...r._lines.slice(r._segEnd)];
|
|
285
|
+
} else {
|
|
286
|
+
newLines = [...r._lines.slice(0, r._segStart), '', `见 \`${stem}.changelog.md\`——历史条目已迁出;新条目直接追加 sidecar,勿写回本卡。`, '', ...r._lines.slice(r._segEnd)];
|
|
287
|
+
}
|
|
259
288
|
writeFileSync(r.card, newLines.join('\n'), 'utf8');
|
|
260
289
|
console.log(`✅ ${r.card} → ${r.sidecar}(迁出 ${(r.bytesMoved / 1024).toFixed(1)}KB,卡内留指针)`);
|
|
261
290
|
}
|
|
@@ -126,6 +126,26 @@ export class ChangeRegistry {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* 读取变更回填的 quicklog_id(quick 启动时 stage.js 写入的分配 ql-ID)。
|
|
131
|
+
* quick --done 兜底路径复用启动 ql-ID 用(坑 platform-takeover-phantom-progress-db
|
|
132
|
+
* 同日变体:guard 缺失时补分配新号制造引用劈叉——落码注释/模块文档的 ql-ID 是启动
|
|
133
|
+
* 时就给出的,兜底换号必然劈叉;启动 ID 进度库里可查)。
|
|
134
|
+
* @param {string} cwd
|
|
135
|
+
* @param {string} changeName
|
|
136
|
+
* @returns {string|null} 行缺失/未回填/读取失败返回 null
|
|
137
|
+
*/
|
|
138
|
+
getQuicklogId(cwd, changeName) {
|
|
139
|
+
if (!changeName) return null;
|
|
140
|
+
try {
|
|
141
|
+
const db = this.pm._ensureDB(cwd);
|
|
142
|
+
const row = db.getDb().prepare('SELECT quicklog_id FROM changes WHERE name = ?').get(changeName);
|
|
143
|
+
return (row && typeof row.quicklog_id === 'string' && row.quicklog_id) || null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
129
149
|
/**
|
|
130
150
|
* 读取变更的隔离状态
|
|
131
151
|
* @param {string} cwd - 项目根目录
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// W6 Step9a: Revision v1 状态一致性检查 + 修复 + --force 审计日志。
|
|
2
2
|
// 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm.read / pm._write /
|
|
3
3
|
// pm._ensureRuntimeDir / pm._runtimePath(persistence-core 留 facade 本体)。
|
|
4
|
-
import { appendFileSync, existsSync, readdirSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
4
|
+
import { appendFileSync, existsSync, readFileSync, readdirSync } from 'fs';
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
|
+
import { DB } from '../db.js';
|
|
6
7
|
import { summarizeTaskCompletion } from '../task-review.js';
|
|
7
8
|
import { STAGE_ORDER, MAIN_FLOW_ORDER } from './shared.js';
|
|
8
9
|
|
|
@@ -11,6 +12,104 @@ export class ConsistencyDoctor {
|
|
|
11
12
|
this.pm = pm;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 双进度库分裂探测(坑 progress-repair-dual-library-blind,2026-09-04 用户实证②:
|
|
17
|
+
* 接管指针切换库后 progress repair 只看当前库,旧库残留的活跃变更进度完全不可见,
|
|
18
|
+
* 报告「未发现问题,无需修复」的假阴性)。
|
|
19
|
+
*
|
|
20
|
+
* 判据:cwd 存在平台接管指针(.sillyspec-platform.json 指向库 A = 当前修复目标),
|
|
21
|
+
* 同时 cwd/.sillyspec/.runtime/sillyspec.db(库 B = 切库前旧库)也存在且 ≠ 库 A
|
|
22
|
+
* → 双库并存。自指指针(specRoot 解析回本地 .sillyspec)不算。
|
|
23
|
+
*
|
|
24
|
+
* 只读探测:另一库仅在 sillyspec.db 已存在时直读(DB 类 + 存在性前置检查,绝不新建库文件);
|
|
25
|
+
* 读失败 fail-open 返回库存在但活跃清单不可读,不阻断 check/repair 主流程。
|
|
26
|
+
*
|
|
27
|
+
* @param {string} cwd - 进度库锚定 cwd(index.js 传入 progDir)
|
|
28
|
+
* @param {string|null} [changeName] - check/repair 的 --change 目标(判「操作错了库」用)
|
|
29
|
+
* @returns {{ split: boolean, dualDb: boolean, bothActive: boolean, targetInOther: boolean, lines: string[] }}
|
|
30
|
+
*/
|
|
31
|
+
detectLibrarySplit(cwd, changeName = null) {
|
|
32
|
+
const out = { split: false, dualDb: false, bothActive: false, targetInOther: false, lines: [] };
|
|
33
|
+
const pointerPath = join(cwd, '.sillyspec-platform.json');
|
|
34
|
+
if (!existsSync(pointerPath)) return out;
|
|
35
|
+
let ptr = null;
|
|
36
|
+
try {
|
|
37
|
+
ptr = JSON.parse(readFileSync(pointerPath, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return out; // 指针损坏由 resolvePlatformSpecDir fail-closed 负责,此处不重复报
|
|
40
|
+
}
|
|
41
|
+
if (!ptr.specRoot) return out;
|
|
42
|
+
|
|
43
|
+
const localRoot = join(cwd, '.sillyspec');
|
|
44
|
+
const localDbPath = join(localRoot, '.runtime', 'sillyspec.db');
|
|
45
|
+
if (resolve(ptr.specRoot) === resolve(localRoot)) return out; // 自指/同库
|
|
46
|
+
if (!existsSync(localDbPath)) return out; // 旧库无 DB → 无分裂
|
|
47
|
+
|
|
48
|
+
out.dualDb = true;
|
|
49
|
+
const currentSpecDir = this.pm._getSpecDir(cwd);
|
|
50
|
+
// 当前库:DB 已存在才读(readGlobal 走 pm 连接池,不新开连接;不存在时 readGlobal 会
|
|
51
|
+
// 隐式建库,须前置 existsSync 挡住——探测绝不产生写副作用)
|
|
52
|
+
let current = null;
|
|
53
|
+
if (existsSync(join(currentSpecDir, '.runtime', 'sillyspec.db'))) {
|
|
54
|
+
try {
|
|
55
|
+
const g = this.pm.readGlobal(cwd);
|
|
56
|
+
current = { activeChanges: (g && g.activeChanges) || [] };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
current = { activeChanges: [], unreadable: true, error: e.message };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const other = this._readActiveQuiet(localRoot, cwd);
|
|
62
|
+
const currentActive = current?.activeChanges || [];
|
|
63
|
+
const otherActive = other?.activeChanges || [];
|
|
64
|
+
|
|
65
|
+
out.lines.push(
|
|
66
|
+
`双进度库并存(接管指针 ${pointerPath} → ${currentSpecDir};切库前旧库残留 ${localRoot})` +
|
|
67
|
+
`——本次 check/repair 只作用于当前库,旧库状态不可见`
|
|
68
|
+
);
|
|
69
|
+
out.lines.push(
|
|
70
|
+
` 当前库活跃变更: ${currentActive.length > 0 ? currentActive.join('、') : '(无)'}${current?.unreadable ? '(读取失败:' + current.error + ')' : ''}`
|
|
71
|
+
);
|
|
72
|
+
out.lines.push(
|
|
73
|
+
` 旧库活跃变更: ${otherActive.length > 0 ? otherActive.join('、') : '(无)'}${other?.unreadable ? '(读取失败:' + other.error + ')' : ''}`
|
|
74
|
+
);
|
|
75
|
+
out.lines.push(
|
|
76
|
+
` 处理:确认权威库后收尾/归档弃用库的活跃变更;或 sillyspec platform pointer --cleanup 删指针后重跑平台 scan 指向正确库;临时读旧库用 --spec-dir ${localRoot}`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (currentActive.length > 0 && otherActive.length > 0) {
|
|
80
|
+
out.bothActive = true;
|
|
81
|
+
out.split = true;
|
|
82
|
+
}
|
|
83
|
+
// 目标变更只在旧库活跃(当前库查无)→ 本次操作几乎必然找错了库,最强信号
|
|
84
|
+
if (changeName && otherActive.includes(changeName) && !currentActive.includes(changeName)) {
|
|
85
|
+
out.targetInOther = true;
|
|
86
|
+
out.split = true;
|
|
87
|
+
out.lines.push(
|
|
88
|
+
` ⚠️ 目标变更 "${changeName}" 不在当前库,但旧库有同名活跃变更——本次操作错了库,修复应带上 --spec-dir ${localRoot}(或修正指针)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 只读读一个库的活跃变更清单(detectLibrarySplit 专用)。
|
|
96
|
+
* sillyspec.db 不存在返回 null(绝不新建库);打开/读取失败返回 { unreadable, error }
|
|
97
|
+
* 不上抛——分裂探测 fail-open,不阻断主流程。
|
|
98
|
+
*/
|
|
99
|
+
_readActiveQuiet(specDir, cwd) {
|
|
100
|
+
const dbPath = join(specDir, '.runtime', 'sillyspec.db');
|
|
101
|
+
if (!existsSync(dbPath)) return null;
|
|
102
|
+
let db = null;
|
|
103
|
+
try {
|
|
104
|
+
db = new DB(dbPath);
|
|
105
|
+
db.init();
|
|
106
|
+
const rows = db.getDb().prepare("SELECT name FROM changes WHERE status = 'active' ORDER BY name").all();
|
|
107
|
+
return { activeChanges: rows.map(r => r.name) };
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return { activeChanges: [], unreadable: true, error: e.message };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
14
113
|
/**
|
|
15
114
|
* 强制状态变更的审计记录(--force 逃生口专用)。
|
|
16
115
|
* 追加到 .runtime/audit.log,供人工/doctor 追溯"谁在什么时候绕过了校验"。
|
|
@@ -36,12 +135,26 @@ export class ConsistencyDoctor {
|
|
|
36
135
|
checkConsistency(cwd, changeName = null) {
|
|
37
136
|
const data = this.pm.read(cwd, changeName);
|
|
38
137
|
if (!data) {
|
|
39
|
-
|
|
138
|
+
// 双库分裂探测不依赖单变更 progress 可读(read 失败时更要看见另一库,坑
|
|
139
|
+
// progress-repair-dual-library-blind):无 progress 也要报分裂,不能只报「无法读取」
|
|
140
|
+
const splitNoData = this.detectLibrarySplit(cwd, changeName);
|
|
141
|
+
const issuesNoData = ['无法读取进度数据'];
|
|
142
|
+
if (splitNoData.targetInOther || splitNoData.bothActive) issuesNoData.push(...splitNoData.lines);
|
|
143
|
+
return { ok: false, issues: issuesNoData, warnings: splitNoData.dualDb && !splitNoData.split ? splitNoData.lines : [] };
|
|
40
144
|
}
|
|
41
145
|
|
|
42
146
|
const issues = [];
|
|
43
147
|
const warnings = [];
|
|
44
148
|
|
|
149
|
+
// 双库分裂(2026-09-04 ②):目标变更只在旧库 / 两库皆有活跃 → issue;
|
|
150
|
+
// 仅双库并存(单侧活跃)→ warning(平台模式本地库保留真实资产是容忍态,避免常态误报)
|
|
151
|
+
const libSplit = this.detectLibrarySplit(cwd, changeName);
|
|
152
|
+
if (libSplit.targetInOther || libSplit.bothActive) {
|
|
153
|
+
issues.push(...libSplit.lines);
|
|
154
|
+
} else if (libSplit.dualDb) {
|
|
155
|
+
warnings.push(...libSplit.lines);
|
|
156
|
+
}
|
|
157
|
+
|
|
45
158
|
for (const stageName of STAGE_ORDER) {
|
|
46
159
|
const sd = data.stages[stageName];
|
|
47
160
|
if (!sd) continue;
|
|
@@ -186,14 +299,20 @@ export class ConsistencyDoctor {
|
|
|
186
299
|
repairConsistency(cwd, opts = {}) {
|
|
187
300
|
const { apply = false, changeName = null } = opts;
|
|
188
301
|
|
|
302
|
+
// 双库分裂(2026-09-04 ②):repair 修不了「哪边是权威库」,只能把另一库的存在与活跃
|
|
303
|
+
// 变更亮出来进 manual 清单。放 read 之前——多活跃/读不了库时 read 早退,探测不能跟着瞎。
|
|
304
|
+
const libSplit = this.detectLibrarySplit(cwd, changeName);
|
|
305
|
+
const splitManual = libSplit.dualDb ? [...libSplit.lines] : [];
|
|
306
|
+
|
|
189
307
|
const data = this.pm.read(cwd, changeName);
|
|
190
308
|
if (!data) {
|
|
191
309
|
console.log('❌ 无法读取进度数据');
|
|
192
|
-
|
|
310
|
+
for (const m of splitManual) console.log(` - ${m}`);
|
|
311
|
+
return { fixable: [], manual: ['无法读取进度数据', ...splitManual], applied: [] };
|
|
193
312
|
}
|
|
194
313
|
|
|
195
314
|
const fixable = []; // { stage, action, description, apply: (data) => void }
|
|
196
|
-
const manual = []; // string
|
|
315
|
+
const manual = [...splitManual]; // string
|
|
197
316
|
|
|
198
317
|
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
199
318
|
|
|
@@ -7,8 +7,19 @@ import { mkdirSync, existsSync, readdirSync, readFileSync } from 'fs';
|
|
|
7
7
|
import { join, resolve, basename } from 'path';
|
|
8
8
|
import { writeAtomicSync } from '../fs-atomic.js';
|
|
9
9
|
import { runValidators } from '../stage-contract.js';
|
|
10
|
+
import { QUICK_SID_RE } from '../run/shared.js';
|
|
10
11
|
import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, STALL_WARN_DAYS, emptyStage } from './shared.js';
|
|
11
12
|
|
|
13
|
+
// quick 会话行(quick-<8hex>)按设计无实体 changes/ 目录(progress.js initChange 跳过建目录),
|
|
14
|
+
// 「DB active + changes/ 无同名目录」的 ghost 判定对它是类别错误——进行中的 quick 从写库那
|
|
15
|
+
// 一刻起即命中误报(2026-09-07 quick-inflight-ghost-misjudge,面板「一键清理」清了又长:
|
|
16
|
+
// 每次清理都成功,新 ghost 是清理后才启动的 quick)。show 与 overview 统一经此判定排除。
|
|
17
|
+
// 注意:排除的只是「判定」,doctor --cleanup-ghosts 仍保留 quick 行归档能力(QUICKLOG 已
|
|
18
|
+
// 完成但 DB 行未注销的收尾中断形态,那是兜底出口,不在此排除)。
|
|
19
|
+
function _isGhostChange(changesRoot, cn) {
|
|
20
|
+
return !QUICK_SID_RE.test(cn) && !existsSync(join(changesRoot, cn));
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
/**
|
|
13
24
|
* 归一化步骤的已记录等待回答(show 渲染用,跨会话恢复数据源)。
|
|
14
25
|
* waitAnswers 走 pm.read 时已 JSON.parse,但容错字符串/损坏;单值列 wait_answer(旧路径)并入第 1 轮。
|
|
@@ -209,7 +220,7 @@ export class StageMachine {
|
|
|
209
220
|
const pendingConflicts = this._listPendingConflicts(this.pm._getSpecDir(cwd));
|
|
210
221
|
for (const cn of changes) {
|
|
211
222
|
const data = this.pm.read(cwd, cn);
|
|
212
|
-
const dirMissing =
|
|
223
|
+
const dirMissing = _isGhostChange(changesRoot, cn);
|
|
213
224
|
if (!data) {
|
|
214
225
|
console.log(` 📂 ${cn} — (无法读取)${dirMissing ? ' ⚠️ 目录缺失(残留记录,sillyspec doctor --cleanup-ghosts --confirm 可归档清理)' : ''}`);
|
|
215
226
|
continue;
|
|
@@ -272,7 +283,7 @@ export class StageMachine {
|
|
|
272
283
|
const list = [];
|
|
273
284
|
for (const cn of changes) {
|
|
274
285
|
const data = this.pm.read(cwd, cn);
|
|
275
|
-
const dirMissing =
|
|
286
|
+
const dirMissing = _isGhostChange(changesRoot, cn);
|
|
276
287
|
const entry = {
|
|
277
288
|
name: cn,
|
|
278
289
|
readable: !!data,
|
package/src/progress.js
CHANGED
|
@@ -868,6 +868,9 @@ export class ProgressManager {
|
|
|
868
868
|
|
|
869
869
|
updateChangeMeta(cwd, changeName, meta) { return this._changeRegistry.updateChangeMeta(cwd, changeName, meta); }
|
|
870
870
|
|
|
871
|
+
// quick --done 兜底复用启动 ql-ID(坑 platform-takeover-phantom-progress-db 同日变体)
|
|
872
|
+
getQuicklogId(cwd, changeName) { return this._changeRegistry.getQuicklogId(cwd, changeName); }
|
|
873
|
+
|
|
871
874
|
// ── CLI 命令 ──
|
|
872
875
|
|
|
873
876
|
init(cwd) {
|