sillyspec 3.28.10 → 3.28.12
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/.husky/pre-commit +4 -0
- package/.husky/pre-push +3 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/change-risk-profile.js +76 -6
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +23 -0
- package/src/design-facts.js +46 -9
- package/src/diagnostic-codes.js +95 -0
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/doctor-diagnostics.js +141 -1
- 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 +129 -1
- package/src/machine-interface.js +63 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +738 -8
- package/src/stage-review-checklist.js +16 -2
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +8 -3
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +95 -10
- package/src/verify-postcheck.js +346 -28
- package/src/verify-probes.js +1221 -46
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
package/src/worktree-apply.js
CHANGED
|
@@ -23,6 +23,7 @@ import { parseAllowedPaths, parseRepo } from './stages/plan-postcheck.js';
|
|
|
23
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';
|
|
@@ -549,10 +550,25 @@ function detectNoOpFiles(projectRoot, worktreePath, candidates) {
|
|
|
549
550
|
* changes/ 实体在 specRoot,本地目录常为空——旧硬编码 CHANGES_REL 恒读空 → allow 集恒空 →
|
|
550
551
|
* Gate1 整批误拦(sync.js BUG-01 同族)。
|
|
551
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
|
+
*
|
|
552
567
|
* @param {string} projectRoot - 主仓库根
|
|
553
568
|
* @param {string} changeName - 变更名
|
|
554
569
|
* @param {{ specBase?: string }} [opts] specBase 显式传入(平台模式必传 specRoot)
|
|
555
|
-
* @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 白名单条目不在其中)
|
|
556
572
|
*/
|
|
557
573
|
export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
558
574
|
const repoMap = new Map();
|
|
@@ -583,6 +599,12 @@ export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
|
583
599
|
for (const p of parseAllowedPaths(content)) repoSet.add(p);
|
|
584
600
|
}
|
|
585
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;
|
|
586
608
|
return repoMap;
|
|
587
609
|
}
|
|
588
610
|
|
|
@@ -680,8 +702,87 @@ function writeApplyManifest({ projectRoot, specBase, changeName, baseHash, files
|
|
|
680
702
|
return { written: true, file: manifestPath, files: entries };
|
|
681
703
|
}
|
|
682
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
|
+
|
|
683
717
|
/**
|
|
684
|
-
*
|
|
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
|
+
|
|
784
|
+
/**
|
|
785
|
+
* 坑 mixed-baseline-drift-hint(ql-20260915-004):apply 尾声混合基线 advisory。
|
|
685
786
|
*
|
|
686
787
|
* 主仓自本变更基点(baseHash)后的已提交推进若触及本变更相关文件(含 .test./.spec. 变体),
|
|
687
788
|
* worktree 快照内验证过的测试断言可能基于旧内容——apply 落地的是混合基线,提示 agent 合并态
|
|
@@ -1188,6 +1289,16 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1188
1289
|
result.errors.push(...crossApplied.errors);
|
|
1189
1290
|
return result; // 跨仓未落地 = 该仓 task 实际未完成,主仓 apply 不可继续
|
|
1190
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])
|
|
1191
1302
|
}
|
|
1192
1303
|
const cross = validateCrossRepoNoOp(ctx, projectRoot, changeName);
|
|
1193
1304
|
for (const e of cross.errors) result.errors.push(e);
|
|
@@ -1359,7 +1470,29 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1359
1470
|
]);
|
|
1360
1471
|
}
|
|
1361
1472
|
const allowSet = allowMap.get('main') || new Set();
|
|
1362
|
-
|
|
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
|
+
}
|
|
1363
1496
|
|
|
1364
1497
|
// --- 3.5 主干已提交推进检测:hashMismatch 前移(Grill P0 修复,design §step 顺序修正)---
|
|
1365
1498
|
// 原在 step5b,但 step4.5/5a dirty 拦截短路在 step5b 之前,致 rescue 拿不到 hashMismatchFiles
|
|
@@ -1808,6 +1941,15 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1808
1941
|
const patchContent = Buffer.concat(patchParts);
|
|
1809
1942
|
if (patchContent.length === 0) {
|
|
1810
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])
|
|
1811
1953
|
result.ok = true;
|
|
1812
1954
|
// apply-manifest(task-01 出口③变体):4.5 三方合并全部消解重叠时,merge 写回面是
|
|
1813
1955
|
// 本出口唯一落盘面——manifest 仍须覆盖(写回批末 add 已先行,指纹取 staged 态)。
|
|
@@ -1877,6 +2019,18 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1877
2019
|
return result;
|
|
1878
2020
|
}
|
|
1879
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
|
+
|
|
1880
2034
|
result.ok = true;
|
|
1881
2035
|
|
|
1882
2036
|
// --- 7.2 apply 成功尾声写 apply-manifest.json(task-01 / FR-02 / D-001@v1 后半)---
|
|
@@ -2400,6 +2554,17 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
2400
2554
|
return result;
|
|
2401
2555
|
}
|
|
2402
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
|
+
|
|
2403
2568
|
result.ok = true;
|
|
2404
2569
|
// apply 成功尾声写 apply-manifest.json(task-01 出口②:applyWorktree 显式 --merge 与
|
|
2405
2570
|
// ENOBUFS 自动降级两条提前 return 入口共用本成功出口)。merge 已 commit → staged 态 =
|
|
@@ -2410,6 +2575,13 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
2410
2575
|
// manifestFace 提升到 try 外(坑 mixed-baseline-drift-hint ql-20260915-004:drift 提示在
|
|
2411
2576
|
// manifest 写点后消费同面,try 内 const 出块即失域)
|
|
2412
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 */ }
|
|
2413
2585
|
try {
|
|
2414
2586
|
const mf = writeApplyManifest({
|
|
2415
2587
|
projectRoot, specBase, changeName,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-audit.js — 批量写入方留痕(2026-09-17 用户驾驭小结⑥②法证收口,ql 20260917-006)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:平台内容回放(platform resolve --take-platform / pull --spec)与 worktree apply 都是
|
|
5
|
+
* 「单命令内批量整文件重写、逐字节保留来源行尾」——sillyspec 自产文件一律 LF,而回放路径会把
|
|
6
|
+
* 他机编辑器写出的 CRLF 内容原样落盘。事后定位只能靠 mtime 考古(2026-09-17 实证:3 文件毫秒
|
|
7
|
+
* 级同 mtime + CRLF + 内容增量,「平台同步回写」被冤枉一整天后才还原真凶候选)。本模块在每个
|
|
8
|
+
* 批量写入口追加一条 JSONL 审计行(.sillyspec/.runtime/write-audit.jsonl):时间 / 触发命令 /
|
|
9
|
+
* 文件集 / CRLF 命中——下次「18:09 谁动了 design.md」查一行即中。
|
|
10
|
+
*
|
|
11
|
+
* 约束:fail-open(任何异常只丢审计不改写入行为);零依赖;单行 appendFileSync(并行会话
|
|
12
|
+
* 各自整行写入,Windows 下最坏交错按行丢失计,不损主行为)。
|
|
13
|
+
*/
|
|
14
|
+
import { appendFileSync, mkdirSync } from 'fs'
|
|
15
|
+
import { join, dirname } from 'path'
|
|
16
|
+
|
|
17
|
+
const CRLF_BUF = Buffer.from('\r\n')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 追加一条批量写入审计记录。
|
|
21
|
+
* @param {string} specDir .sillyspec 根(审计落 .runtime/write-audit.jsonl)
|
|
22
|
+
* @param {{ via: string, change?: string, files?: string[], crlfPaths?: string[], detail?: object }} record
|
|
23
|
+
* via=触发命令锚('platform-resolve-take-platform' | 'pull-spec-bundle' | 'worktree-apply')
|
|
24
|
+
* @returns {boolean} 是否落盘成功(fail-open:false 只损失审计)
|
|
25
|
+
*/
|
|
26
|
+
export function appendWriteAudit(specDir, record) {
|
|
27
|
+
try {
|
|
28
|
+
const p = join(specDir, '.runtime', 'write-audit.jsonl')
|
|
29
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
30
|
+
appendFileSync(p, JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n', 'utf8')
|
|
31
|
+
return true
|
|
32
|
+
} catch { return false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CRLF 命中探测(法证关键字段:本仓 sillyspec 自产文件一律 LF——落盘后呈 CRLF 的文件
|
|
37
|
+
* 几乎必来自「他机内容逐字节回放」或仓外工具写入)。
|
|
38
|
+
* @param {Array<[string, Buffer|string]>} entries [path, 写入内容] 对
|
|
39
|
+
* @returns {string[]} 含 \r\n 的路径(探测失败的条目不连坐)
|
|
40
|
+
*/
|
|
41
|
+
export function detectCrlfFiles(entries) {
|
|
42
|
+
const out = []
|
|
43
|
+
for (const pair of Array.isArray(entries) ? entries : []) {
|
|
44
|
+
if (!Array.isArray(pair)) continue
|
|
45
|
+
const [path, data] = pair
|
|
46
|
+
try {
|
|
47
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data))
|
|
48
|
+
if (buf.includes(CRLF_BUF)) out.push(path)
|
|
49
|
+
} catch { /* 单条探测失败跳过 */ }
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 文件清单截断帽(防大树/大面把审计行撑爆;截断时附 +N 标记由调用方自理) */
|
|
55
|
+
export const WRITE_AUDIT_FILE_CAP = 20
|