sillyspec 3.28.9 → 3.28.11
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-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +51 -3
- package/src/config-schema.js +51 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +94 -18
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +147 -2
- package/src/knowledge-classify.js +20 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +87 -0
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +240 -5
- package/src/run/command.js +25 -2
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +143 -11
- package/src/run/gate-snapshot.js +372 -2
- package/src/run/gates.js +130 -12
- package/src/run/prompt.js +166 -7
- package/src/run/shared.js +81 -1
- package/src/scope-audit.js +101 -31
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +314 -8
- package/src/stage-review-checklist.js +3 -1
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +62 -8
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +74 -10
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync-noise.js +57 -1
- package/src/sync.js +77 -4
- package/src/task-review.js +84 -29
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +439 -68
- package/src/verify-probes.js +1156 -45
- package/src/worktree-apply.js +323 -8
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
- package/src/write-audit.js +55 -0
package/src/worktree-apply.js
CHANGED
|
@@ -20,9 +20,10 @@ import { WorktreeManager } from './worktree.js';
|
|
|
20
20
|
import { getCrossWorktreeMeta, cleanupCrossWorktrees } from './worktree-cross.js';
|
|
21
21
|
import { parseFileChangeList, parseFileChangeListDetailed, pathMatches } from './change-list.js';
|
|
22
22
|
import { parseAllowedPaths, parseRepo } from './stages/plan-postcheck.js';
|
|
23
|
-
import { git, gitQuiet, safeGit } from './git-helper.js';
|
|
23
|
+
import { git, gitQuiet, safeGit, unquoteGitPath } from './git-helper.js';
|
|
24
24
|
import { resolveLatestExecuteRunId, resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } from './task-review.js';
|
|
25
25
|
import { collectActiveQuickGuardFiles } from './quicklog.js';
|
|
26
|
+
import { appendWriteAudit, WRITE_AUDIT_FILE_CAP } from './write-audit.js';
|
|
26
27
|
import { detectCommittedDrift, formatCommittedDriftWarning } from './run/concurrent-detect.js';
|
|
27
28
|
|
|
28
29
|
const CHANGES_REL = '.sillyspec/changes';
|
|
@@ -43,6 +44,40 @@ function consumeCleanupResult(cr, result) {
|
|
|
43
44
|
return cr;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* 工具/平台脚手架分类(scope-audit-cross-repo-blindness 改进点 1,2026-09-15):
|
|
49
|
+
* CLI/平台自己写进仓的文件,不是本变更的交付改动。分硬排(filterDeliverableFiles
|
|
50
|
+
* 直接滤除)与软桶(classifyToolScaffold 打标、scope-audit 归入「工具/平台设施」
|
|
51
|
+
* 不占「计划外」)两档——区分依据是 apply 语义:filterDeliverableFiles 的滤除会让
|
|
52
|
+
* apply 不回放该文件(坑 apply-glob-manifest 静默丢失类),故只有「绝无交付语义、
|
|
53
|
+
* 回放反而有害」的进硬排;可能被 design 清单正当声明的(SKILL.md/CLAUDE.md 改写
|
|
54
|
+
* 本身是合法交付)只打标不滤除。
|
|
55
|
+
*
|
|
56
|
+
* 硬排(platform 设施,绝不交付):
|
|
57
|
+
* - .worktrees/:嵌套 worktree 目录(cp 回放会把整个 worktree 副本塞进主仓)
|
|
58
|
+
* - .sillyspec-platform 系根标记文件(含 -cleaned / -managed / .json 变体):平台元数据
|
|
59
|
+
* - .sillyspec/knowledge/:CLI 自维护知识库(路由 INDEX 等,机器本地态)
|
|
60
|
+
* - .sillyspec/local.yaml:机器本地配置(gitignore 约定;回放会覆盖主仓本地配置)
|
|
61
|
+
* 软桶(tool 脚手架,可能正当交付 → 只打标):
|
|
62
|
+
* - .claude/skills/(含 sillyspec-*):CLI 自装 skill 脚手架
|
|
63
|
+
* - CLAUDE.md:CLI/平台维护的 agent 指引
|
|
64
|
+
* - attachments/:brainstorm 需求附件(docx/html/截图等流程产物)
|
|
65
|
+
* @param {string} f 仓库根相对路径(正斜杠)
|
|
66
|
+
* @returns {'platform'|'tool'|null}
|
|
67
|
+
*/
|
|
68
|
+
export function classifyToolScaffold(f) {
|
|
69
|
+
const p = String(f || '').replace(/\\/g, '/');
|
|
70
|
+
if (!p) return null;
|
|
71
|
+
if (p.startsWith('.worktrees/')
|
|
72
|
+
|| /^\.sillyspec-platform(?:-cleaned|-managed)?(?:\.json)?$/.test(p)
|
|
73
|
+
|| p.startsWith('.sillyspec/knowledge/')
|
|
74
|
+
|| p === '.sillyspec/local.yaml') return 'platform';
|
|
75
|
+
if (p.startsWith('.claude/skills/')
|
|
76
|
+
|| p === 'CLAUDE.md'
|
|
77
|
+
|| p.startsWith('attachments/')) return 'tool';
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
/**
|
|
47
82
|
* 过滤掉 worktree 基础设施文件(非交付物),让 apply 只关心真正的变更产出:
|
|
48
83
|
* - meta.json:worktree 元数据,baseline commit 中被跟踪、working-tree 被 CLI 改写
|
|
@@ -54,14 +89,19 @@ function consumeCleanupResult(cr, result) {
|
|
|
54
89
|
* - .sillyspec/.runtime/:运行时产物(进度库/锁/review 产物,非源码)。
|
|
55
90
|
* - .sillyspec/quicklog/:quicklog 条目(worktree 进度,非交付物)。
|
|
56
91
|
* 保留 .sillyspec/docs/(dogfood 模块规范文档 = 交付物,apply 回主仓)。
|
|
57
|
-
*
|
|
92
|
+
* - classifyToolScaffold 的 platform 档(.worktrees/、.sillyspec-platform 系根标记、
|
|
93
|
+
* .sillyspec/knowledge/、.sillyspec/local.yaml):工具/平台设施,绝无交付语义
|
|
94
|
+
* (scope-audit-cross-repo-blindness 改进点 1——锚点窗口内 CLI 自装文件曾全量
|
|
95
|
+
* 落「计划外」;回放有害类直接硬排)。
|
|
96
|
+
* 对 modified-tracked(git diff)与 untracked(ls-files --others)一视同仁。
|
|
58
97
|
*/
|
|
59
98
|
export function filterDeliverableFiles(files) {
|
|
60
99
|
return files.filter(f =>
|
|
61
100
|
!f.startsWith('.sillyspec/changes/') &&
|
|
62
101
|
!f.startsWith('.sillyspec/.runtime/') &&
|
|
63
102
|
!f.startsWith('.sillyspec/quicklog/') &&
|
|
64
|
-
f !== 'meta.json'
|
|
103
|
+
f !== 'meta.json' &&
|
|
104
|
+
classifyToolScaffold(f) !== 'platform'
|
|
65
105
|
);
|
|
66
106
|
}
|
|
67
107
|
|
|
@@ -424,6 +464,69 @@ export function chunkPaths(paths, maxChars = 8000) {
|
|
|
424
464
|
return batches
|
|
425
465
|
}
|
|
426
466
|
|
|
467
|
+
/**
|
|
468
|
+
* no-op 文件检测(FR-02 / D-002@v1,坑② assess-noop-blocked):判定候选文件中
|
|
469
|
+
* 「worktree 工作区内容 = 主仓 HEAD blob」者。execute 期主仓 HEAD 前进后,worktree 内
|
|
470
|
+
* 自救/重同步成主仓最新内容的文件对 baseline checkpoint diff 非空,apply 回主仓实为
|
|
471
|
+
* no-op,却被 assess 误判「变更文件超出 allowed_paths」BLOCKED——由 applyWorktree
|
|
472
|
+
* step 2 单点 choke point 调用(apply 与 assess checkOnly 复用同函数,自动同口径)。
|
|
473
|
+
*
|
|
474
|
+
* 比对:worktree 工作区 `git hash-object`(默认带 clean filter,与 tree blob 同口径)
|
|
475
|
+
* 按 argv 分批(chunkPaths,worktree.js _changesAlreadyOnMain 同型),批次输出按行序
|
|
476
|
+
* 拼接与候选索引对齐;主仓 HEAD blob 复用 getBlobHashMap(ls-tree)。路径对照键统一
|
|
477
|
+
* quotepath 剥引号(unquoteGitPath)+ 反斜杠归一正斜杠,仅用于比对不回写(changedFiles
|
|
478
|
+
* 路径字面保持现状口径,下游 pathMatches/gates/archive-delta 零改动)。
|
|
479
|
+
*
|
|
480
|
+
* fail-safe(D-002 故障面契约):hash-object 某批失败 / 行数不齐 / 整段异常 → 该批
|
|
481
|
+
* (或全部)保守不剔,保留 changed——退回现状误报而非误放行。每调用现算不缓存(主仓
|
|
482
|
+
* HEAD 在 assess 与 apply 间可能推进,以各自当下事实为准)。
|
|
483
|
+
*
|
|
484
|
+
* @param {string} projectRoot 主仓根(HEAD blob 读取处)
|
|
485
|
+
* @param {string} worktreePath worktree 根(工作区 hash-object 执行处)
|
|
486
|
+
* @param {string[]} candidates 候选文件(调用方已保证 worktree 工作区存在——删除类天然出局)
|
|
487
|
+
* @returns {string[]} 判定为 no-op 的文件子集(candidates 原字面);判定不可得时返回 []
|
|
488
|
+
*/
|
|
489
|
+
function detectNoOpFiles(projectRoot, worktreePath, candidates) {
|
|
490
|
+
try {
|
|
491
|
+
if (!candidates || candidates.length === 0) return [];
|
|
492
|
+
// 主仓 HEAD 树 blob map:路径不在 HEAD 树 → 不在 Map → 主仓无该路径的新文件天然非 no-op
|
|
493
|
+
const mainBlobs = getBlobHashMap(projectRoot, 'HEAD', candidates);
|
|
494
|
+
if (mainBlobs.size === 0) return [];
|
|
495
|
+
const pathKey = (p) => {
|
|
496
|
+
let s = String(p);
|
|
497
|
+
if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) s = unquoteGitPath(s.slice(1, -1));
|
|
498
|
+
return s.replace(/\\/g, '/');
|
|
499
|
+
};
|
|
500
|
+
const mainBlobByKey = new Map();
|
|
501
|
+
for (const [p, h] of mainBlobs) mainBlobByKey.set(pathKey(p), h);
|
|
502
|
+
const present = candidates.filter(f => mainBlobByKey.has(pathKey(f)));
|
|
503
|
+
if (present.length === 0) return [];
|
|
504
|
+
|
|
505
|
+
// worktree 工作区 blob:hash-object 按 argv 顺序逐行输出;某批失败(文件被并发删/权限等)
|
|
506
|
+
// → 占位空串填充保批次间索引对齐(空串永不等于 blob hash → 该批各文件保守不剔)
|
|
507
|
+
const wtHashes = [];
|
|
508
|
+
for (const batch of chunkPaths(present)) {
|
|
509
|
+
const out = gitQuiet(worktreePath, ['hash-object', '--', ...batch], { timeout: 30000 });
|
|
510
|
+
const lines = out === null ? null : out.split('\n');
|
|
511
|
+
if (!lines || lines.length !== batch.length) {
|
|
512
|
+
console.warn(`⚠️ no-op 检测:hash-object 批失败/行数不齐,该批 ${batch.length} 个文件保守保留在 changedFiles`);
|
|
513
|
+
for (let i = 0; i < batch.length; i++) wtHashes.push('');
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
wtHashes.push(...lines);
|
|
517
|
+
}
|
|
518
|
+
const noopFiles = [];
|
|
519
|
+
for (let i = 0; i < present.length; i++) {
|
|
520
|
+
if (wtHashes[i] && wtHashes[i] === mainBlobByKey.get(pathKey(present[i]))) noopFiles.push(present[i]);
|
|
521
|
+
}
|
|
522
|
+
return noopFiles;
|
|
523
|
+
} catch (e) {
|
|
524
|
+
// 意外异常整体保守不剔(fail-safe:宁可误报 BLOCKED,不可误放行剔除)
|
|
525
|
+
console.warn(`⚠️ no-op 文件检测失败(保守保留 changedFiles):${(e.message || '').split('\n')[0]}`);
|
|
526
|
+
return [];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
427
530
|
/**
|
|
428
531
|
* apply 的文件清单 = design §6 清单 ∪ plan TaskCard allowed_paths(execute 复盘 c)。
|
|
429
532
|
*
|
|
@@ -447,10 +550,25 @@ export function chunkPaths(paths, maxChars = 8000) {
|
|
|
447
550
|
* changes/ 实体在 specRoot,本地目录常为空——旧硬编码 CHANGES_REL 恒读空 → allow 集恒空 →
|
|
448
551
|
* Gate1 整批误拦(sync.js BUG-01 同族)。
|
|
449
552
|
*
|
|
553
|
+
* **.sillyspec/docs/ 条件白名单 + declaredFace 快照(D-003@v2,2026-09-16-friction5-hardening
|
|
554
|
+
* R3/FR-03)**:坑 apply-docs-sync-blocked(用户 2026-09-16 驾驭小结③)——filterDeliverableFiles
|
|
555
|
+
* 明确保留 `.sillyspec/docs/` 为交付物,而 allow 面(design §6 ∪ allowed_paths)不含 docs 时
|
|
556
|
+
* Gate1 又拦同一文件,「filter 保交付、Gate1 拦交付」两道口径矛盾,approved 文档同步照样 BLOCKED。
|
|
557
|
+
* 修正:聚合完成后仅当 main 仓声明面非空(mainSet.size > 0)才 mainSet.add('.sillyspec/docs/')
|
|
558
|
+
* (尾斜杠写法——pathMatches 目录前缀语义放行整目录,step3.5 getBlobHashMap 对目录条目 inert)。
|
|
559
|
+
* **必须条件加白**(design-grill M5):无条件加白会把空清单翻转成非空——design 与任务卡全缺的
|
|
560
|
+
* 存量变更从「fail-open(Gate1 跳过、patch 全量)」变成「docs-only 单条目面 → 非 docs 交付全
|
|
561
|
+
* BLOCKED + patch 静默收窄」,故空清单维持空(零行为变化)。加白前快照 declaredFace = 原
|
|
562
|
+
* 声明面(design §6 ∪ allowed_paths,不含白名单条目),以 Map 附挂属性 repoMap.declaredFace
|
|
563
|
+
* 带出(Map 方法全保留,既有调用点零破坏)——Gate1 hasAllowList 判定与 docs 越权审计报备均以
|
|
564
|
+
* declaredFace 为口径(M3:allowed_paths 已声明的 docs 文件不误报越权)。白名单只进 main 仓
|
|
565
|
+
* Set(`.sillyspec/docs/` 是主仓 specRoot 概念),跨仓 Map 切片不受影响。
|
|
566
|
+
*
|
|
450
567
|
* @param {string} projectRoot - 主仓库根
|
|
451
568
|
* @param {string} changeName - 变更名
|
|
452
569
|
* @param {{ specBase?: string }} [opts] specBase 显式传入(平台模式必传 specRoot)
|
|
453
|
-
* @returns {Map<string, Set<string>>} repoKey → 并集清单 Set(无 design 清单且无 task 卡片时为空 Map
|
|
570
|
+
* @returns {Map<string, Set<string>>} repoKey → 并集清单 Set(无 design 清单且无 task 卡片时为空 Map);
|
|
571
|
+
* 附挂属性 declaredFace: Set<string>——main 仓加白前原声明面(审计口径,docs 白名单条目不在其中)
|
|
454
572
|
*/
|
|
455
573
|
export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
456
574
|
const repoMap = new Map();
|
|
@@ -481,6 +599,12 @@ export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
|
481
599
|
for (const p of parseAllowedPaths(content)) repoSet.add(p);
|
|
482
600
|
}
|
|
483
601
|
}
|
|
602
|
+
|
|
603
|
+
// D-003@v2:.sillyspec/docs/ 条件加白——仅 main 仓声明面非空才加(空清单维持 fail-open,
|
|
604
|
+
// 见函数头 jsdoc)。加白前快照 declaredFace(原声明面)供 Gate1 审计口径用(Map 附挂带出)。
|
|
605
|
+
const declaredFace = new Set(mainSet);
|
|
606
|
+
if (mainSet.size > 0) mainSet.add('.sillyspec/docs/');
|
|
607
|
+
repoMap.declaredFace = declaredFace;
|
|
484
608
|
return repoMap;
|
|
485
609
|
}
|
|
486
610
|
|
|
@@ -578,8 +702,87 @@ function writeApplyManifest({ projectRoot, specBase, changeName, baseHash, files
|
|
|
578
702
|
return { written: true, file: manifestPath, files: entries };
|
|
579
703
|
}
|
|
580
704
|
|
|
705
|
+
// ── db/*.sql 声明门(2026-09-17-pass-cap-semantics task-04 / FR-05 / FR-06 / D-007 / D-012)──
|
|
706
|
+
// 文法实现单点:verify-probes 的 parseDbScriptDeclarations(X-03 双形态:声明行 + 回执
|
|
707
|
+
// command)/ parseHandoverRows(task-01 导出),禁二份副本。顶层 await 动态绑定
|
|
708
|
+
// (verify-probes :1630 stage-contract 同款先例;已核 verify-probes 静态闭包与其顶层
|
|
709
|
+
// await 链均不回指本文件,任一加载序无环)。绑定失败降级 null → 门 fail-open(warn
|
|
710
|
+
// 放行)——门自身异常不锁死 apply/归档主流程。
|
|
711
|
+
let dbScriptDeclareParser = null
|
|
712
|
+
let handoverRowsParser = null
|
|
713
|
+
try {
|
|
714
|
+
;({ parseDbScriptDeclarations: dbScriptDeclareParser, parseHandoverRows: handoverRowsParser } = await import('./verify-probes.js'))
|
|
715
|
+
} catch { /* fail-open:绑定失败时下方门函数降级 warn 放行 */ }
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* db/*.sql 执行声明对账门(task-04 兜底侧,apply 尾声与 archive --confirm 前置共用同一函数)。
|
|
719
|
+
* 纯「文件集 × 文本声明」对账,零连库(design 非目标——不做 information_schema 对账)。
|
|
720
|
+
*
|
|
721
|
+
* 口径:
|
|
722
|
+
* - 入参文件集 posix 化后取 ^db/ 开头且 .sql 结尾者(与声明文法产出 db/<file>.sql 同形,
|
|
723
|
+
* 对账零变换;深路径 src/.../db/x.sql 不进门——声明文法同样不认该形态,两侧一致无死锁);
|
|
724
|
+
* - 声明集 = parseDbScriptDeclarations(verifyMd),verifyMd 自 <changeDir>/verify-result.md
|
|
725
|
+
* 读取(changeDir 经 resolveActiveOrArchiveChangeDir 归档感知,specBase 缺省时与
|
|
726
|
+
* writeApplyManifest 同链兜底:透传 specBase > 平台指针 > 本地 .sillyspec);
|
|
727
|
+
* - verify-result.md 缺失(ENOENT)= 空声明集 → 有 db 交付即拦(fail-closed 正常判定路径);
|
|
728
|
+
* - 互斥分支(D-005@v2:db-script 恒 blocking):声明集全命中 × handover 存在 db-script
|
|
729
|
+
* 类型行 → 两面矛盾 error。数据源 facts.handover(verify-facts.json,{ count, items[] }
|
|
730
|
+
* 形态)优先,缺 facts 时 parseHandoverRows(verifyMd) 兜底,两源任一命中即拦。
|
|
731
|
+
*
|
|
732
|
+
* fail 分布:正常判定路径 fail-closed(缺声明/互斥 → ok:false + 修复指引);门自身异常
|
|
733
|
+
* (绑定失败 / verifyMd 读取抛非 ENOENT 异常 / facts 解析异常走 md 兜底)fail-open
|
|
734
|
+
* (ok:true + warning)。
|
|
735
|
+
*
|
|
736
|
+
* @param {{ projectRoot: string, specBase?: string, changeName: string, files: string[] }} p
|
|
737
|
+
* @returns {{ ok: true, warning?: string } | { ok: false, error: string }}
|
|
738
|
+
*/
|
|
739
|
+
export function checkDbScriptDeclarationGate({ projectRoot, specBase, changeName, files }) {
|
|
740
|
+
const dbFiles = [...new Set((Array.isArray(files) ? files : [])
|
|
741
|
+
.map((f) => String(f || '').replace(/\\/g, '/')).filter(Boolean))]
|
|
742
|
+
.filter((f) => /^db\/[\w./-]+\.sql$/.test(f)).sort()
|
|
743
|
+
if (dbFiles.length === 0) return { ok: true } // 无 db/*.sql 交集 → 零行为变化(存量兼容)
|
|
744
|
+
if (!dbScriptDeclareParser) {
|
|
745
|
+
return { ok: true, warning: `db 声明门降级放行(fail-open):verify-probes 文法绑定不可用——本次未对账以下 db 脚本执行声明,请人工确认已对目标库执行:${dbFiles.join(', ')}` }
|
|
746
|
+
}
|
|
747
|
+
const base = specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec')
|
|
748
|
+
const changeDir = resolveActiveOrArchiveChangeDir(base, changeName)
|
|
749
|
+
let verifyMd = ''
|
|
750
|
+
try {
|
|
751
|
+
verifyMd = readFileSync(join(changeDir, 'verify-result.md'), 'utf8')
|
|
752
|
+
} catch (e) {
|
|
753
|
+
if (e && e.code === 'ENOENT') verifyMd = '' // verify-result.md 缺失 = 空声明集(走下方缺声明拦截)
|
|
754
|
+
else return { ok: true, warning: `db 声明门降级放行(fail-open):verify-result.md 读取异常(${(e && e.message) || e})——本次未对账以下 db 脚本执行声明,请人工确认:${dbFiles.join(', ')}` }
|
|
755
|
+
}
|
|
756
|
+
const declared = new Set(dbScriptDeclareParser(verifyMd))
|
|
757
|
+
const missing = dbFiles.filter((f) => !declared.has(f))
|
|
758
|
+
if (missing.length > 0) {
|
|
759
|
+
return { ok: false, error: [
|
|
760
|
+
`db 脚本执行声明缺失:以下 ${missing.length} 个 db/*.sql 交付文件未在 verify-result.md 声明已对目标库执行(fix.sql 双门兜底,D-007/D-012——覆盖 verify 后新增 sql 的时序窗口):`,
|
|
761
|
+
...missing.map((f) => ` - ${f}`),
|
|
762
|
+
`修复:先在 ${join(changeDir, 'verify-result.md').replace(/\\/g, '/')} 声明已执行——补「已对目标库执行:db/<file>.sql」声明行,或「## 集成验证回执」条目 command 含 db/<file>.sql(X-03 双形态,含 log 路径可选),补后重跑本命令。`,
|
|
763
|
+
].join('\n') }
|
|
764
|
+
}
|
|
765
|
+
// 互斥分支:声明全在场 × handover db-script 行在场 = 「已执行」vs「移交待执行」两面矛盾
|
|
766
|
+
let handoverItems = null
|
|
767
|
+
try {
|
|
768
|
+
const facts = JSON.parse(readFileSync(join(changeDir, 'verify-facts.json'), 'utf8'))
|
|
769
|
+
if (facts && facts.handover && Array.isArray(facts.handover.items)) handoverItems = facts.handover.items
|
|
770
|
+
} catch { /* facts 缺失/损坏 → parseHandoverRows(verifyMd) 兜底 */ }
|
|
771
|
+
if (!handoverItems) {
|
|
772
|
+
try { handoverItems = handoverRowsParser ? handoverRowsParser(verifyMd) : [] } catch { handoverItems = [] }
|
|
773
|
+
}
|
|
774
|
+
if (handoverItems.some((h) => h && String(h.type || '').toLowerCase().replace(/[\s_]+/g, '-') === 'db-script')) {
|
|
775
|
+
return { ok: false, error: [
|
|
776
|
+
`db-script 移交项与执行声明互斥(D-005@v2):verify-result.md 已声明以下 db 脚本对目标库执行,但「移交项(结构化)」存在 type=db-script 行(承认该脚本待执行)——两面矛盾,二选一以真实状态为准:`,
|
|
777
|
+
...dbFiles.map((f) => ` - ${f}`),
|
|
778
|
+
`修复:确已执行 → 从 verify-result.md「## 移交项(结构化)」删除该 db-script 行后重跑;确未执行 → 删除对应「已对目标库执行」声明行/回执条目后重跑(届时按缺声明门指引处理)。`,
|
|
779
|
+
].join('\n') }
|
|
780
|
+
}
|
|
781
|
+
return { ok: true }
|
|
782
|
+
}
|
|
783
|
+
|
|
581
784
|
/**
|
|
582
|
-
* 坑 mixed-baseline-drift-hint(ql-20260915-004):apply
|
|
785
|
+
* 坑 mixed-baseline-drift-hint(ql-20260915-004):apply 尾声混合基线 advisory。
|
|
583
786
|
*
|
|
584
787
|
* 主仓自本变更基点(baseHash)后的已提交推进若触及本变更相关文件(含 .test./.spec. 变体),
|
|
585
788
|
* worktree 快照内验证过的测试断言可能基于旧内容——apply 落地的是混合基线,提示 agent 合并态
|
|
@@ -879,7 +1082,14 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
879
1082
|
}
|
|
880
1083
|
}
|
|
881
1084
|
const untrackedRaw = gitQuiet(cm.worktreePath, ['ls-files', '--others', '--exclude-standard']) || '';
|
|
882
|
-
|
|
1085
|
+
const allFiles = [...new Set([...statusFiles, ...untrackedRaw.split('\n').filter(Boolean)])];
|
|
1086
|
+
changedFiles = filterDeliverableFiles(allFiles);
|
|
1087
|
+
// 硬排可见性(scope-audit-cross-repo-blindness 改进点 1):被滤除的平台设施文件列
|
|
1088
|
+
// warning 不静默——apply 不回放它们;若某文件确为交付物(极少),人工落主仓。
|
|
1089
|
+
const dropped = allFiles.filter(f => !changedFiles.includes(f) && classifyToolScaffold(f) === 'platform');
|
|
1090
|
+
if (dropped.length > 0) {
|
|
1091
|
+
out.warnings.push(`跨仓 ${repoKey}:${dropped.length} 个工具/平台设施文件未纳入 apply(${dropped.slice(0, 5).join(', ')}${dropped.length > 5 ? ' 等' : ''})——非交付物不回放,确需交付请人工处理`);
|
|
1092
|
+
}
|
|
883
1093
|
} catch (e) {
|
|
884
1094
|
out.errors.push(`跨仓 ${repoKey}:获取变更文件列表失败(${cm.worktreePath}): ${e.message}`);
|
|
885
1095
|
continue;
|
|
@@ -1079,6 +1289,16 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1079
1289
|
result.errors.push(...crossApplied.errors);
|
|
1080
1290
|
return result; // 跨仓未落地 = 该仓 task 实际未完成,主仓 apply 不可继续
|
|
1081
1291
|
}
|
|
1292
|
+
// --- db/*.sql 声明门·跨仓面(task-04 / FR-05):跨仓 apply 成功面 out.applied[].changedFiles
|
|
1293
|
+
// 统一过同一门(声明文法跨仓同源——verify-result.md 仍在主仓 changes/<name>/ 下)。跨仓
|
|
1294
|
+
// apply 物理动作已发生,门拦的是流程收口(ok 不置 true + 修复指引,补声明重跑幂等)。
|
|
1295
|
+
const crossDbFiles = crossApplied.applied.flatMap((a) => (a && Array.isArray(a.changedFiles) ? a.changedFiles : []))
|
|
1296
|
+
const dbGateCross = checkDbScriptDeclarationGate({
|
|
1297
|
+
projectRoot, changeName, files: crossDbFiles,
|
|
1298
|
+
specBase: (ctx.platformOpts && ctx.platformOpts.specRoot) || undefined,
|
|
1299
|
+
})
|
|
1300
|
+
if (!dbGateCross.ok) { result.errors.push(dbGateCross.error); return result }
|
|
1301
|
+
if (dbGateCross.warning) result.warnings = (result.warnings || []).concat([dbGateCross.warning])
|
|
1082
1302
|
}
|
|
1083
1303
|
const cross = validateCrossRepoNoOp(ctx, projectRoot, changeName);
|
|
1084
1304
|
for (const e of cross.errors) result.errors.push(e);
|
|
@@ -1159,7 +1379,41 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1159
1379
|
// 排除 worktree 基础设施文件(meta.json / .sillyspec/,见 filterDeliverableFiles)。
|
|
1160
1380
|
// 对 modified-tracked(statusFiles)与 untracked 一视同仁——否则 modified 的 meta.json
|
|
1161
1381
|
// 会被误算入 changedFiles,触发 design.md 清单校验失败(assess 恒 BLOCKED)。
|
|
1162
|
-
|
|
1382
|
+
const allChangedRaw = [...new Set([...statusFiles, ...untrackedFiles])];
|
|
1383
|
+
changedFiles = filterDeliverableFiles(allChangedRaw);
|
|
1384
|
+
|
|
1385
|
+
// no-op 文件剔除(FR-02 / D-002@v1):主仓 HEAD 在 execute 期间前进后,worktree 内
|
|
1386
|
+
// 「内容=主仓 HEAD」的自救/重同步文件对 deliverableBase diff 非空,apply 回主仓实为
|
|
1387
|
+
// no-op,却被 assess 误判「变更文件超出 allowed_paths」BLOCKED。候选收窄:allChangedRaw
|
|
1388
|
+
// 中 worktree 工作区存在者(existsSync,删除类天然出局)∩ 主仓 HEAD 树存在者(helper
|
|
1389
|
+
// 内反查);判定失败保守不剔。diff-size/Gate 判定消费的 changedFiles 自然跟随收窄,
|
|
1390
|
+
// patch 面(resolvePatchFiles ⊆ changedFiles)同步不再回放该类文件。
|
|
1391
|
+
const noopCandidates = allChangedRaw.filter(f => existsSync(join(worktreePath, f)));
|
|
1392
|
+
const noopFiles = noopCandidates.length > 0 ? detectNoOpFiles(projectRoot, worktreePath, noopCandidates) : [];
|
|
1393
|
+
// 只报实际从三集剔出的文件(候选含 filterDeliverableFiles 已硬排的平台设施时,
|
|
1394
|
+
// 那部分本就不在 changedFiles,不计入告警面)
|
|
1395
|
+
const changedSetForNoop = new Set([...changedFiles, ...deletedFiles, ...absentAfterMerge]);
|
|
1396
|
+
const removedNoopFiles = noopFiles.filter(f => changedSetForNoop.has(f));
|
|
1397
|
+
if (removedNoopFiles.length > 0) {
|
|
1398
|
+
const noopSet = new Set(removedNoopFiles);
|
|
1399
|
+
changedFiles = changedFiles.filter(f => !noopSet.has(f));
|
|
1400
|
+
// 防御性剔除(候选已限工作区存在,正常不命中后两集;过滤保三集不变式)
|
|
1401
|
+
for (let i = deletedFiles.length - 1; i >= 0; i--) if (noopSet.has(deletedFiles[i])) deletedFiles.splice(i, 1);
|
|
1402
|
+
for (let i = absentAfterMerge.length - 1; i >= 0; i--) if (noopSet.has(absentAfterMerge[i])) absentAfterMerge.splice(i, 1);
|
|
1403
|
+
result.warnings = result.warnings || [];
|
|
1404
|
+
result.warnings.push(
|
|
1405
|
+
`${removedNoopFiles.length} 个文件内容与主仓 HEAD 一致(apply 为 no-op,不计入 changedFiles):${removedNoopFiles.slice(0, 5).join(', ')}${removedNoopFiles.length > 5 ? ' 等' : ''}`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// 硬排可见性(scope-audit-cross-repo-blindness 改进点 1):平台设施被滤出不静默——
|
|
1410
|
+
// apply 不回放它们(.worktrees//.sillyspec-platform*/knowledge/local.yaml);确为交付物
|
|
1411
|
+
// 的极少数场景(如刻意改 knowledge 模板)人工落主仓。
|
|
1412
|
+
const droppedScaffold = allChangedRaw.filter(f => !changedFiles.includes(f) && classifyToolScaffold(f) === 'platform');
|
|
1413
|
+
if (droppedScaffold.length > 0) {
|
|
1414
|
+
result.warnings = result.warnings || [];
|
|
1415
|
+
result.warnings.push(`${droppedScaffold.length} 个工具/平台设施文件未纳入 apply(${droppedScaffold.slice(0, 5).join(', ')}${droppedScaffold.length > 5 ? ' 等' : ''})——非交付物不回放,确需交付请人工处理`);
|
|
1416
|
+
}
|
|
1163
1417
|
} catch (e) {
|
|
1164
1418
|
result.errors.push(`获取变更文件列表失败: ${e.message}`);
|
|
1165
1419
|
return result;
|
|
@@ -1216,7 +1470,29 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1216
1470
|
]);
|
|
1217
1471
|
}
|
|
1218
1472
|
const allowSet = allowMap.get('main') || new Set();
|
|
1219
|
-
|
|
1473
|
+
// D-003@v2(R3/FR-03):hasAllowList 与 docs 审计报备以 declaredFace(design §6 ∪
|
|
1474
|
+
// allowed_paths 原面,resolveApplyAllowSet 加白前快照、Map 附挂带出)为口径——Gate1 违规
|
|
1475
|
+
// 判定仍消费 allowSet(已含 .sillyspec/docs/ 白名单条目 = 放行 docs)。旧调用路径拿不到
|
|
1476
|
+
// declaredFace 时回退 mainSet 判定保持兼容(条件加白下两者空性等价,纯口径统一)。
|
|
1477
|
+
const declaredFace = (allowMap && allowMap.declaredFace instanceof Set) ? allowMap.declaredFace : allowSet;
|
|
1478
|
+
const hasAllowList = declaredFace.size > 0;
|
|
1479
|
+
|
|
1480
|
+
// --- 3c. docs 白名单审计报备(D-003@v2,风险 R-01 应对)---
|
|
1481
|
+
// 白名单放行是有意的语义放宽(approved 文档同步不再拦),审计面由此报备行保住:实际
|
|
1482
|
+
// changedFiles 中以 .sillyspec/docs/ 开头且不被 declaredFace 任一条目 pathMatches 覆盖的
|
|
1483
|
+
// 文件 = 纯靠白名单放行(design §6 / 任务卡 allowed_paths 均未声明)——逐名列出供追认/
|
|
1484
|
+
// 审计,docs-check 独立校验内容质量。declaredFace 命中(已声明)不报备(M3 口径)。
|
|
1485
|
+
// 无清单(fail-open)时白名单未生效,不报备(与旧版零行为差异)。
|
|
1486
|
+
{
|
|
1487
|
+
const docsAdmitted = hasAllowList
|
|
1488
|
+
? changedFiles.filter(f => f.startsWith('.sillyspec/docs/') && ![...declaredFace].some(ap => pathMatches(f, ap)))
|
|
1489
|
+
: [];
|
|
1490
|
+
if (docsAdmitted.length > 0) {
|
|
1491
|
+
result.warnings = (result.warnings || []).concat([
|
|
1492
|
+
`${docsAdmitted.length} 个 .sillyspec/docs/ 文件经模块文档白名单放行(未在 design §6/任务卡 allowed_paths 声明,docs-check 独立校验内容):${docsAdmitted.join('、')}`
|
|
1493
|
+
]);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1220
1496
|
|
|
1221
1497
|
// --- 3.5 主干已提交推进检测:hashMismatch 前移(Grill P0 修复,design §step 顺序修正)---
|
|
1222
1498
|
// 原在 step5b,但 step4.5/5a dirty 拦截短路在 step5b 之前,致 rescue 拿不到 hashMismatchFiles
|
|
@@ -1665,6 +1941,15 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1665
1941
|
const patchContent = Buffer.concat(patchParts);
|
|
1666
1942
|
if (patchContent.length === 0) {
|
|
1667
1943
|
// patch 为空(清单中部分文件可能没实际变更)
|
|
1944
|
+
// --- db/*.sql 声明门·出口①(task-04 / FR-05):空 patch 出口的 merge 写回面过门 ---
|
|
1945
|
+
//(写回批末 add 已先行,本出口唯一落盘面 = mergedDirtyFiles ∪ mergedMismatchFiles,
|
|
1946
|
+
//与下方 manifest files 同口径;门失败 → result.ok 不置 true,走既有 error 通道)
|
|
1947
|
+
const dbGateEmpty = checkDbScriptDeclarationGate({
|
|
1948
|
+
projectRoot, specBase: allowSpecBase, changeName,
|
|
1949
|
+
files: [...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])],
|
|
1950
|
+
})
|
|
1951
|
+
if (!dbGateEmpty.ok) { result.errors.push(dbGateEmpty.error); return result }
|
|
1952
|
+
if (dbGateEmpty.warning) result.warnings = (result.warnings || []).concat([dbGateEmpty.warning])
|
|
1668
1953
|
result.ok = true;
|
|
1669
1954
|
// apply-manifest(task-01 出口③变体):4.5 三方合并全部消解重叠时,merge 写回面是
|
|
1670
1955
|
// 本出口唯一落盘面——manifest 仍须覆盖(写回批末 add 已先行,指纹取 staged 态)。
|
|
@@ -1734,6 +2019,18 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1734
2019
|
return result;
|
|
1735
2020
|
}
|
|
1736
2021
|
|
|
2022
|
+
// --- 7.1 db/*.sql 声明门·出口②(task-04 / FR-05 / D-007 / D-012 兜底第一门)---
|
|
2023
|
+
// patch 主路径 apply 文件集(patch 实际落盘集 ∪ 4.5 三方合并写回集,与 7.2 manifestFace
|
|
2024
|
+
// 同口径)∩ db/*.sql 逐文件对账 verify-result.md 执行声明(X-03 双形态文法,verify-probes
|
|
2025
|
+
// 单点实现)——缺失/与 db-script handover 互斥 → result.errors 阻断(result.ok 不置 true,
|
|
2026
|
+
// 走既有 apply error 通道,不抛异常);无 db/*.sql 交集零行为变化;门异常 fail-open。
|
|
2027
|
+
const dbGateMain = checkDbScriptDeclarationGate({
|
|
2028
|
+
projectRoot, specBase: allowSpecBase, changeName,
|
|
2029
|
+
files: [...patchFiles, ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])],
|
|
2030
|
+
})
|
|
2031
|
+
if (!dbGateMain.ok) { result.errors.push(dbGateMain.error); return result }
|
|
2032
|
+
if (dbGateMain.warning) result.warnings = (result.warnings || []).concat([dbGateMain.warning])
|
|
2033
|
+
|
|
1737
2034
|
result.ok = true;
|
|
1738
2035
|
|
|
1739
2036
|
// --- 7.2 apply 成功尾声写 apply-manifest.json(task-01 / FR-02 / D-001@v1 后半)---
|
|
@@ -2257,6 +2554,17 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
2257
2554
|
return result;
|
|
2258
2555
|
}
|
|
2259
2556
|
|
|
2557
|
+
// --- db/*.sql 声明门·出口③(task-04 / FR-05):merge 成功出口——changedFiles(落地校验过
|
|
2558
|
+
// 的交付集)∪ 两个三方合并写回面(与下方 manifestFace 同口径)过同一门;门失败 →
|
|
2559
|
+
// result.ok 不置 true,走既有 error 通道。specBase 为 applyWorktree 透传值(可能 undefined,
|
|
2560
|
+
// 门函数内 projectRoot 兜底链)。---
|
|
2561
|
+
const dbGateMerge = checkDbScriptDeclarationGate({
|
|
2562
|
+
projectRoot, specBase, changeName,
|
|
2563
|
+
files: [...(result.changedFiles || []), ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])],
|
|
2564
|
+
})
|
|
2565
|
+
if (!dbGateMerge.ok) { result.errors.push(dbGateMerge.error); return result }
|
|
2566
|
+
if (dbGateMerge.warning) result.warnings = (result.warnings || []).concat([dbGateMerge.warning])
|
|
2567
|
+
|
|
2260
2568
|
result.ok = true;
|
|
2261
2569
|
// apply 成功尾声写 apply-manifest.json(task-01 出口②:applyWorktree 显式 --merge 与
|
|
2262
2570
|
// ENOBUFS 自动降级两条提前 return 入口共用本成功出口)。merge 已 commit → staged 态 =
|
|
@@ -2267,6 +2575,13 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
2267
2575
|
// manifestFace 提升到 try 外(坑 mixed-baseline-drift-hint ql-20260915-004:drift 提示在
|
|
2268
2576
|
// manifest 写点后消费同面,try 内 const 出块即失域)
|
|
2269
2577
|
const manifestFace = [...new Set([...(result.changedFiles || []), ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])];
|
|
2578
|
+
// 批量写入留痕(2026-09-17 用户反馈⑥②法证收口):apply 是主仓面的批量写入(patch/merge
|
|
2579
|
+
// 落盘)——审计行记 时间/触发面/交付文件集/删除面(fail-open,specBase 与 manifest 同源)。
|
|
2580
|
+
try {
|
|
2581
|
+
const auditFace = manifestFace.slice(0, WRITE_AUDIT_FILE_CAP)
|
|
2582
|
+
.concat(manifestFace.length > WRITE_AUDIT_FILE_CAP ? [`…+${manifestFace.length - WRITE_AUDIT_FILE_CAP}`] : []);
|
|
2583
|
+
appendWriteAudit(specBase, { via: 'worktree-apply', change: changeName, files: auditFace, deleted: (result.deletedFiles || []).length, merge: true });
|
|
2584
|
+
} catch { /* 审计 fail-open */ }
|
|
2270
2585
|
try {
|
|
2271
2586
|
const mf = writeApplyManifest({
|
|
2272
2587
|
projectRoot, specBase, changeName,
|
package/src/worktree-deps.js
CHANGED
|
@@ -616,10 +616,33 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
616
616
|
// generic monorepo(如 multi-agent-platform)worktree 子模块 frontend/sillyhub-daemon 无 node_modules,
|
|
617
617
|
// 跑 pnpm test 失败。读 local.yaml modules 块,对 nodejs 子模块 tryLink main 的 node_modules。
|
|
618
618
|
const modulePaths = extractModulePaths(yamlText);
|
|
619
|
+
// 注册仓根集合(坑 deps-sibling-repo-false-reject,2026-09-15 EHS 生产实证:modules 里登记的
|
|
620
|
+
// `../sub-grid-security` 是跨仓测试命令的合法配置,被「path 越界拒绝 link」当攻击面误报——该
|
|
621
|
+
// 目录本来就不在 worktree 内、无 main/worktree 双份语义,node_modules link 天然不适用,正确
|
|
622
|
+
// 动作是分类跳过+准确理由。未注册的越界路径维持 fail-closed 拒绝)。轻量解析(不引
|
|
623
|
+
// plan-postcheck 依赖边,条目形状 <key>: <path> 与 parseRepoRegistry 对齐)。
|
|
624
|
+
const registeredRepoRoots = (() => {
|
|
625
|
+
const roots = [];
|
|
626
|
+
const lines = String(yamlText || '').replace(/\r\n?/g, '\n').split('\n');
|
|
627
|
+
const start = lines.findIndex(l => /^repos:\s*(?:#.*)?$/.test(l));
|
|
628
|
+
if (start === -1) return roots;
|
|
629
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
630
|
+
if (lines[i].length > 0 && !/\s/.test(lines[i][0]) && lines[i].trim() !== '' && !lines[i].startsWith('#')) break;
|
|
631
|
+
const m = lines[i].match(/^\s+([A-Za-z0-9_.\-]+):\s*(\S+)/);
|
|
632
|
+
if (m && !m[2].startsWith('#')) roots.push(m[2].replace(/^['"]|['"]$/g, ''));
|
|
633
|
+
}
|
|
634
|
+
return roots;
|
|
635
|
+
})();
|
|
619
636
|
const moduleResults = [];
|
|
620
637
|
for (const mp of modulePaths) {
|
|
621
638
|
if (!isSafeModulePath(mp)) {
|
|
622
|
-
|
|
639
|
+
const resolvedMp = resolvePath(worktreePath, mp);
|
|
640
|
+
const hitRepo = registeredRepoRoots.some(rRoot => {
|
|
641
|
+
try { return resolvePath(mainCwd || worktreePath, rRoot) === resolvedMp } catch { return false }
|
|
642
|
+
});
|
|
643
|
+
moduleResults.push({ path: mp, status: 'skipped', reason: hitRepo
|
|
644
|
+
? '跨仓注册仓路径(repos 已注册)——不在 worktree 内,node_modules link 不适用;其测试命令经 modules.<name>.test 由 verify 直接在该仓执行'
|
|
645
|
+
: 'path 越界(绝对路径或 .. 段),拒绝 link' });
|
|
623
646
|
continue;
|
|
624
647
|
}
|
|
625
648
|
const wtDir = join(worktreePath, mp);
|