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/scope-audit.js
CHANGED
|
@@ -31,6 +31,9 @@ import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, coll
|
|
|
31
31
|
import { computeGateProfile, resolveGateThresholds } from './quick-gate-profile.js'
|
|
32
32
|
import { safeGit } from './git-helper.js'
|
|
33
33
|
import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
|
|
34
|
+
import { classifyToolScaffold } from './worktree-apply.js'
|
|
35
|
+
import { parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
36
|
+
import { _readWorktreeMeta } from './contract-matrix.js'
|
|
34
37
|
|
|
35
38
|
/** quick 会话 id 形态(与 run/command.js :652 QUICK_SID_RE 同款——crypto.randomUUID 前 8 hex) */
|
|
36
39
|
const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
@@ -211,10 +214,11 @@ function resolveChangeDir(sb, changeName) {
|
|
|
211
214
|
function resolveDiffRoot(sb, changeName, form, cwd) {
|
|
212
215
|
if (form === 'worktree') {
|
|
213
216
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
// 双根读取(坑 platform-dual-root-form-misjudge):平台模式 sb=specRoot 时项目侧
|
|
218
|
+
// .sillyspec 的 meta 读不到 → worktree 根错落 cwd。_readWorktreeMeta 双候选(contract-matrix
|
|
219
|
+
// 既有样板,gitDir 已含 worktreePath/mode 判定)。
|
|
220
|
+
const found = _readWorktreeMeta(sb || null, cwd, changeName)
|
|
221
|
+
if (found) return found.gitDir
|
|
218
222
|
} catch { /* 读失败 → cwd 兜底(advisory) */ }
|
|
219
223
|
}
|
|
220
224
|
return cwd
|
|
@@ -376,6 +380,22 @@ function findQuickPatchRecord(cwd, sessionId) {
|
|
|
376
380
|
/** QUICKLOG 条目号形态(ql-20260914-001-abcd:日期-序号-短 hash,短 hash 4 hex) */
|
|
377
381
|
const QL_ID_RE = /^ql-\d{8}-\d{3}-[0-9a-f]{4}$/
|
|
378
382
|
|
|
383
|
+
/**
|
|
384
|
+
* 注册仓 key 清单(scope-audit-cross-repo-blindness 改进点 2):读 <specBase>/local.yaml 的
|
|
385
|
+
* repos 段(parseRepoRegistry 同源,D-003 不另造解析)——design 清单跨仓子段 / `cross-repo:`
|
|
386
|
+
* 前缀只认注册 key(fail-closed,防普通路径误判)。local.yaml 缺失/坏 → 空清单(全部按主仓
|
|
387
|
+
* 解析,行为同改进前)。
|
|
388
|
+
* @param {string} specBase .sillyspec 根
|
|
389
|
+
* @returns {string[]} repoKey 清单
|
|
390
|
+
*/
|
|
391
|
+
function loadRegisteredRepoKeys(specBase) {
|
|
392
|
+
try {
|
|
393
|
+
const p = join(specBase || '', 'local.yaml')
|
|
394
|
+
if (!specBase || !existsSync(p)) return []
|
|
395
|
+
return [...parseRepoRegistry(readFileSync(p, 'utf8')).keys()]
|
|
396
|
+
} catch { /* 读取/解析失败 → 空清单(主仓口径兜底) */ return [] }
|
|
397
|
+
}
|
|
398
|
+
|
|
379
399
|
/**
|
|
380
400
|
* ql-xxx → quick-xxx 反查(用户「为什么不直接用 ql-xxx 查」根治,ql-20260910-014 设计:
|
|
381
401
|
* 映射只在本机 guard,会话结束即清;patches 记录按 qlId 命名且 json 冗余 sessionId——
|
|
@@ -704,7 +724,8 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
704
724
|
// 审计 tag)全无 = 尚未进入 execute——实际侧不存在(B2 会吞整个工作区脏文件误归属本变更,
|
|
705
725
|
// 2026-09-11-cross-change-decision-guard 实证 28 个「计划外」全是并行在途)。出计划清单
|
|
706
726
|
// 视图:rows=design 清单 untouched(待实现),工作区改动归属各自会话不进表。
|
|
707
|
-
|
|
727
|
+
// 双根(坑 platform-dual-root-form-misjudge):平台模式 sb=specRoot 时项目侧 meta 单路径读不到
|
|
728
|
+
const hasWorktreeMeta = !!_readWorktreeMeta(sb || null, cwd, changeName)
|
|
708
729
|
const hasBranch = !!safeGit(cwd, ['rev-parse', '--verify', '--quiet', `sillyspec/${changeName}^{commit}`], { timeout: 15 * 1000 }).value
|
|
709
730
|
const hasAuditTag = !!safeGit(cwd, ['rev-parse', '--verify', '--quiet', `sillyspec-audit/sillyspec/${changeName}^{commit}`], { timeout: 15 * 1000 }).value
|
|
710
731
|
// 审查 C-F02:预执行判定必须排除归档(归档=流程走完,58/77 归档变更三信号全缺被误判
|
|
@@ -740,11 +761,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
740
761
|
try {
|
|
741
762
|
prePlanned = parseFileChangeListDetailed(
|
|
742
763
|
join(changeDirInfo ? changeDirInfo.dir : join(sb, 'changes', changeName), 'design.md'),
|
|
743
|
-
{ keepSillyspecDocs: true })
|
|
764
|
+
{ keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(sb) })
|
|
744
765
|
} catch { /* 清单解析失败 → 空清单 + 说明 */ }
|
|
745
766
|
const preRows = prePlanned.map(e => ({
|
|
746
767
|
path: e.path, planned: e.operation || null, additions: 0, deletions: 0,
|
|
747
768
|
kind: 'modified', verdict: 'untouched',
|
|
769
|
+
...(e.repo ? { crossRepo: e.repo } : {}),
|
|
748
770
|
}))
|
|
749
771
|
return {
|
|
750
772
|
...emptyBase,
|
|
@@ -762,8 +784,9 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
762
784
|
let planDegraded = null
|
|
763
785
|
try {
|
|
764
786
|
// keepSillyspecDocs=true:与 actual 侧 filterDeliverableFiles 保留 .sillyspec/docs/** 交付物
|
|
765
|
-
// 的口径对齐(dogfood
|
|
766
|
-
|
|
787
|
+
// 的口径对齐(dogfood 模块文档=交付物);repoKeys=注册仓清单——跨仓子段/`cross-repo:` 前缀
|
|
788
|
+
// 条目带 repo 字段(改进点 2,标注「跨仓(本表不含)」的数据源)
|
|
789
|
+
plannedEntries = parseFileChangeListDetailed(designMdPath, { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(sb) })
|
|
767
790
|
} catch (e) {
|
|
768
791
|
planDegraded = `design.md 清单解析异常: ${e && e.message ? String(e.message).split('\n')[0] : e}`
|
|
769
792
|
}
|
|
@@ -783,13 +806,10 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
783
806
|
let actual = null
|
|
784
807
|
let actualFailure = null
|
|
785
808
|
try {
|
|
786
|
-
const
|
|
787
|
-
if (
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
? meta.worktreePath
|
|
791
|
-
: cwd
|
|
792
|
-
const anchor = meta.baselineCommit || meta.actualBaseHash || meta.baseHash || null
|
|
809
|
+
const found = _readWorktreeMeta(sb || null, cwd, changeName)
|
|
810
|
+
if (found) {
|
|
811
|
+
const wtRoot = found.gitDir
|
|
812
|
+
const anchor = found.meta.baselineCommit || found.meta.actualBaseHash || found.meta.baseHash || null
|
|
793
813
|
if (anchor) {
|
|
794
814
|
const diffOut = safeGit(wtRoot, ['diff', '--name-only', anchor], { timeout: 30 * 1000 })
|
|
795
815
|
if (!diffOut.error && typeof diffOut.value === 'string') {
|
|
@@ -811,9 +831,9 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
811
831
|
if (wtFilter(p) && !files.includes(p)) files.push(p)
|
|
812
832
|
}
|
|
813
833
|
}
|
|
814
|
-
actual = { ok: true, form: 'worktree', files, sources: ['worktree:diff-anchor..worktree', 'worktree:status-untracked'], foreignExcluded: 0, degradedReason: null, baseAnchor: anchor }
|
|
834
|
+
actual = { ok: true, form: 'worktree', files, sources: ['worktree:diff-anchor..worktree', 'worktree:status-untracked'], foreignExcluded: 0, foreignExcludedFiles: [], degradedReason: null, baseAnchor: anchor }
|
|
815
835
|
} else {
|
|
816
|
-
actual = { ok: false, form: 'worktree', files: [], sources: [], foreignExcluded: 0, degradedReason: 'worktree 锚定 diff 失败', baseAnchor: null }
|
|
836
|
+
actual = { ok: false, form: 'worktree', files: [], sources: [], foreignExcluded: 0, foreignExcludedFiles: [], degradedReason: 'worktree 锚定 diff 失败', baseAnchor: null }
|
|
817
837
|
}
|
|
818
838
|
} else {
|
|
819
839
|
// 无锚 meta(baseHash 等字段缺失):退 HEAD 未提交窗口口径——文件集=status 全量
|
|
@@ -829,7 +849,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
829
849
|
&& !p.startsWith('.sillyspec/quicklog/')) files.push(p)
|
|
830
850
|
}
|
|
831
851
|
}
|
|
832
|
-
actual = { ok: true, form: 'worktree', files, sources: ['worktree:status-porcelain(HEAD-window)'], foreignExcluded: 0, degradedReason: null, baseAnchor: null }
|
|
852
|
+
actual = { ok: true, form: 'worktree', files, sources: ['worktree:status-porcelain(HEAD-window)'], foreignExcluded: 0, foreignExcludedFiles: [], degradedReason: null, baseAnchor: null }
|
|
833
853
|
}
|
|
834
854
|
} else {
|
|
835
855
|
const mod = await import('./verify-postcheck.js')
|
|
@@ -983,10 +1003,20 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
983
1003
|
notes.push('baseAnchor 缺失——行数按 HEAD 未提交窗口采集(不含已提交改动,quick 模式同口径;numstat 采集失败时行列为 —)')
|
|
984
1004
|
}
|
|
985
1005
|
|
|
986
|
-
// 他者声明排除面可见(R-04
|
|
987
|
-
//
|
|
1006
|
+
// 他者声明排除面可见(R-04 同精神)+ 退栈名单消费(scope-audit-cross-repo-blindness 改进点 3):
|
|
1007
|
+
// planned 文件被退栈排除但已写盘 → 该行标 suspectedForeignDone「疑似他者已实现·已退栈」,
|
|
1008
|
+
// 与真未动区分(名单来自 resolveReconcileActualFiles 新增的 foreignExcludedFiles)。
|
|
1009
|
+
const foreignExcludedSet = new Set((Array.isArray(actual.foreignExcludedFiles) ? actual.foreignExcludedFiles : []).map(toPosix))
|
|
988
1010
|
if (actual.foreignExcluded > 0) {
|
|
989
|
-
notes.push(`实际侧另有 ${actual.foreignExcluded}
|
|
1011
|
+
notes.push(`实际侧另有 ${actual.foreignExcluded} 个文件按他者会话声明退栈未进本表——「计划未动」行标注「疑似他者已实现·已退栈」者即此类(声明即归属,git diff 核实)`)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 计划侧跨仓段汇总(改进点 2):跨仓条目不进本仓对账(actual 只采主仓 git),行级标
|
|
1015
|
+
// crossRepo + note 交代去对应仓对账。
|
|
1016
|
+
const crossRepoKeys = [...new Set(plannedEntries.map(e => e.repo).filter(Boolean))]
|
|
1017
|
+
if (crossRepoKeys.length > 0) {
|
|
1018
|
+
const crossCount = plannedEntries.filter(e => e.repo).length
|
|
1019
|
+
notes.push(`计划侧含 ${crossCount} 个跨仓文件(repo:${crossRepoKeys.join('、')})——⊘ 行不在本仓对账面(实际侧只采主仓 git),请到对应仓跑 scope-audit 对账`)
|
|
990
1020
|
}
|
|
991
1021
|
|
|
992
1022
|
// —— 三态判定 ——
|
|
@@ -1006,6 +1036,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1006
1036
|
} else {
|
|
1007
1037
|
row.planned = null
|
|
1008
1038
|
row.verdict = 'unplanned'
|
|
1039
|
+
// 工具/平台脚手架(改进点 1 软桶):CLI/平台自装文件不占「计划外」——硬排档
|
|
1040
|
+
// (.worktrees//.sillyspec-platform*/knowledge/local.yaml)已被 filterDeliverableFiles
|
|
1041
|
+
// 滤出不进 actual;此处打标的是可能正当交付的软档(.claude/skills/、CLAUDE.md、
|
|
1042
|
+
// attachments/),范围视图归入「工具/平台设施」桶。
|
|
1043
|
+
const facility = classifyToolScaffold(path)
|
|
1044
|
+
if (facility) row.facility = facility
|
|
1009
1045
|
}
|
|
1010
1046
|
}
|
|
1011
1047
|
rows.push(row)
|
|
@@ -1013,7 +1049,16 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1013
1049
|
// 计划未动:清单文件无实际改动 → 补行(行数 0/0);计划侧降级时不产(plannedEntries 为空天然不跑)
|
|
1014
1050
|
for (const e of plannedEntries) {
|
|
1015
1051
|
if (matchedPlanned.has(e.path)) continue
|
|
1016
|
-
|
|
1052
|
+
const row = { path: e.path, planned: e.operation || null, additions: 0, deletions: 0, kind: 'modified', verdict: 'untouched' }
|
|
1053
|
+
if (e.repo) {
|
|
1054
|
+
// 跨仓条目(改进点 2):不是「未动」是「本表不含」
|
|
1055
|
+
row.crossRepo = e.repo
|
|
1056
|
+
} else {
|
|
1057
|
+
// 退栈排除 + 盘面已存在 → 他者会话已写盘(改进点 3);NEW: 前缀剥除后比对(pathMatches 同款口径)
|
|
1058
|
+
const norm = toPosix(e.path).replace(/^NEW:/, '')
|
|
1059
|
+
if (foreignExcludedSet.has(norm) && existsSync(join(numstatRoot, norm))) row.suspectedForeignDone = true
|
|
1060
|
+
}
|
|
1061
|
+
rows.push(row)
|
|
1017
1062
|
}
|
|
1018
1063
|
|
|
1019
1064
|
// 冻结 patch(collectPatch,execute --done 落盘方消费):与行数同根同锚(numstatRoot +
|
|
@@ -1054,7 +1099,11 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1054
1099
|
* rows: Array<{ path: string, planned?: string|null, declared?: boolean,
|
|
1055
1100
|
* additions: number|null, deletions: number|null,
|
|
1056
1101
|
* kind: 'binary'|'new'|'modified'|'deleted',
|
|
1057
|
-
* verdict?: 'planned'|'unplanned'|'untouched', attribution?: 'declared'|'soft'|'undeclared'
|
|
1102
|
+
* verdict?: 'planned'|'unplanned'|'untouched', attribution?: 'declared'|'soft'|'undeclared',
|
|
1103
|
+
* crossRepo?: string, suspectedForeignDone?: boolean, facility?: 'tool'|'platform' }>,
|
|
1104
|
+
* (crossRepo/suspectedForeignDone/facility 为 scope-audit-cross-repo-blindness 改进点 1/2/3
|
|
1105
|
+
* 增量字段:跨仓条目标 repoKey / 退栈排除但已写盘的 planned 文件 / 计划外行里的工具平台
|
|
1106
|
+
* 脚手架桶——均为按存在性读取,缺省无字段零回归)
|
|
1058
1107
|
* excluded: { foreignDeclared: Array<{ file: string, sessions: string[] }> },
|
|
1059
1108
|
* note?: string|null,
|
|
1060
1109
|
* gateProfile?: object|null }>}
|
|
@@ -1187,7 +1236,7 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
|
|
|
1187
1236
|
const located = locateQuickSessionGuard(cwd, changeName)
|
|
1188
1237
|
if (located) root = dirname(located.specBase)
|
|
1189
1238
|
} else {
|
|
1190
|
-
const form =
|
|
1239
|
+
const form = _readWorktreeMeta(sb || null, cwd, changeName) ? 'worktree' : 'post-apply'
|
|
1191
1240
|
root = resolveDiffRoot(sb, changeName, form, cwd)
|
|
1192
1241
|
}
|
|
1193
1242
|
|
|
@@ -1210,12 +1259,16 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
|
|
|
1210
1259
|
}
|
|
1211
1260
|
}
|
|
1212
1261
|
|
|
1213
|
-
/** verdict / attribution → 展示标记(人类可读面;BIN 为 binary 行数占位,—
|
|
1262
|
+
/** verdict / attribution → 展示标记(人类可读面;BIN 为 binary 行数占位,— 为降级占位)。
|
|
1263
|
+
* 行级标注优先(scope-audit-cross-repo-blindness 改进点 1/2/3):跨仓 > 疑似他者已实现 > 设施桶 */
|
|
1214
1264
|
const VERDICT_LABEL = {
|
|
1215
1265
|
planned: '✓ 计划内',
|
|
1216
1266
|
unplanned: '⚠️ 计划外',
|
|
1217
1267
|
untouched: '⚠️ 计划未动',
|
|
1218
1268
|
}
|
|
1269
|
+
const CROSS_REPO_LABEL = '⊘ 跨仓(本表不含)'
|
|
1270
|
+
const SUSPECTED_FOREIGN_LABEL = '⚠️ 计划未动(疑似他者已实现·已退栈)'
|
|
1271
|
+
const FACILITY_LABEL = '⚠️ 计划外(工具/平台设施)'
|
|
1219
1272
|
const ATTRIBUTION_LABEL = {
|
|
1220
1273
|
declared: '✓ 已声明',
|
|
1221
1274
|
soft: '🔍 软归属',
|
|
@@ -1278,7 +1331,10 @@ export function renderScopeAuditTable(result, opts = {}) {
|
|
|
1278
1331
|
const stat = row.kind === 'binary'
|
|
1279
1332
|
? { a: 'BIN', d: 'BIN' }
|
|
1280
1333
|
: { a: fmtCount(row.additions) ?? '—', d: fmtCount(row.deletions) ?? '—' }
|
|
1281
|
-
const label =
|
|
1334
|
+
const label = row.crossRepo ? CROSS_REPO_LABEL
|
|
1335
|
+
: row.suspectedForeignDone ? SUSPECTED_FOREIGN_LABEL
|
|
1336
|
+
: row.facility ? FACILITY_LABEL
|
|
1337
|
+
: VERDICT_LABEL[row.verdict] || ATTRIBUTION_LABEL[row.attribution] || ''
|
|
1282
1338
|
lines.push(` ${row.path || '(未知路径)'}${' '.repeat(Math.max(1, 40 - String(row.path || '').length))}${label}${' '.repeat(Math.max(1, 12 - label.length))}${String(stat.a).padStart(5)} ${String(stat.d).padStart(5)} ${row.kind || ''}`)
|
|
1283
1339
|
}
|
|
1284
1340
|
if (rows.length > shown.length) {
|
|
@@ -1287,15 +1343,29 @@ export function renderScopeAuditTable(result, opts = {}) {
|
|
|
1287
1343
|
|
|
1288
1344
|
lines.push(` 合计:${totals.files ?? rows.length} 文件 +${totals.additions ?? 0} / -${totals.deletions ?? 0}`)
|
|
1289
1345
|
|
|
1290
|
-
// ⚠️ 出口指引(full-flow
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1346
|
+
// ⚠️ 出口指引(full-flow 三态面):计划外补声明、计划未动确认遗漏。
|
|
1347
|
+
// 行级标注三分(scope-audit-cross-repo-blindness 改进点 1/2/3):跨仓行/疑似他者已实现行/
|
|
1348
|
+
// 工具设施桶各有去向说明,不混入笼统的「计划外/计划未动」计数逼用户排查噪音。
|
|
1349
|
+
const unplanned = rows.filter(x => x && x.verdict === 'unplanned' && !x.facility).length
|
|
1350
|
+
const facility = rows.filter(x => x && x.verdict === 'unplanned' && x.facility).length
|
|
1351
|
+
const untouchedReal = rows.filter(x => x && x.verdict === 'untouched' && !x.suspectedForeignDone && !x.crossRepo).length
|
|
1352
|
+
const suspectedForeign = rows.filter(x => x && x.suspectedForeignDone).length
|
|
1353
|
+
const crossRepo = rows.filter(x => x && x.crossRepo).length
|
|
1354
|
+
if (unplanned > 0 || untouchedReal > 0) {
|
|
1294
1355
|
const parts = []
|
|
1295
1356
|
if (unplanned > 0) parts.push(`计划外 ${unplanned} 文件`)
|
|
1296
|
-
if (
|
|
1357
|
+
if (untouchedReal > 0) parts.push(`计划未动 ${untouchedReal} 文件`)
|
|
1297
1358
|
lines.push(` ⚠️ ${parts.join('、')}——计划外请补 design.md 声明或 --output 注明原因;计划未动请确认是否遗漏`)
|
|
1298
1359
|
}
|
|
1360
|
+
if (suspectedForeign > 0) {
|
|
1361
|
+
lines.push(` ℹ️ 疑似他者已实现 ${suspectedForeign} 文件(已写盘但被退栈——他者会话声明归属,本表不计未动;git diff 核实归属)`)
|
|
1362
|
+
}
|
|
1363
|
+
if (facility > 0) {
|
|
1364
|
+
lines.push(` ℹ️ 工具/平台设施 ${facility} 文件(CLI/平台自装脚手架,非本变更改动,不占计划外)`)
|
|
1365
|
+
}
|
|
1366
|
+
if (crossRepo > 0) {
|
|
1367
|
+
lines.push(` ℹ️ 跨仓 ${crossRepo} 文件(⊘ 行——本表不含,请到对应仓跑 scope-audit 对账)`)
|
|
1368
|
+
}
|
|
1299
1369
|
const undeclared = rows.filter(x => x && x.attribution === 'undeclared').length
|
|
1300
1370
|
if (undeclared > 0) lines.push(` ⚠️ 未声明 ${undeclared} 文件——超出 allowedFiles 声明面,补 --files 声明或注明原因`)
|
|
1301
1371
|
|
|
@@ -274,6 +274,20 @@ const PLAN_RULES = [
|
|
|
274
274
|
spec: 'large 变更产出 `module-impact.md` 首版(plan review_plan 步生成;scale=small 豁免)',
|
|
275
275
|
failMessage: 'module-impact.md 缺失: ${path}(large 变更应在 plan 审查计划步生成模块影响分析首版)',
|
|
276
276
|
},
|
|
277
|
+
// 全局硬约束段存在性(ql-20260917-003,warning——ql-20260917-002 落地段的事后校验):plan_level=full
|
|
278
|
+
// 的 plan.md 应含「全局硬约束」段(design.md 跨 task 硬约束逐字抄录,执行期子代理只读自己的
|
|
279
|
+
// 任务卡+本段)。condition 生效依赖 validatePlanOutputs 把 plan.md frontmatter plan_level 传入
|
|
280
|
+
// ctx;读不到(无 frontmatter/旧格式/light)→ eq:'full' 不成立跳过(存量零误报 fail-safe)。
|
|
281
|
+
// 确无跨 task 硬约束时写一段「无」占位即可消警(段在场即过,不校验段体非空——空体由 execute
|
|
282
|
+
// 注入端容忍+审查目检)。
|
|
283
|
+
{
|
|
284
|
+
id: 'plan.global-constraints', stage: 'plan', source: 'validatePlanOutputs', severity: 'warning', kind: 'literal-any',
|
|
285
|
+
target: { root: 'change', path: 'plan.md', scope: 'full' },
|
|
286
|
+
data: { literals: ['全局硬约束'] },
|
|
287
|
+
condition: { ctxField: 'planLevel', eq: 'full' },
|
|
288
|
+
spec: 'plan_level=full 的 plan.md 含「全局硬约束」段(design.md 跨 task 硬约束逐字抄录;无跨 task 硬约束时写「无」占位消警)',
|
|
289
|
+
failMessage: 'plan.md 缺少「## 全局硬约束」段(plan_level=full:从 design.md 逐字抄录绑定所有 task 的硬约束,执行期子代理只读自己的任务卡+本段;确无跨 task 硬约束时写一段「无」占位消警)',
|
|
290
|
+
},
|
|
277
291
|
// entry-point-wiring(custom):design.md 命中入口实例化/启动路径模式时,被提到的入口文件
|
|
278
292
|
// (cli.ts/main.ts/server.(js|ts)/index.(js|ts))必须出现在某 task allowed_paths 或 plan.md
|
|
279
293
|
// 文件变更清单中,否则需 design.md 紧邻写明豁免。判定算法(多源 allowed_paths 收集 + 逐文件对账 +
|