sillyspec 3.32.9 → 3.32.10
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-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/decision-distill.js +77 -21
- package/src/decision-renumber.js +157 -0
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +3 -2
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-parity.js +18 -2
- package/src/flow.js +75 -12
- package/src/fr-index.js +15 -1
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +7 -5
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +92 -14
- package/src/knowledge-match.js +34 -9
- package/src/knowledge-stats.js +12 -13
- package/src/knowledge-vector.js +98 -7
- package/src/machine-interface.js +26 -0
- package/src/quicklog.js +6 -1
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +3 -3
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +113 -44
- package/src/run/gates.js +93 -89
- package/src/run/prompt.js +3 -3
- package/src/run/stage.js +1 -1
- package/src/sentinel-assertions.js +43 -1
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +44 -11
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +5 -1
- package/src/stages/knowledge.js +56 -5
- package/src/sync.js +28 -2
- package/src/task-tick.js +12 -0
- package/src/timeline.js +60 -19
- package/src/verify-postcheck.js +49 -1
- package/src/verify-probes.js +61 -18
- package/templates/agents-instruction.md +1 -1
package/src/stage-review.js
CHANGED
|
@@ -366,6 +366,31 @@ export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
|
366
366
|
return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* stage review 目录读侧归一(坑 stage-review-dir-naming-mismatch,2026-10-15 消费仓三阶段
|
|
371
|
+
* 连环实证):规范目录 = `<stage>-<runId>`(runId 自带 review- 前缀,拼出来恰一次 review 字样)。
|
|
372
|
+
* skill 文档字面 `<stage>-review-<run-id>` + 「run-id 必须 review- 前缀」诱导出两类错位目录:
|
|
373
|
+
* ① 裸 runId 目录(agent 拿 marker 值当目录名):<runId>/
|
|
374
|
+
* ② 双前缀目录(字面拼装):<stage>-review-<runId>/
|
|
375
|
+
* 本助手按 规范 → ②双前缀 → ①裸 runId 顺序探测 review.json 在场性,返回首个命中目录;全不在
|
|
376
|
+
* 则返回规范目录(保持既有「期望路径」报错文案语义)。只读不搬文件——写入侧恒规范路径,读侧
|
|
377
|
+
* 容错让 gate / refresh-hash / autoRefresh 对错位文件天然可用。
|
|
378
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
379
|
+
* @param {string} stage - brainstorm|plan|execute
|
|
380
|
+
* @param {string} reviewRunId - run id(review-<ts>)
|
|
381
|
+
* @returns {string} review 目录绝对路径(容错命中或规范缺省)
|
|
382
|
+
*/
|
|
383
|
+
export function resolveStageReviewDir(runtimeRoot, stage, reviewRunId) {
|
|
384
|
+
const base = join(runtimeRoot, 'stage-reviews')
|
|
385
|
+
const canonical = join(base, `${stage}-${reviewRunId}`)
|
|
386
|
+
const doublePrefix = join(base, `${stage}-review-${reviewRunId}`)
|
|
387
|
+
const bare = join(base, reviewRunId)
|
|
388
|
+
for (const dir of [canonical, doublePrefix, bare]) {
|
|
389
|
+
if (existsSync(join(dir, 'review.json'))) return dir
|
|
390
|
+
}
|
|
391
|
+
return canonical
|
|
392
|
+
}
|
|
393
|
+
|
|
369
394
|
/**
|
|
370
395
|
* 获取最新的 stage review run id
|
|
371
396
|
*
|
|
@@ -395,33 +420,37 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
395
420
|
}
|
|
396
421
|
} catch {}
|
|
397
422
|
|
|
398
|
-
// fallback:扫描 stage-reviews
|
|
423
|
+
// fallback:扫描 stage-reviews/ 目录取字典序最新(向后兼容无 marker 旧数据)。候选两形:
|
|
424
|
+
// 规范/双前缀 `<stage>-review-*` + 裸 runId `review-*`(坑 stage-review-dir-naming-mismatch
|
|
425
|
+
// 错位目录——裸目录不编码 stage,须 reviewType 归属校验防跨段误认)
|
|
399
426
|
const dir = join(runtimeRoot, 'stage-reviews')
|
|
400
427
|
if (!existsSync(dir)) return null
|
|
401
428
|
try {
|
|
402
429
|
const entries = readdirSync(dir, { withFileTypes: true })
|
|
403
|
-
.filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
|
|
404
|
-
.map(e => e.name)
|
|
405
|
-
.sort()
|
|
430
|
+
.filter(e => e.isDirectory() && (e.name.startsWith(`${stage}-review-`) || e.name.startsWith('review-')))
|
|
431
|
+
.map(e => ({ name: e.name, runId: e.name.startsWith(`${stage}-`) ? e.name.slice(stage.length + 1) : e.name }))
|
|
432
|
+
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
|
406
433
|
.reverse()
|
|
407
434
|
if (!entries[0]) return null
|
|
408
435
|
// cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
|
|
409
436
|
// 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
|
|
410
437
|
// 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
|
|
438
|
+
// 裸目录另加 reviewType 校验(STAGE_REVIEW_TYPE 段专属,防他 stage 的错位文件串台)。
|
|
411
439
|
if (changeName) {
|
|
412
440
|
const matching = entries.find(e => {
|
|
413
441
|
try {
|
|
414
|
-
const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
|
|
442
|
+
const rj = JSON.parse(readFileSync(join(dir, e.name, 'review.json'), 'utf8'))
|
|
443
|
+
if (!e.name.startsWith(`${stage}-`) && rj.reviewType && rj.reviewType !== STAGE_REVIEW_TYPE[stage]) return false
|
|
415
444
|
const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
|
|
416
445
|
return r0.includes(`changes/${changeName}/`)
|
|
417
446
|
} catch { return false }
|
|
418
447
|
})
|
|
419
|
-
if (matching) return matching.
|
|
448
|
+
if (matching) return matching.runId
|
|
420
449
|
console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
|
|
421
450
|
return null
|
|
422
451
|
}
|
|
423
|
-
//
|
|
424
|
-
return entries[0].
|
|
452
|
+
// 规范形 "plan-review-2026-..." 剥 `${stage}-` 前缀得 runId;裸形目录名即 runId
|
|
453
|
+
return entries[0].runId
|
|
425
454
|
} catch {
|
|
426
455
|
return null
|
|
427
456
|
}
|
|
@@ -613,7 +642,8 @@ export function validateStageReview(opts) {
|
|
|
613
642
|
const errors = []
|
|
614
643
|
const warnings = []
|
|
615
644
|
|
|
616
|
-
|
|
645
|
+
// 读侧目录归一(坑 stage-review-dir-naming-mismatch):容错命中裸 runId / 双前缀错位目录
|
|
646
|
+
const reviewDir = resolveStageReviewDir(runtimeRoot, stage, reviewRunId)
|
|
617
647
|
const reviewPath = join(reviewDir, 'review.json')
|
|
618
648
|
|
|
619
649
|
if (!reviewRunId || !existsSync(reviewPath)) {
|
|
@@ -705,7 +735,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
|
|
|
705
735
|
&& result.errors.every(e => String(e).includes('docHash 与主审查文档'))
|
|
706
736
|
if (!mismatchOnly) return { result, autoRefreshed: false }
|
|
707
737
|
|
|
708
|
-
const reviewDir =
|
|
738
|
+
const reviewDir = resolveStageReviewDir(opts.runtimeRoot, opts.stage, opts.reviewRunId)
|
|
709
739
|
const reviewPath = join(reviewDir, 'review.json')
|
|
710
740
|
// 定位主文档并重算 hash(与 verifyStageReviewDocHash 同法:reviewedFiles[0] × searchDirs)
|
|
711
741
|
const primaryRel = Array.isArray(result.review.reviewedFiles) ? result.review.reviewedFiles[0] : null
|
|
@@ -922,8 +952,11 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
922
952
|
if (refreshHash) {
|
|
923
953
|
if (fromFile) throw new Error('register-stage-review: --refresh-hash 与 --from 互斥(refresh 就地更新既有 review,adopt 引入新文件)')
|
|
924
954
|
const existingRunId = getLatestStageReviewRunId(runtimeRoot, stage, changeName)
|
|
955
|
+
// 读侧目录归一(坑 stage-review-dir-naming-mismatch):--refresh-hash 按 marker 取 runId 后
|
|
956
|
+
// 经 resolveStageReviewDir 容错定位——review.json 落在裸 runId / 双前缀错位目录也能刷新,
|
|
957
|
+
// 不再恒报「无对应 run」。
|
|
925
958
|
const existingPath = existingRunId
|
|
926
|
-
? join(runtimeRoot,
|
|
959
|
+
? join(resolveStageReviewDir(runtimeRoot, stage, existingRunId), 'review.json')
|
|
927
960
|
: null
|
|
928
961
|
if (!existingPath || !existsSync(existingPath)) {
|
|
929
962
|
throw new Error(`register-stage-review: --refresh-hash 找不到 ${stage} 的既有 review.json(marker ${existingRunId || '无'} 无对应 run)——先去掉 --refresh-hash 生成骨架,或派审查子代理产出 review`)
|
package/src/stage-templates.js
CHANGED
|
@@ -120,6 +120,8 @@ created_at: <now-datetime>
|
|
|
120
120
|
- normalized_requirement: 可测试的约束
|
|
121
121
|
- impacts: [FR-01, task-01, verify-01]
|
|
122
122
|
- evidence: 用户回答轮次或代码/文档路径
|
|
123
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
124
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
123
125
|
\`\`\`
|
|
124
126
|
|
|
125
127
|
> 可选字段(按需另加,旧格式决策缺这些字段不受影响):**锚点**(决策落点主文件,\`<src 路径>:<行号或符号>\`;status=confirmed 时必填);**模块域**(决策涉及的模块 ID,可多个逗号分隔——合法 id 只取**当前变更所属项目**的 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`,多项目仓勿读其他子项目的 map(核验不认,报错会点名归属);规划中的新模块用 \`NEW:<名>\` 前缀声明,冒号后不加空格:\`NEW:foo\` 合法、\`NEW: foo\` 属书写错误。「生成规范文件」步 --done 的模块域核验对不认识的 id 直接阻断);**否决理由**(status=rejected 时必填);**复潮条件**(status=rejected 时必填);**故障面**(本决策引入的新失败模式——新机制落地时留痕它引入什么失败模式;type=architecture 时建议填写,可选);**退役判据**(出现什么信号时简化或删除本机制——信号出现就该简化它;type=architecture 时建议填写,可选)。
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -58,7 +58,7 @@ export const definition = {
|
|
|
58
58
|
- **明显 small**(满足:单上下文可吞吐的纯执行类——改文案/修 bug/样式调整/配置微调,预计无 Wave 编排/上下文分片需求)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出可照抄建议命令(--input 引号内换行合法):
|
|
59
59
|
|
|
60
60
|
\`\`\`
|
|
61
|
-
sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
61
|
+
sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景
|
|
62
62
|
|
|
63
63
|
成功标准:
|
|
64
64
|
- <可验证标准>"
|
|
@@ -188,6 +188,8 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
|
188
188
|
- evidence: <文件路径/代码位置/用户回答轮次>(源码引用写仓根相对全路径+行号,如 src/stage-contract.js:789——裸文件名在 docs-check 层1 靠 basename 全仓扫描找候选,找不到候选或关键词窗口不匹配即失效,到 pre-push 才拦,2026-09-19 实证 64 处返工)
|
|
189
189
|
- 故障面: <本决策引入的新失败模式>(可选,type=architecture 时建议填写)
|
|
190
190
|
- 退役判据: <出现什么信号时简化/删除本机制>(可选,type=architecture 时建议填写)
|
|
191
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
192
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
191
193
|
\`\`\`
|
|
192
194
|
|
|
193
195
|
- 幂等:写入前按 D-xxx@vN 查重,已存在的不重复追加;后续修正走新版本 D-001@v2 + supersedes(见 Design Grill 版本规则)
|
|
@@ -491,6 +493,8 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
|
|
|
491
493
|
- evidence: design.md §3/§7, src/cache/...
|
|
492
494
|
- 故障面: <本决策引入的新失败模式>(可选,type=architecture 时建议填写)
|
|
493
495
|
- 退役判据: <出现什么信号时简化/删除本机制>(可选,type=architecture 时建议填写)
|
|
496
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
497
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
494
498
|
\`\`\`
|
|
495
499
|
|
|
496
500
|
### 输出格式
|
package/src/stages/knowledge.js
CHANGED
|
@@ -110,8 +110,16 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
110
110
|
// 复用 knowledge-match 引擎(三层 hybrid:路由→平台向量→本地词片,2026-09-29-knowledge-vector-recall)
|
|
111
111
|
const result = await matchKnowledgeHybrid(knowledgeDir, query, { cwd: dir })
|
|
112
112
|
|
|
113
|
+
// 向量开关发现性提示(2026-10-15-vector-switch-discoverability):平台已连接+开关未开
|
|
114
|
+
// (缺省关)→ hint 键(vectorSwitchHint 单源,flow 注入零命中行同款文案);其余空串零输出。
|
|
115
|
+
let vectorHint = ''
|
|
116
|
+
try {
|
|
117
|
+
const { vectorSwitchHint } = await import('../knowledge-vector.js')
|
|
118
|
+
vectorHint = vectorSwitchHint(dir)
|
|
119
|
+
} catch { /* 提示 fail-open */ }
|
|
120
|
+
|
|
113
121
|
if (!result.matched) {
|
|
114
|
-
output(true, { query, matches: [] })
|
|
122
|
+
output(true, { query, matches: [], ...(vectorHint ? { hint: vectorHint } : {}) })
|
|
115
123
|
return
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -139,7 +147,7 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
139
147
|
}
|
|
140
148
|
})
|
|
141
149
|
|
|
142
|
-
output(true, { query, matches })
|
|
150
|
+
output(true, { query, matches, ...(vectorHint ? { hint: vectorHint } : {}) })
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
// ── inspect ──
|
|
@@ -610,7 +618,8 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
610
618
|
' digest 治理信号卡(rot/伪域/坏绑定——纯读)',
|
|
611
619
|
' stats 覆盖率/死条目统计(纯读)',
|
|
612
620
|
' refresh 重扫 scan 文档重写 generated/ 区(**写盘**——先 validate 再用)',
|
|
613
|
-
' propose / classify / inbox / graph / stats',
|
|
621
|
+
' propose / classify / inbox / graph / stats / renumber',
|
|
622
|
+
' renumber [--dry-run|--apply] 存量决策号迁移为全局号(dry-run 先行;apply 写盘幂等)',
|
|
614
623
|
'',
|
|
615
624
|
'写盘面子命令(refresh/propose/classify)先跑同名只读命令确认现状;探索用法先看本帮助。',
|
|
616
625
|
]
|
|
@@ -693,11 +702,53 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
693
702
|
}
|
|
694
703
|
return
|
|
695
704
|
}
|
|
705
|
+
case 'renumber': {
|
|
706
|
+
// 存量决策号一次性迁移(2026-10-14-decision-legacy-renumber):--dry-run 打印计划,
|
|
707
|
+
// --apply 执行重写(快照指纹校验 + 原子写;重跑幂等——原编号在场节段零触碰)。
|
|
708
|
+
const specBase = opts.specDir || join(dir, '.sillyspec')
|
|
709
|
+
const rest = args.slice(1)
|
|
710
|
+
const apply = rest.includes('--apply')
|
|
711
|
+
try {
|
|
712
|
+
const { planRenumber, applyRenumberPlan } = await import('../decision-renumber.js')
|
|
713
|
+
const plan = planRenumber(join(specBase, 'knowledge'))
|
|
714
|
+
const st = plan.stats
|
|
715
|
+
console.log(`🗺️ 决策号迁移计划:legacy ${st.legacySections} 节段|fanout ${st.fanoutGroups} 组(${st.fanoutCopies} 副本→单一身份)|发号 ${st.mintedNumbers}|无变更条目 ${st.noChangeEntries}(逐节独立发号)|epoch 零触碰 ${st.untouchedEpoch}`)
|
|
716
|
+
for (const [dom, n] of Object.entries(st.perDomainStart)) console.log(` 域 ${dom}:号段自 ${String(n).padStart(3, '0')} 起`)
|
|
717
|
+
if (!apply) {
|
|
718
|
+
console.log('(--dry-run 形态——加 --apply 执行重写;git 可回退)')
|
|
719
|
+
output(true, { subcommand: 'renumber', dryRun: true, ...st })
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
if (plan.files.every((f) => !f.ops || f.ops.length === 0)) {
|
|
723
|
+
console.log('✅ 无可迁移节段(幂等重跑——零变化)')
|
|
724
|
+
output(true, { subcommand: 'renumber', filesWritten: 0, ...st })
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
let filesWritten = 0
|
|
728
|
+
for (const fe of plan.files) {
|
|
729
|
+
if (!fe.ops || !fe.ops.length) continue
|
|
730
|
+
const p = join(specBase, 'knowledge', fe.file)
|
|
731
|
+
const cur = readFileSync(p, 'utf8')
|
|
732
|
+
if (cur !== fe.content) {
|
|
733
|
+
console.error(`❌ 快照漂移:${fe.file} 在计划后被并行修改——中止不覆盖(重跑重新计划)`)
|
|
734
|
+
process.exitCode = 1
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
writeAtomicSync(p, applyRenumberPlan(new Map([[fe.file, cur]]), plan).get(fe.file))
|
|
738
|
+
filesWritten++
|
|
739
|
+
}
|
|
740
|
+
console.log(`✅ 迁移完成:${filesWritten} 文件重写、${st.mintedNumbers} 号铸出(epoch 形态)——重跑 --apply 验证零变化;平台侧随后 platform sync 推送`)
|
|
741
|
+
output(true, { subcommand: 'renumber', filesWritten, ...st })
|
|
742
|
+
} catch (e) {
|
|
743
|
+
output(false, {}, { code: 'renumber_failed', message: String((e && e.message) || e) })
|
|
744
|
+
}
|
|
745
|
+
return
|
|
746
|
+
}
|
|
696
747
|
default:
|
|
697
748
|
output(false, {}, {
|
|
698
749
|
code: 'unknown_subcommand',
|
|
699
|
-
|
|
700
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
750
|
+
subCommand: subCommand,
|
|
751
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph', 'renumber'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
701
752
|
})
|
|
702
753
|
}
|
|
703
754
|
}
|
package/src/sync.js
CHANGED
|
@@ -273,6 +273,32 @@ function combineSignals(external, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
273
273
|
return external ? AbortSignal.any([timeoutSignal, external]) : timeoutSignal;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* 非 2xx 回显折叠(坑 probe-noise-and-commit-suggest-quality 坑C,2026-10-15 消费仓实证:
|
|
278
|
+
* 409 冲突响应体含整段 platform_progress 大对象,text.slice(0,200) 密排回显挂在每条 CLI 命令
|
|
279
|
+
* 尾部刷屏)。结构化冲突体折叠为一行摘要(conflict 标识 + 关键时间戳/变更字段,不回显大
|
|
280
|
+
* 对象);其他 JSON 体打键名摘要;非 JSON/解析失败退回截断原文(既有形态零回归)。
|
|
281
|
+
*/
|
|
282
|
+
export function formatSyncHttpErrorLine(method, url, status, text) {
|
|
283
|
+
const raw = String(text || '');
|
|
284
|
+
let body = null;
|
|
285
|
+
try { body = JSON.parse(raw); } catch { body = null; }
|
|
286
|
+
const m = method || 'GET';
|
|
287
|
+
if (body && typeof body === 'object') {
|
|
288
|
+
if (body.conflict === true) {
|
|
289
|
+
const parts = ['conflict'];
|
|
290
|
+
if (body.change) parts.push(`change=${body.change}`);
|
|
291
|
+
if (typeof body.last_pushed_at === 'string') parts.push(`last_pushed_at=${body.last_pushed_at}`);
|
|
292
|
+
if (body.base_ts !== undefined) parts.push(`base_ts=${body.base_ts}`);
|
|
293
|
+
if (body.reason) parts.push(`reason=${String(body.reason).slice(0, 80)}`);
|
|
294
|
+
return `[sync] ${m} ${url} → ${status} ${parts.join(' ')}(冲突响应体已折叠为一行)`;
|
|
295
|
+
}
|
|
296
|
+
const keys = Object.keys(body).slice(0, 6).join(',');
|
|
297
|
+
return `[sync] ${m} ${url} → ${status} ${keys ? `json(${keys})` : '(空响应体)'}`;
|
|
298
|
+
}
|
|
299
|
+
return `[sync] ${m} ${url} → ${status} ${raw.slice(0, 200)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
276
302
|
async function fetchJson(url, options = {}) {
|
|
277
303
|
const signal = combineSignals(options.signal);
|
|
278
304
|
// noMute 是本模块的闸门开关不是 fetch 参数,先剥掉再透传
|
|
@@ -281,7 +307,7 @@ async function fetchJson(url, options = {}) {
|
|
|
281
307
|
const res = await fetch(url, { ...init, signal });
|
|
282
308
|
if (!res.ok) {
|
|
283
309
|
const text = await res.text().catch(() => '');
|
|
284
|
-
const line =
|
|
310
|
+
const line = formatSyncHttpErrorLine(options.method, url, res.status, text);
|
|
285
311
|
// 连接类失败(404=端点未就绪 / 5xx)走跨进程噪音闸:平台未就绪期每条命令的自动
|
|
286
312
|
// 同步都撞同一批失败,重复刷屏淹没门禁输出(见 sync-noise.js 模块头)。409 冲突与
|
|
287
313
|
// 其余 4xx 业务态每轮都该看见,不走闸。用户显式命令(connect health ping)传 noMute。
|
|
@@ -320,7 +346,7 @@ async function fetchJsonWithStatus(url, options = {}) {
|
|
|
320
346
|
try { body = JSON.parse(text); } catch { body = null; }
|
|
321
347
|
}
|
|
322
348
|
if (!res.ok) {
|
|
323
|
-
const line =
|
|
349
|
+
const line = formatSyncHttpErrorLine(options.method, url, res.status, text);
|
|
324
350
|
if (!noMute && isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
325
351
|
else console.warn(line);
|
|
326
352
|
}
|
package/src/task-tick.js
CHANGED
|
@@ -145,6 +145,18 @@ export async function runTaskTick({ changeName, cwd, taskId, specBase, evidence
|
|
|
145
145
|
...(ev.text ? { evidence: ev.text.slice(0, 300) } : {}), // 事件面截 300 字符(全文在 task-evidence.jsonl)
|
|
146
146
|
}) + '\n', 'utf8')
|
|
147
147
|
} catch { /* 事件 best-effort:写失败翻格不受影响(节奏门退回采样判) */ }
|
|
148
|
+
// 侧车勾选留痕(2026-10-14-timeline-ticktime-anchor-scope):{ts,task,source} 追加进 change 目录
|
|
149
|
+
// 的 task-ticks.jsonl——勾选时刻的事实源(timeline 任务面精确时刻),落 change 目录随归档进 git
|
|
150
|
+
// 跨机可读,不随 .runtime 机本位/观测盲窗蒸发(2026-10-14-deploy-release-notes 实证:盲窗内
|
|
151
|
+
// 勾选两个记录面全空)。事件面不带任务 id(节奏门只读 checked N→M),id 口径以本侧车为权威。
|
|
152
|
+
// best-effort:写失败仅告警,翻格不受影响(timeline 退事件流推断)。
|
|
153
|
+
try {
|
|
154
|
+
appendFileSync(join(changesRoot, changeName, 'task-ticks.jsonl'), JSON.stringify({
|
|
155
|
+
ts: Date.now(), task: normalizeTaskId(taskId) || taskId, source: 'task-tick',
|
|
156
|
+
}) + '\n', 'utf8')
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn(`⚠️ 勾选留痕写失败(task-ticks.jsonl:${e && e.code ? e.code : e})——翻格不受影响,timeline 勾选时刻退事件流推断`)
|
|
159
|
+
}
|
|
148
160
|
// 工件即时重推(2026-10-07-allticked-gate-docs-resync):翻格即任务面变更——顺带触发
|
|
149
161
|
// spec-sync,平台不再停留 start 时点快照(真实会话实证:改写后平台恒显初稿)。best-effort。
|
|
150
162
|
try {
|
package/src/timeline.js
CHANGED
|
@@ -24,6 +24,33 @@ export function parseTaskLines(tasksMd) {
|
|
|
24
24
|
return out;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** task id 归一:task-2 → task-02(与 task-tick normalizeTaskId 同口径,非 task-N 形态返 null)。 */
|
|
28
|
+
function normTaskId(s) {
|
|
29
|
+
const m = /^task-(\d+)$/.exec(String(s || '').trim());
|
|
30
|
+
return m ? `task-${String(Number(m[1])).padStart(2, '0')}` : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 侧车勾选留痕解析(2026-10-14-timeline-ticktime-anchor-scope):task-ticks.jsonl 行集
|
|
35
|
+
* ({ts,task,source})→ Array<{ts,task}>。坏行跳过;id 归一;同任务取最早(时钟回拨/重复
|
|
36
|
+
* 留痕容错——勾选时刻语义=首次翻格时刻)。文件缺失/不可读由调用方 catch 成空数组。
|
|
37
|
+
*/
|
|
38
|
+
export function parseTickLog(raw) {
|
|
39
|
+
const byTask = new Map();
|
|
40
|
+
for (const line of String(raw || '').split(/\r?\n/)) {
|
|
41
|
+
if (!line.trim()) continue;
|
|
42
|
+
try {
|
|
43
|
+
const o = JSON.parse(line);
|
|
44
|
+
const id = normTaskId(o && o.task);
|
|
45
|
+
const ts = Number(o && o.ts);
|
|
46
|
+
if (!id || !Number.isFinite(ts)) continue;
|
|
47
|
+
const p = byTask.get(id);
|
|
48
|
+
if (!p || ts < p.ts) byTask.set(id, { ts, task: id });
|
|
49
|
+
} catch { /* 坏行跳过 */ }
|
|
50
|
+
}
|
|
51
|
+
return [...byTask.values()];
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
/**
|
|
28
55
|
* 翻格顺序推断(FR-04):task-done(stage='tasks')事件序列 → 每任务序号的勾选时刻。
|
|
29
56
|
* 计数链衔接(from === 游标)才赋值;**中段**断裂(from ≠ 游标)标 broken 并停止赋值。
|
|
@@ -60,9 +87,12 @@ function tokensOf(text) {
|
|
|
60
87
|
/**
|
|
61
88
|
* 提交锚解析:事件流 kind=commit 的短 hash → gitLook 解析出主题/时间/token。
|
|
62
89
|
* @param {Function} gitLook (hash) => null | {hash, dateISO, message}(注入面,接线层包 gitQuiet)
|
|
63
|
-
* @
|
|
90
|
+
* @param {string|null} change 变更名(2026-10-14-timeline-ticktime-anchor-scope):在场时逐 anchor
|
|
91
|
+
* 算 sameChange=提交全消息(%B 含正文)includes(change)——锚定面据此收敛,杀跨变更 token 撞车
|
|
92
|
+
* (litellm-retire 实证:他变更提交消息含 task-01/04/05 被误锚);缺省恒 true 向后兼容。
|
|
93
|
+
* @returns {Array<{hash, ts, subject, tokens, resolvable, sameChange}>} ts 取事件观测时刻(git 日期仅存证)。
|
|
64
94
|
*/
|
|
65
|
-
export function resolveCommitAnchors(events, gitLook) {
|
|
95
|
+
export function resolveCommitAnchors(events, gitLook, change = null) {
|
|
66
96
|
const out = [];
|
|
67
97
|
for (const e of events || []) {
|
|
68
98
|
if (!e || e.kind !== 'commit' || !e.detail) continue;
|
|
@@ -75,6 +105,7 @@ export function resolveCommitAnchors(events, gitLook) {
|
|
|
75
105
|
subject: info ? String(info.message || '').split(/\r?\n/)[0] : null,
|
|
76
106
|
tokens: info ? tokensOf(info.message) : [],
|
|
77
107
|
resolvable: Boolean(info),
|
|
108
|
+
sameChange: !change || (info ? String(info.message || '').includes(change) : false),
|
|
78
109
|
});
|
|
79
110
|
}
|
|
80
111
|
return out;
|
|
@@ -125,11 +156,13 @@ function fmtDur(ms) {
|
|
|
125
156
|
* @param {string} args.change 变更名
|
|
126
157
|
* @param {Array} args.events 事件流(readWatcherEvents.events)
|
|
127
158
|
* @param {Array|null} args.tasks parseTaskLines 产物(null=任务面缺失)
|
|
128
|
-
* @param {Array} args.anchors resolveCommitAnchors
|
|
159
|
+
* @param {Array} args.anchors resolveCommitAnchors 产物(锚定只取 sameChange!==false 的行)
|
|
129
160
|
* @param {number|null} args.birthTs 诞生锚(工件 frontmatter created_at 毫秒;null=缺)
|
|
130
161
|
* @param {string|null} args.tier flow-state tier(null=缺/未知)
|
|
162
|
+
* @param {Array|null} args.tickLog parseTickLog 产物(2026-10-14-timeline-ticktime-anchor-scope:
|
|
163
|
+
* 侧车勾选留痕——任务行时刻三级降级:侧车精确(无 ≈)> 事件流推断(≈)> ?)
|
|
131
164
|
*/
|
|
132
|
-
export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }) {
|
|
165
|
+
export function renderTimeline({ change, events, tasks, anchors, birthTs, tier, tickLog = null }) {
|
|
133
166
|
const L = [];
|
|
134
167
|
L.push(`📅 ${change} — 合成时间线(${tier || 'tier 未知'}|事件流 × tasks.md × git 提交锚)`);
|
|
135
168
|
L.push('');
|
|
@@ -140,16 +173,22 @@ export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }
|
|
|
140
173
|
|
|
141
174
|
if (tasks) {
|
|
142
175
|
L.push('');
|
|
143
|
-
L.push('──
|
|
176
|
+
L.push('── 任务面(勾选时刻:精确=task tick 侧车留痕|≈=事件流顺序推断|tasks.md 描述行 × 提交锚)──');
|
|
144
177
|
const { times, broken } = inferFlipTimes(events, tasks.length);
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
const tickMap = new Map((tickLog || []).map((r) => [String(r && r.task), Number(r && r.ts)]));
|
|
179
|
+
// 断裂注按需打(2026-10-14-timeline-ticktime-anchor-scope):侧车已覆盖全部已勾位时推断层
|
|
180
|
+
// 与展示无关——watcher 重复采样(0→1 双拍)也会撞断游标,此时打断裂注是误导噪音。
|
|
181
|
+
const inferenceMatters = tasks.some((t) => t.checked && !tickMap.has(t.id));
|
|
182
|
+
if (broken && inferenceMatters) L.push('(计数链中段断裂——断裂点后勾选时刻推断不可用,标 ?)');
|
|
183
|
+
if (tasks.some((t, i) => t.checked && times[i] == null && !tickMap.has(t.id))) L.push('(已勾任务缺推断时刻——观测盲窗/水位丢失,标 ?)');
|
|
147
184
|
for (let i = 0; i < tasks.length; i++) {
|
|
148
185
|
const t = tasks[i];
|
|
149
|
-
const when = t.checked
|
|
150
|
-
?
|
|
151
|
-
:
|
|
152
|
-
|
|
186
|
+
const when = !t.checked
|
|
187
|
+
? '未勾'
|
|
188
|
+
: tickMap.has(t.id) ? fmtHMS(tickMap.get(t.id))
|
|
189
|
+
: times[i] != null ? `≈${fmtHMS(times[i])}`
|
|
190
|
+
: '?';
|
|
191
|
+
const anchor = anchors.filter((a) => a && a.sameChange !== false && a.tokens.includes(t.id)).sort((a, b) => a.ts - b.ts)[0];
|
|
153
192
|
const ev = anchor ? anchor.hash : (t.checked ? '无提交锚⚠️' : '—');
|
|
154
193
|
L.push(`${t.id} ${when.padEnd(10)} ${clip(t.desc, 44).padEnd(46)}${ev}`);
|
|
155
194
|
}
|
|
@@ -169,7 +208,7 @@ export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }
|
|
|
169
208
|
if (stages.length > 0) {
|
|
170
209
|
L.push(`阶段墙钟:${stages.map((s) => `${s.stage} ${fmtDur(s.durationMs)}`).join('|')}`);
|
|
171
210
|
}
|
|
172
|
-
L.push('注:观测起点≠诞生时刻(watcher
|
|
211
|
+
L.push('注:观测起点≠诞生时刻(watcher 后拉起/单飞锁盲窗);勾选时刻精确值=task tick 侧车留痕(task-ticks.jsonl),≈=事件流顺序推断(事件不记任务 id);描述行含机器稿截断。');
|
|
173
212
|
return L.join('\n');
|
|
174
213
|
}
|
|
175
214
|
|
|
@@ -179,27 +218,29 @@ export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }
|
|
|
179
218
|
* 烤制落盘——诚实注记而非数据错误。eventsCopySkipped=副本未随包形态(尺寸超帽或读源失败,
|
|
180
219
|
* 评审 P3-1:注记必须与实际产出一致,不虚报副本在场)。
|
|
181
220
|
*/
|
|
182
|
-
export function renderBakedTimeline({ change, events, tasks, anchors, birthTs, tier, bakedAtIso, eventsCopySkipped = false }) {
|
|
221
|
+
export function renderBakedTimeline({ change, events, tasks, anchors, birthTs, tier, bakedAtIso, eventsCopySkipped = false, tickLog = null }) {
|
|
183
222
|
const head = [
|
|
184
223
|
`# 合成时间线快照 — ${change}`,
|
|
185
224
|
'',
|
|
186
225
|
`> 烤制于归档链(${bakedAtIso}):事件流机本位(.runtime gitignore),本快照随归档包进 git 跨机可读。`,
|
|
187
|
-
'> 末尾事件(含「目录已移入 archive
|
|
226
|
+
'> 末尾事件(含「目录已移入 archive」终拍)可能晚于烤制未入快照;勾选时刻:精确=task tick 侧车留痕,≈=事件流顺序推断。',
|
|
188
227
|
eventsCopySkipped
|
|
189
228
|
? '> 原始事件副本未随包(尺寸超帽或读源失败)——细粒度复核回本机 .runtime 或平台 observation 事件面。'
|
|
190
229
|
: '> 原始事件副本:本目录 watcher-events.jsonl(时点快照)。',
|
|
191
230
|
'',
|
|
192
231
|
]
|
|
193
|
-
return head.join('\n') + renderTimeline({ change, events, tasks, anchors, birthTs, tier })
|
|
232
|
+
return head.join('\n') + renderTimeline({ change, events, tasks, anchors, birthTs, tier, tickLog })
|
|
194
233
|
}
|
|
195
234
|
|
|
196
|
-
/** change 目录双路径探测(活跃 > 归档)+ tasks.md 读取 → {changeDir, tasksMd}(缺 tasksMd=null
|
|
235
|
+
/** change 目录双路径探测(活跃 > 归档)+ tasks.md / task-ticks.jsonl 读取 → {changeDir, tasksMd, tickLog}(缺 tasksMd=null、tickLog=空数组)。 */
|
|
197
236
|
export function loadChangeTasks(specBase, change) {
|
|
198
237
|
const active = join(specBase, 'changes', change);
|
|
199
238
|
const archived = join(specBase, 'changes', 'archive', change);
|
|
200
239
|
const dir = [active, archived].find((d) => { try { return readFileSync(join(d, 'tasks.md'), 'utf8') != null } catch { return false } });
|
|
201
|
-
if (!dir) return { changeDir: null, tasksMd: null };
|
|
202
|
-
|
|
240
|
+
if (!dir) return { changeDir: null, tasksMd: null, tickLog: [] };
|
|
241
|
+
let tickLog = [];
|
|
242
|
+
try { tickLog = parseTickLog(readFileSync(join(dir, 'task-ticks.jsonl'), 'utf8')); } catch { /* 侧车缺失/不可读按空 */ }
|
|
243
|
+
return { changeDir: dir, tasksMd: readFileSync(join(dir, 'tasks.md'), 'utf8'), tickLog };
|
|
203
244
|
}
|
|
204
245
|
|
|
205
246
|
/** 诞生锚:change 目录工件 frontmatter created_at(requirements > proposal > design > tasks)。 */
|
|
@@ -215,4 +256,4 @@ export function readBirthTs(changeDir) {
|
|
|
215
256
|
return null;
|
|
216
257
|
}
|
|
217
258
|
|
|
218
|
-
export default { parseTaskLines, inferFlipTimes, resolveCommitAnchors, renderTimeline, renderBakedTimeline, loadChangeTasks, readBirthTs };
|
|
259
|
+
export default { parseTaskLines, parseTickLog, inferFlipTimes, resolveCommitAnchors, renderTimeline, renderBakedTimeline, loadChangeTasks, readBirthTs };
|
package/src/verify-postcheck.js
CHANGED
|
@@ -4223,6 +4223,51 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
4223
4223
|
}
|
|
4224
4224
|
}
|
|
4225
4225
|
|
|
4226
|
+
// —— EOL-only 噪声过滤(坑 verify-close-gate-staggered-exposure 建议④,2026-10-15 消费仓实证:
|
|
4227
|
+
// gen:types 幂等重生成纯行尾 diff 进 actual 面 → ③类 undeclared 假红,agent 被迫为零内容
|
|
4228
|
+
// 改动补声明或回退)。手法与 run/shared.js quick 审计的 eolOnly 先例同源:同一 git 范围
|
|
4229
|
+
// 跑 plain 名单与 --ignore-cr-at-eol 名单,差集 = 纯行尾差异 → 剔出对账面,eolOnlyFiles
|
|
4230
|
+
// 软留痕。两条硬边界:① 必须正向出现在 plain diff 且内容零变更才剔——absence-based
|
|
4231
|
+
// (「不在真实集即剔」)会误伤 B3 pathspec/救援窗口来源的已交付文件(它们不在任何
|
|
4232
|
+
// git diff 里);② --name-only 不吃 --ignore-cr-at-eol(git 实测怪癖),内容过滤必须走
|
|
4233
|
+
// --numstat。探测失败 fail-open 不过滤;untracked 天然不在 plain diff 不受影响;删除/
|
|
4234
|
+
// 重命名是内容变更(numstat 在场)不受影响。——
|
|
4235
|
+
const eolOnlyFiles = []
|
|
4236
|
+
try {
|
|
4237
|
+
const eolOnlySet = new Set()
|
|
4238
|
+
const probePair = (dir, refs) => {
|
|
4239
|
+
const plainOut = gitQuiet(dir, ['diff', '--name-only', ...refs], { timeout: 30 * 1000, trim: false })
|
|
4240
|
+
if (plainOut === null) return
|
|
4241
|
+
const realOut = gitQuiet(dir, ['diff', '--numstat', '--ignore-cr-at-eol', ...refs], { timeout: 30 * 1000, trim: false })
|
|
4242
|
+
if (realOut === null) return
|
|
4243
|
+
const real = new Set()
|
|
4244
|
+
for (const l of realOut.split('\n')) {
|
|
4245
|
+
const i = l.lastIndexOf('\t')
|
|
4246
|
+
if (i > 0) real.add(normalizeReconcilePath(l.slice(i + 1)))
|
|
4247
|
+
}
|
|
4248
|
+
for (const f of plainOut.split('\n').map(x => x.trim()).filter(Boolean)) {
|
|
4249
|
+
const n = normalizeReconcilePath(f)
|
|
4250
|
+
if (!real.has(n)) eolOnlySet.add(n)
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
// 工作树(HEAD 对照):主仓 + worktree(form A 双仓各自 HEAD)
|
|
4254
|
+
const probeDirs = [cwd]
|
|
4255
|
+
if (form === 'worktree' && wtMeta && wtMeta.gitDir && wtMeta.gitDir !== cwd) probeDirs.push(wtMeta.gitDir)
|
|
4256
|
+
for (const dir of probeDirs) probePair(dir, ['HEAD'])
|
|
4257
|
+
// 已提交侧:form B B1 同基点(mergeBase 对照工作树);form A worktree 已提交段(meta 锚点..HEAD)
|
|
4258
|
+
if (form === 'post-apply' && baseAnchor) {
|
|
4259
|
+
probePair(cwd, [baseAnchor])
|
|
4260
|
+
} else if (form === 'worktree' && wtMeta) {
|
|
4261
|
+
const mb = wtMeta.meta && (wtMeta.meta.baselineCommit || wtMeta.meta.actualBaseHash || wtMeta.meta.baseHash)
|
|
4262
|
+
if (mb) probePair(wtMeta.gitDir, [mb, 'HEAD'])
|
|
4263
|
+
}
|
|
4264
|
+
for (const f of eolOnlySet) {
|
|
4265
|
+
if (union.has(f)) {
|
|
4266
|
+
union.delete(f)
|
|
4267
|
+
eolOnlyFiles.push(f)
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
} catch { /* EOL 探测异常 → 不过滤(fail-open,行为与修复前一致) */ }
|
|
4226
4271
|
const files = [...new Set(filterDeliverableFiles([...union]).filter(Boolean))].sort()
|
|
4227
4272
|
// 多会话推进死锁诊断(reconcile-source-isolation,R18-SF-full 实证):post-apply 形态下
|
|
4228
4273
|
// 实际面全空(B1 无 ref + porcelain 无未提交面 + 无 apply-pathspec 兜底)= 典型「主仓被
|
|
@@ -4232,7 +4277,7 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
4232
4277
|
const parallelAdvanceHint = (form === 'post-apply' && files.length === 0 && union.size === 0 && !sources.some((s) => s.startsWith('main:diff-merge-base') || s.startsWith('apply-pathspec')))
|
|
4233
4278
|
? '⚠️ actual 源全空(无分支锚点 diff + 主仓无未提交面 + 无 apply-pathspec 兜底)——典型「主仓被并行会话推进」形态(本变更的提交/暂存可能已被他侧裸提交扫走)。切勿用「暂存物化」自救(touch 文件进共享暂存区——会被再次扫走且污染他侧,R18-SF-full 实证 matched=0 死循环);出路:① 恢复/登记本变更分支(worktree meta.json 的 branch 字段)让锚定 diff 生效后重跑 ② 确属全量已在他侧提交:与该会话按 AGENTS 规则 18 对账后以该提交为锚核销'
|
|
4234
4279
|
: null
|
|
4235
|
-
return { ok: true, form, files, sources, foreignExcluded, foreignExcludedFiles, degradedReason: null, baseAnchor, commitWindowFiles, parallelAdvanceHint }
|
|
4280
|
+
return { ok: true, form, files, sources, foreignExcluded, foreignExcludedFiles, degradedReason: null, baseAnchor, commitWindowFiles, parallelAdvanceHint, eolOnlyFiles }
|
|
4236
4281
|
}
|
|
4237
4282
|
|
|
4238
4283
|
/**
|
|
@@ -4462,6 +4507,9 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
4462
4507
|
notes.push(`③类另有 ${scaffoldTotal} 个工具/平台脚手架文件(platform ${scaffoldBuckets.platform} / tool ${scaffoldBuckets.tool},如 ${scaffoldSamples.join('、')}${scaffoldTotal > scaffoldSamples.length ? ' 等' : ''})已聚合不逐条列——非交付物(scope-audit 设施桶同口径)`)
|
|
4463
4508
|
}
|
|
4464
4509
|
|
|
4510
|
+
if (Array.isArray(actual.eolOnlyFiles) && actual.eolOnlyFiles.length > 0) {
|
|
4511
|
+
notes.push(`EOL-only 剔除:${actual.eolOnlyFiles.length} 个文件仅行尾差异(--ignore-cr-at-eol 对照零内容变更,如 gen 脚本幂等重生成)已剔出对账面——${actual.eolOnlyFiles.slice(0, 5).join('、')}${actual.eolOnlyFiles.length > 5 ? ' 等' : ''}(.gitattributes 统一行尾可根治)`)
|
|
4512
|
+
}
|
|
4465
4513
|
// 状态聚合:②在场即 ERROR 态优先(gates 阻断语义靠它兑现);仅③ → WARNING 态
|
|
4466
4514
|
const status = missing.length > 0 ? 'missing_declared' : (undeclared.length > 0 ? 'undeclared' : 'ok')
|
|
4467
4515
|
return { status, matched, missing, undeclared, undeclaredScaffold: scaffoldTotal, skipReason: null, notes, form: actual.form, sources: actual.sources, crossRepo }
|