sillyspec 3.30.0 → 3.32.0
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -18
- package/.zcodeignore +66 -0
- package/AGENTS.md +15 -15
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +184 -17
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/verify-probes.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
|
|
25
25
|
* 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs'
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, copyFileSync } from 'fs'
|
|
28
28
|
import { join, dirname, basename, resolve, isAbsolute, relative } from 'path'
|
|
29
29
|
import { gitQuiet, unquoteGitPath } from './git-helper.js'
|
|
30
30
|
import { hasUnconfirmedPrefill } from './prefill.js'
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from './verify-facts-schema.js'
|
|
35
35
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
36
36
|
import { parseDecisions } from './decision-distill.js'
|
|
37
|
+
import { collectFrLinkedTests } from './fr-index.js'
|
|
37
38
|
import { writeChangeTrace, orphanAccRef } from './test-bindings.js'
|
|
38
39
|
import { parseAllowedPaths, parseRepo, parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
39
40
|
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
@@ -1081,6 +1082,12 @@ const API_FACE_METHOD_RE = /(?:^|[^A-Za-z])(GET|POST|PUT|DELETE|PATCH)(?![A-Za-z
|
|
|
1081
1082
|
// 首字符字母数字/_/-/{,后续段字符含 {}/: 模板形态(/orders/{id}、/orders/:id 全串命中)
|
|
1082
1083
|
const API_FACE_PATH_RE = /(?:^|[^A-Za-z0-9])(\/[A-Za-z0-9_\-{}][\w\-./{}:]*)/
|
|
1083
1084
|
const API_FACE_DECLARED_RE = /本变更接口面[::]\s*(\d+)\s*端点/
|
|
1085
|
+
// 声明宽收(2026-10-06-verify-friction-fix task-04):同义零端点措辞认作 declared=0——厚流程实证
|
|
1086
|
+
// agent 写「无接口变更/不涉及接口」散文不被数字式声明行识别,被迫写伪表格绕过。(?<!\d) 防
|
|
1087
|
+
// 「10 端点」的尾 0 误命中(数字声明优先级在前,并存时同义式不参与);(?<!非) 防「非零端点」
|
|
1088
|
+
// 误命中(2026-10-06-verify-docs-prefill task-04 评审 P3-2 清偿——critical 判级下误命中会把
|
|
1089
|
+
// D-005 零面硬错降为 warning,影响面不止 advisory 渲染)。
|
|
1090
|
+
const API_FACE_DECLARED_ZERO_RE = /无接口变更|不涉及接口(?:变更)?|(?<!非)零端点|无端点|(?<!\d)0\s*端点/
|
|
1084
1091
|
|
|
1085
1092
|
/**
|
|
1086
1093
|
* design.md 接口段 tolerant 解析(task-04 / D-005)。纯函数、本地正则零新依赖;输入统一
|
|
@@ -1091,8 +1098,13 @@ const API_FACE_DECLARED_RE = /本变更接口面[::]\s*(\d+)\s*端点/
|
|
|
1091
1098
|
*/
|
|
1092
1099
|
export function parseDesignApiTable(designMd) {
|
|
1093
1100
|
const text = String(designMd || '').replace(/\r\n/g, '\n')
|
|
1094
|
-
|
|
1095
|
-
|
|
1101
|
+
// 声明匹配面剥 HTML 注释(task-04 宽收配套):design 骨架的接口段 TODO 指引自带可粘贴句式
|
|
1102
|
+
// 「本变更接口面:0 端点(无接口变更)」,留在注释里被匹配会让每份新骨架自动声明零端点。
|
|
1103
|
+
// 数字式声明在注释内的同理由不算(注释是写作指引不是声明正文——fail-closed 方向收紧)。
|
|
1104
|
+
const textNoComments = text.replace(/<!--[\s\S]*?-->/g, '')
|
|
1105
|
+
const dm = textNoComments.match(API_FACE_DECLARED_RE)
|
|
1106
|
+
let declared = dm ? parseInt(dm[1], 10) : null
|
|
1107
|
+
if (declared === null && API_FACE_DECLARED_ZERO_RE.test(textNoComments)) declared = 0
|
|
1096
1108
|
const endpoints = []
|
|
1097
1109
|
const sectionHint = []
|
|
1098
1110
|
for (const sec of splitMdSections(text)) {
|
|
@@ -1949,6 +1961,11 @@ function renderProbe7Lines(p7) {
|
|
|
1949
1961
|
for (const t of (p7.tasks || [])) {
|
|
1950
1962
|
L.push('')
|
|
1951
1963
|
L.push(`**${t.task}**`)
|
|
1964
|
+
if (Array.isArray(t.existingTests) && t.existingTests.length > 0) {
|
|
1965
|
+
// 既有用例注记(2026-10-06-verify-docs-prefill task-02):归属列里的这些测试是 FR 关联回归
|
|
1966
|
+
// 候选(本变更未改动)——与自身新建测试区分,判定仍由 agent 复核(命中≠结论)
|
|
1967
|
+
L.push(`- ℹ️ 既有用例候选(FR 关联回归面,本变更未改动;判定仍由你复核,命中≠结论):${t.existingTests.map(f => `\`${f}\``).join('、')}`)
|
|
1968
|
+
}
|
|
1952
1969
|
if (t.fmError) {
|
|
1953
1970
|
// 坏 YAML 如实陈述(2026-09-20-taskcard-yaml-hardgate):非「无 acceptance」防御——
|
|
1954
1971
|
// plan 门禁 0b 硬校验应已拦截;已过 plan 门仍见此行 = 门禁失效信号(可反馈工具缺陷)
|
|
@@ -2187,6 +2204,15 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
2187
2204
|
}
|
|
2188
2205
|
}
|
|
2189
2206
|
} catch { /* 注册表不可读 → 无跨仓根(fail-open) */ }
|
|
2207
|
+
// ── 既有用例候选(2026-10-06-verify-docs-prefill task-02):FR 关联回归面(active FR
|
|
2208
|
+
// 覆盖 ∩ 本变更触碰文件的绑定 tests——CLI 收口实测面三源之二)作为无归属卡的预填
|
|
2209
|
+
// 候选。postmortem 实证 38 格矩阵手誊 40 分钟:无归属格预填「判定大概率 uncovered」,
|
|
2210
|
+
// agent 只能全仓找既有测试锚点。只注入 testFiles 为空的卡(有自身归属的卡零噪音);
|
|
2211
|
+
// 变更级一次计算,逐卡不重算;命中≠判定(预填语义同其他格子——agent 复核改写)。
|
|
2212
|
+
let frLinkedExisting = null
|
|
2213
|
+
try {
|
|
2214
|
+
frLinkedExisting = collectFrLinkedTests({ specBase, changeName, changedFiles: detailed.map(e => e.path), projectRoot: cwd })
|
|
2215
|
+
} catch { frLinkedExisting = null /* FR 索引不可读 → 无候选(行为与现状一致) */ }
|
|
2190
2216
|
// 跨卡归属(坑 probe7-provider-tests-in-consumer-card,2026-09-16 E 变更 verify 实证:8 格
|
|
2191
2217
|
// 被机械预填 uncovered——task-03(测试卡)的用例测的是 task-01(provider)的导出函数,但归属
|
|
2192
2218
|
// 只看本卡 allowed_paths ∪ review changedFiles,provider 卡的 acceptance 永远连不上消费卡的
|
|
@@ -2216,12 +2242,21 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
2216
2242
|
}
|
|
2217
2243
|
}
|
|
2218
2244
|
const testFiles = [...new Set([...fromAllowed, ...fromReview, ...fromDependents])]
|
|
2245
|
+
// 无归属卡注入 FR 关联回归既有用例候选(task-02;有归属卡 existingTests 恒空——零噪音)
|
|
2246
|
+
const existingTests = []
|
|
2247
|
+
if (frLinkedExisting && testFiles.length === 0) {
|
|
2248
|
+
for (const f of frLinkedExisting.files) {
|
|
2249
|
+
if (isProbe7TestPath(f) && !existingTests.includes(f)) existingTests.push(f)
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const finalTestFiles = existingTests.length > 0 ? [...new Set([...testFiles, ...existingTests])] : testFiles
|
|
2219
2253
|
probe7.tasks.push({
|
|
2220
2254
|
task: card.task,
|
|
2221
2255
|
acceptance: card.acceptance,
|
|
2222
2256
|
fmError: card.fmError || null,
|
|
2223
|
-
testFiles,
|
|
2224
|
-
|
|
2257
|
+
testFiles: finalTestFiles,
|
|
2258
|
+
existingTests,
|
|
2259
|
+
hints: buildAcceptanceHints(card.acceptance, finalTestFiles, cwd, wtRoot, probe7CrossRoots),
|
|
2225
2260
|
})
|
|
2226
2261
|
}
|
|
2227
2262
|
// ── trace 落盘(2026-09-24-fr-test-bindings task-03,fr-test-binding 写侧):矩阵归属
|
|
@@ -3643,3 +3678,179 @@ export function generateVerifyResultSkeleton(result) {
|
|
|
3643
3678
|
]
|
|
3644
3679
|
return L.join('\n')
|
|
3645
3680
|
}
|
|
3681
|
+
|
|
3682
|
+
// ── 防丢失与定向刷新(2026-10-06-verify-friction-fix task-01)──
|
|
3683
|
+
// postmortem(sess_4769fd5d):--force 整体重置全骨架且无备份,38 格手填复核成果被清后只能靠
|
|
3684
|
+
// 对话记录重建。两件护栏:
|
|
3685
|
+
// backupVerifyResult——覆盖/刷新前落时间戳备份(fail-soft:失败返回 null 不阻断,但 CLI 层
|
|
3686
|
+
// 会醒目提示手动备份);
|
|
3687
|
+
// refreshProbeSections——定向刷新:只动「仍含待填占位(<待填 / <!--TODO)的探针机械段」,
|
|
3688
|
+
// 已手填段保留;被刷新段内的表格已填行按行携载到新渲染(半填矩阵不丢已填格)。安全方向
|
|
3689
|
+
// 取「宁可少刷新不误删」:探针 1/3/5/6 等直接渲染结论行、无占位的段永远保留旧文(stale
|
|
3690
|
+
// 由 gate 的 checkProbeConsistency 独立把关,不靠 refresh 兜)。
|
|
3691
|
+
|
|
3692
|
+
/**
|
|
3693
|
+
* 覆盖/刷新 verify-result.md 前自动备份到 <runtimeRoot>/verify-runs/。
|
|
3694
|
+
* @param {{ mdPath: string, runtimeRoot: string, label?: string }} opts
|
|
3695
|
+
* @returns {string|null} 备份文件绝对路径;目标缺失/写失败返回 null(fail-soft,调用方提示)
|
|
3696
|
+
*/
|
|
3697
|
+
export function backupVerifyResult({ mdPath, runtimeRoot, label = 'verify-result', changeName = null }) {
|
|
3698
|
+
try {
|
|
3699
|
+
if (!existsSync(mdPath)) return null
|
|
3700
|
+
const runsDir = join(runtimeRoot, 'verify-runs')
|
|
3701
|
+
mkdirSync(runsDir, { recursive: true })
|
|
3702
|
+
// 毫秒精度时间戳:同秒连跑两次不互相覆盖(秒级 ts 会静默吞前一份备份)
|
|
3703
|
+
const ts = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 17)
|
|
3704
|
+
// change 段(2026-10-06-verify-docs-prefill task-04 评审 P3-1 清偿):多 change 并行时备份
|
|
3705
|
+
// 归属靠文件名可辨,不靠内容;缺省不带(向后兼容旧调用形态)
|
|
3706
|
+
const changeSeg = changeName ? `${changeName.replace(/[^A-Za-z0-9._-]/g, '_')}-` : ''
|
|
3707
|
+
const backupPath = join(runsDir, `${label}-backup-${changeSeg}${ts}.md`)
|
|
3708
|
+
copyFileSync(mdPath, backupPath)
|
|
3709
|
+
return backupPath
|
|
3710
|
+
} catch {
|
|
3711
|
+
return null
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
const PROBE_SECTION_HEADING_RE = /^#### 探针 (\d+)[::]/
|
|
3716
|
+
|
|
3717
|
+
/**
|
|
3718
|
+
* 按行切出探针段范围(#### 探针 N: 起,至下一任意级别标题或 EOF 止)。
|
|
3719
|
+
* @param {string} text 全文
|
|
3720
|
+
* @returns {{ lines: string[], sections: Array<{ num: number, startLine: number, endLine: number }> }}
|
|
3721
|
+
*/
|
|
3722
|
+
function splitProbeSectionRanges(text) {
|
|
3723
|
+
const lines = String(text ?? '').split('\n')
|
|
3724
|
+
const sections = []
|
|
3725
|
+
let cur = null
|
|
3726
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3727
|
+
const m = lines[i].match(PROBE_SECTION_HEADING_RE)
|
|
3728
|
+
if (m) {
|
|
3729
|
+
if (cur) sections.push({ ...cur, endLine: i })
|
|
3730
|
+
cur = { num: parseInt(m[1], 10), startLine: i }
|
|
3731
|
+
} else if (cur && /^#{1,4} /.test(lines[i])) {
|
|
3732
|
+
// 边界认任意 1-4 级标题(2026-10-06-verify-docs-prefill task-04 评审 P3-3 清偿):只认
|
|
3733
|
+
// #{1,3} 时 agent 在含占位段后手写的 #### 子节会被并入段内,刷新时连带被吞
|
|
3734
|
+
sections.push({ ...cur, endLine: i })
|
|
3735
|
+
cur = null
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
if (cur) sections.push({ ...cur, endLine: lines.length })
|
|
3739
|
+
return { lines, sections }
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
/**
|
|
3743
|
+
* 段级合并:用 fresh 渲染替换段内容,但旧段里已填的表格行(不含 <待填/<TODO 占位)按首列
|
|
3744
|
+
* 键携载到新渲染的同键行上——半填矩阵的已填格不丢。旧已填行在新渲染无同键行时追加到段尾
|
|
3745
|
+
* (验收项从 design 撤下等 stale 形态,宁可保留给 agent 裁决)。
|
|
3746
|
+
* @param {string[]} oldLines 旧段行
|
|
3747
|
+
* @param {string[]} freshLines 新渲染行
|
|
3748
|
+
* @returns {{ text: string, carried: number }}
|
|
3749
|
+
*/
|
|
3750
|
+
function mergeProbeSection(oldLines, freshLines) {
|
|
3751
|
+
const firstCell = (l) => {
|
|
3752
|
+
const cell = l.split('|')[1]
|
|
3753
|
+
return cell ? cell.trim() : null
|
|
3754
|
+
}
|
|
3755
|
+
const oldFilled = new Map()
|
|
3756
|
+
for (const l of oldLines) {
|
|
3757
|
+
if (!/^\s*\|/.test(l)) continue
|
|
3758
|
+
if (/<待填|<TODO/.test(l)) continue
|
|
3759
|
+
const key = firstCell(l)
|
|
3760
|
+
if (!key || /^-{3,}$/.test(key)) continue // 分隔行/空键不携载
|
|
3761
|
+
oldFilled.set(key, l)
|
|
3762
|
+
}
|
|
3763
|
+
if (oldFilled.size === 0) return { text: freshLines.join('\n'), carried: 0 }
|
|
3764
|
+
let carried = 0
|
|
3765
|
+
const out = []
|
|
3766
|
+
const used = new Set()
|
|
3767
|
+
for (const l of freshLines) {
|
|
3768
|
+
if (/^\s*\|/.test(l)) {
|
|
3769
|
+
const key = firstCell(l)
|
|
3770
|
+
if (key && oldFilled.has(key)) {
|
|
3771
|
+
out.push(oldFilled.get(key))
|
|
3772
|
+
used.add(key)
|
|
3773
|
+
carried++
|
|
3774
|
+
continue
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
out.push(l)
|
|
3778
|
+
}
|
|
3779
|
+
const stale = [...oldFilled.entries()].filter(([k]) => !used.has(k))
|
|
3780
|
+
if (stale.length > 0) {
|
|
3781
|
+
out.push('', '<!-- refresh 携载的已填行(新渲染中无同键行,agent 裁决去留) -->')
|
|
3782
|
+
for (const [, l] of stale) out.push(l)
|
|
3783
|
+
}
|
|
3784
|
+
return { text: out.join('\n'), carried }
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
/**
|
|
3788
|
+
* 定向刷新探针预填段(--refresh-probes 的核心,纯函数)。
|
|
3789
|
+
* 语义(安全方向:宁可少刷新不误删):
|
|
3790
|
+
* - 含待填占位(<待填 或 <!--TODO)的旧探针段 → 用 fresh 同号段替换(表格已填行携载);
|
|
3791
|
+
* - 无占位的旧探针段(已手填或纯机械结论行)→ 原样保留;
|
|
3792
|
+
* - fresh 里有而旧文没有的探针号 → 追加(有旧段时插在最后一个旧探针段后,无旧段时按
|
|
3793
|
+
* --init 补注入口径带包装标题整体注入);
|
|
3794
|
+
* - 旧文有而 fresh 没有的探针号 → 保留不动(残段不销毁)。
|
|
3795
|
+
* @param {string} existingText verify-result.md 现文
|
|
3796
|
+
* @param {string} freshReportText renderVerifyProbesReport 新渲染
|
|
3797
|
+
* @returns {{ text: string, replaced: number[], kept: number[], appended: number[], carriedRows: number }}
|
|
3798
|
+
*/
|
|
3799
|
+
export function refreshProbeSections(existingText, freshReportText) {
|
|
3800
|
+
const { lines, sections } = splitProbeSectionRanges(existingText)
|
|
3801
|
+
const fresh = splitProbeSectionRanges(freshReportText)
|
|
3802
|
+
const freshByNum = new Map(fresh.sections.map(s => [s.num, s]))
|
|
3803
|
+
const replaced = []
|
|
3804
|
+
const kept = []
|
|
3805
|
+
const appended = []
|
|
3806
|
+
let carriedRows = 0
|
|
3807
|
+
|
|
3808
|
+
// 替换计划:旧段起始行 → { endLine, mergedText }
|
|
3809
|
+
const replaceMap = new Map()
|
|
3810
|
+
for (const s of sections) {
|
|
3811
|
+
const freshSec = freshByNum.get(s.num)
|
|
3812
|
+
if (!freshSec) continue // 残段保留
|
|
3813
|
+
const oldText = lines.slice(s.startLine, s.endLine).join('\n')
|
|
3814
|
+
if (/<待填|<!--TODO/.test(oldText)) {
|
|
3815
|
+
const merged = mergeProbeSection(
|
|
3816
|
+
lines.slice(s.startLine, s.endLine),
|
|
3817
|
+
fresh.lines.slice(freshSec.startLine, freshSec.endLine),
|
|
3818
|
+
)
|
|
3819
|
+
carriedRows += merged.carried
|
|
3820
|
+
replaceMap.set(s.startLine, { endLine: s.endLine, text: merged.text })
|
|
3821
|
+
replaced.push(s.num)
|
|
3822
|
+
} else {
|
|
3823
|
+
kept.push(s.num)
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
for (const num of freshByNum.keys()) {
|
|
3827
|
+
if (!sections.some(o => o.num === num)) appended.push(num)
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
// 新段插入点(原行号空间):有旧探针段 → 最后一个旧段结束处;无 → EOF
|
|
3831
|
+
const insertions = new Map()
|
|
3832
|
+
if (appended.length > 0) {
|
|
3833
|
+
const appendText = appended
|
|
3834
|
+
.map(num => fresh.lines.slice(freshByNum.get(num).startLine, freshByNum.get(num).endLine).join('\n'))
|
|
3835
|
+
.join('\n')
|
|
3836
|
+
const insIdx = sections.length > 0 ? sections[sections.length - 1].endLine : lines.length
|
|
3837
|
+
insertions.set(insIdx, sections.length > 0
|
|
3838
|
+
? [appendText, '']
|
|
3839
|
+
: ['', '## 探针结果(CLI 机械预填,--refresh-probes 补注入) [层:可复跑探针——gate 抽查防篡改]', appendText, ''])
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
const out = []
|
|
3843
|
+
for (let i = 0; i <= lines.length; i++) {
|
|
3844
|
+
const ins = insertions.get(i)
|
|
3845
|
+
if (ins) out.push(...ins)
|
|
3846
|
+
if (i === lines.length) break
|
|
3847
|
+
const rep = replaceMap.get(i)
|
|
3848
|
+
if (rep) {
|
|
3849
|
+
out.push(rep.text)
|
|
3850
|
+
i = rep.endLine - 1
|
|
3851
|
+
continue
|
|
3852
|
+
}
|
|
3853
|
+
out.push(lines[i])
|
|
3854
|
+
}
|
|
3855
|
+
return { text: out.join('\n'), replaced, kept, appended, carriedRows }
|
|
3856
|
+
}
|