sillyspec 3.28.3 → 3.28.4
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 +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +7 -2
- package/src/change-risk-profile.js +77 -6
- package/src/config-cat.js +12 -1
- package/src/constants.js +3 -1
- package/src/docs-check.js +34 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +181 -6
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +16 -0
- package/src/progress.js +62 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +30 -9
- package/src/run/complete-handlers.js +152 -6
- package/src/run/complete.js +45 -0
- package/src/run/gates.js +94 -6
- package/src/run/prompt.js +96 -5
- package/src/run/shared.js +74 -6
- package/src/run/stage.js +35 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review.js +3 -0
- package/src/stages/archive.js +4 -4
- package/src/stages/brainstorm.js +2 -8
- package/src/stages/doctor.js +41 -515
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +5 -26
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +10 -6
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +25 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +257 -19
- package/src/verify-probes.js +142 -13
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +51 -13
- package/src/worktree.js +5 -1
package/src/archive-delta.js
CHANGED
|
@@ -34,6 +34,7 @@ import { parseDecisionDomains, loadModuleMap, deriveActualModules } from './desi
|
|
|
34
34
|
import { parseModuleMapSimple } from './modules.js'
|
|
35
35
|
import { scanBackendEndpoints } from './endpoint-extractor.js'
|
|
36
36
|
import { diffEndpointSets } from './endpoint-baseline.js'
|
|
37
|
+
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
37
38
|
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// 小工具(全部 fail-soft:读不到/解析失败 → null / [],不抛出)
|
|
@@ -460,3 +461,84 @@ export function writeLastDeltaSidecar(runtimeRoot, summary) {
|
|
|
460
461
|
return { ok: false, reason: e && e.message ? e.message : String(e) }
|
|
461
462
|
}
|
|
462
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* archive 三重核对(2026-09-09 ql-20260909-004,轮次经济学 §3.1 archive 收口机械化):
|
|
466
|
+
* module-impact.md 矩阵记录 vs 真实变更文件 vs module-map 归属——机械比对 CLI 代算,
|
|
467
|
+
* agent 只裁决不一致项。只读,供 archive extract-module-impact 步 prompt 注入/命令消费。
|
|
468
|
+
* @returns {{ ok: boolean, mismatches: string[], impactFiles: string[], actualFiles: string[], summary: string }}
|
|
469
|
+
*/
|
|
470
|
+
export function auditModuleImpactAgainstDiff({ cwd, changeName, specDir = null }) {
|
|
471
|
+
const mismatches = []
|
|
472
|
+
const specBase = specDir || join(cwd, '.sillyspec')
|
|
473
|
+
const impactPath = join(specBase, 'changes', changeName, 'module-impact.md')
|
|
474
|
+
let impactText = null
|
|
475
|
+
try { impactText = readFileSync(impactPath, 'utf8') } catch { /* 缺失由降级路径处理 */ }
|
|
476
|
+
const actualFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase: specDir || join(cwd, '.sillyspec') }) || [])
|
|
477
|
+
.map(f => String(f).replace(/\\/g, '/'))
|
|
478
|
+
.filter(f => !f.startsWith('.sillyspec/changes/'))
|
|
479
|
+
if (!impactText) {
|
|
480
|
+
return { ok: false, mismatches: ['module-impact.md 不存在(走 --init 降级路径)'], impactFiles: [], actualFiles, summary: `真实变更 ${actualFiles.length} 文件;无 module-impact 可核` }
|
|
481
|
+
}
|
|
482
|
+
// 矩阵反引号路径收集(启发式——advisory 注入用,非硬门真相源:无反引号表格格会漏,外部审核 P3-3)
|
|
483
|
+
const impactFiles = [...impactText.matchAll(/`([^`]+[.](?:js|mjs|ts|py|java|md|yaml))`/g)].map(m => m[1].split(String.fromCharCode(92)).join('/'))
|
|
484
|
+
const impactSet = new Set(impactFiles)
|
|
485
|
+
const actualSet = new Set(actualFiles)
|
|
486
|
+
const inDiffNotDoc = [...actualSet].filter(f => !impactSet.has(f))
|
|
487
|
+
const inDocNotDiff = [...impactSet].filter(f => !actualSet.has(f))
|
|
488
|
+
if (inDiffNotDoc.length > 0) mismatches.push(`diff 有而 module-impact 未列(${inDiffNotDoc.length}):${inDiffNotDoc.slice(0, 5).join('、')}${inDiffNotDoc.length > 5 ? ' …' : ''}`)
|
|
489
|
+
if (inDocNotDiff.length > 0) mismatches.push(`module-impact 列而 diff 无(${inDocNotDiff.length}):${inDocNotDiff.slice(0, 5).join('、')}${inDocNotDiff.length > 5 ? ' …' : ''}`)
|
|
490
|
+
// 第三重:module-map 归属一致性(2026-09-09 外部审核 P2-1——「三重核对」此前名实不符:
|
|
491
|
+
// 矩阵行的模块列 vs map 前缀推导——误标模块点名;未命中 map 的归 unmatched 提示不判红)
|
|
492
|
+
const moduleMisattrib = auditImpactModuleAttribution(impactText, actualFiles, specBase)
|
|
493
|
+
mismatches.push(...moduleMisattrib)
|
|
494
|
+
return {
|
|
495
|
+
ok: mismatches.length === 0,
|
|
496
|
+
mismatches,
|
|
497
|
+
impactFiles,
|
|
498
|
+
actualFiles,
|
|
499
|
+
summary: `module-impact ${impactFiles.length} 文件 × 真实 diff ${actualFiles.length} 文件 × map 归属:${mismatches.length === 0 ? '一致 ✓' : mismatches.length + ' 类不一致'}`,
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 第三重核对(P2-1):矩阵行「| <模块> | `文件` |」的模块列 vs _module-map.yaml 前缀推导。
|
|
505
|
+
* 只读 fail-soft:map 不可得 → 返回 [](前两重仍有效,注入文案会带 map 跳过注记)。
|
|
506
|
+
*/
|
|
507
|
+
function auditImpactModuleAttribution(impactText, actualFiles, specBase) {
|
|
508
|
+
try {
|
|
509
|
+
// 找 _module-map.yaml(docs/<p>/modules/ 扫描——detectModuleDocHealth 同款)
|
|
510
|
+
let mapText = null
|
|
511
|
+
const docsDir = join(specBase, 'docs')
|
|
512
|
+
if (existsSync(docsDir)) {
|
|
513
|
+
for (const d of readdirSync(docsDir)) {
|
|
514
|
+
const cand = join(docsDir, d, 'modules', '_module-map.yaml')
|
|
515
|
+
if (existsSync(cand)) { mapText = readFileSync(cand, 'utf8'); break }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!mapText) return []
|
|
519
|
+
const { parseModuleMapPaths } = require_module_impact()
|
|
520
|
+
const map = parseModuleMapPaths(mapText)
|
|
521
|
+
const deriveMod = (posixPath) => {
|
|
522
|
+
for (const [id, paths] of map) {
|
|
523
|
+
for (const pp of paths) {
|
|
524
|
+
if (pp.endsWith('/') ? posixPath.startsWith(pp) : (posixPath === pp || posixPath.startsWith(pp + '/'))) return id
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return null
|
|
528
|
+
}
|
|
529
|
+
const out = []
|
|
530
|
+
for (const m of impactText.matchAll(/^\|\s*([^|]+?)\s*\|[^|]*`([^`]+)`/gm)) {
|
|
531
|
+
const recorded = m[1].trim()
|
|
532
|
+
const file = m[2].split(String.fromCharCode(92)).join('/')
|
|
533
|
+
if (recorded.startsWith('---') || recorded.startsWith('模块')) continue
|
|
534
|
+
const derived = deriveMod(file)
|
|
535
|
+
if (derived && recorded !== derived && recorded !== 'unmapped') {
|
|
536
|
+
out.push(`map 归属不一致:${file} 矩阵记「${recorded}」但 map 推导为「${derived}」——修正矩阵模块列或跑 modules rebuild`)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return out
|
|
540
|
+
} catch { return [] }
|
|
541
|
+
}
|
|
542
|
+
// 延迟引用(防环:module-impact 不反向依赖本模块)
|
|
543
|
+
import * as _moduleImpactMod from './module-impact.js'
|
|
544
|
+
function require_module_impact() { return _moduleImpactMod }
|
package/src/change-delete.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - DB:status='deleted'(与 archived 语义分离,DB 可直接审计区分,行保留不物理删)
|
|
10
10
|
* - 目录:changes/<name>/ 物理移除(git tracked 文件删除后照常 commit,历史仍可回溯)
|
|
11
11
|
* - worktree:复用 archiveWorktreeCleanup(有未 apply 变更时保留 worktree 只警告——
|
|
12
|
-
* 不因删变更丢用户唯一副本的代码;顺带清 execute/stage-review runId marker
|
|
12
|
+
* 不因删变更丢用户唯一副本的代码;顺带清 execute/stage-review runId marker
|
|
13
|
+
* + 该变更的 runtime 取证(pruneArchivedChangeRuntime))
|
|
13
14
|
* - git:safeGit add -A 暂存目录删除(best-effort,对齐 archive CLI 下沉 git add)
|
|
14
15
|
* - 平台:triggerSync 推终态(sync.js 对 status='deleted' 上行墓碑,平台收敛软删)
|
|
15
16
|
*
|
package/src/change-list.js
CHANGED
|
@@ -74,8 +74,13 @@ export function globMatch(str, pattern) {
|
|
|
74
74
|
* @returns {boolean}
|
|
75
75
|
*/
|
|
76
76
|
export function pathMatches(a, b) {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// 坑 platform-sync-progress-rollback-and-db-corruption 坑3:NEW: 前缀(待建文件标记)在
|
|
78
|
+
// 「比对」语义下剥除对齐——design 清单带 NEW: 的待建文件 vs task allowed_paths 裸路径 /
|
|
79
|
+
// 主仓脏文件,比的是目标文件而非「新建意图」,剥后两道门禁(brainstorm 幻觉核验要求加
|
|
80
|
+
// NEW: / plan 覆盖对账按裸路径匹配)不再互斥。仅在比对处剥:存在性核验(design-facts
|
|
81
|
+
// 的 NEW: 豁免)不走本函数、仍见原文前缀;target_files 解析器(plan-postcheck)本就剥。
|
|
82
|
+
a = normalizePath(a).replace(/^NEW:/, '')
|
|
83
|
+
b = normalizePath(b).replace(/^NEW:/, '')
|
|
79
84
|
if (!a || !b) return false
|
|
80
85
|
if (a === b) return true
|
|
81
86
|
if (a.startsWith(b + '/') || b.startsWith(a + '/')) return true
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* 判定 5 级风险(doc-only / unit-sufficient / contract-required /
|
|
6
6
|
* integration-critical / deployment-critical),产出门控验证需求。
|
|
7
7
|
*/
|
|
8
|
+
import { readFileSync, statSync } from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
8
10
|
|
|
9
11
|
// ============ 向后兼容:旧的 INTEGRATION_CRITICAL_PATTERNS ============
|
|
10
12
|
|
|
@@ -292,10 +294,40 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification, op
|
|
|
292
294
|
const warnings = []
|
|
293
295
|
const lower = (String(verifyContent || '') + '\n' + String(opts.extraEvidenceText || '')).toLowerCase()
|
|
294
296
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
297
|
+
// ── v2 回执槽优先(2026-09-08-ir-verify-facts FR-04,D-003@v1):「集成验证回执」槽在场时
|
|
298
|
+
// 走结构化一致性校验(绿判据:logPath 存在 × mtime ∈ verify 窗口 × 日志尾 200 行失败签名
|
|
299
|
+
// 扫描(噪声剔除)× exitCode===0),literals 降 legacy 回退(存量兼容)。
|
|
300
|
+
// extraEvidenceText(verify-services 回执注入)保留合并为补充候选文本。CLI 不代跑集成进程。──
|
|
301
|
+
if (Array.isArray(opts.runtimeEvidence) && opts.runtimeEvidence.length > 0) {
|
|
302
|
+
const greenReceipts = []
|
|
303
|
+
const receiptAudit = []
|
|
304
|
+
for (const r of opts.runtimeEvidence) {
|
|
305
|
+
const audit = auditRuntimeReceipt(r, opts)
|
|
306
|
+
receiptAudit.push(audit)
|
|
307
|
+
if (audit.green) greenReceipts.push(r)
|
|
308
|
+
}
|
|
309
|
+
const needsIntegration = requiredVerification.includes('real_daemon_backend_integration')
|
|
310
|
+
const needsLogEvidence = requiredVerification.includes('runtime_log_evidence')
|
|
311
|
+
const needsRealStartup = requiredVerification.includes('real_startup_once')
|
|
312
|
+
// 任一绿回执 = 结构化在场证据(claim 语义由 agent 声明,一致性由 CLI 校验)
|
|
313
|
+
const hasGreen = greenReceipts.length > 0
|
|
314
|
+
if (needsIntegration && !hasGreen) {
|
|
315
|
+
const bad = receiptAudit.filter(a => !a.green)
|
|
316
|
+
errors.push(`integration-critical 变更无绿回执(${bad.length} 条回执校验不过:${bad.map(a => a.reason).join(';') || '无有效回执'})——绿判据:log 存在 × mtime ∈ verify 窗口 × 日志无失败签名 × exit 0`)
|
|
317
|
+
}
|
|
318
|
+
if (needsLogEvidence && !hasGreen) {
|
|
319
|
+
errors.push('integration-critical 变更的回执未通过一致性校验(Runtime Evidence 等价物)')
|
|
320
|
+
}
|
|
321
|
+
if (needsRealStartup && !hasGreen) {
|
|
322
|
+
errors.push('deployment-critical 变更无绿回执(真实启动验证)')
|
|
323
|
+
}
|
|
324
|
+
if (receiptAudit.some(a => !a.green && a.failSignatures > 0)) {
|
|
325
|
+
warnings.push('回执日志含失败签名(error/exception/traceback/fatal 行首命中,已剔除「0 errors」类良性行)——确认是否预期内失败')
|
|
326
|
+
}
|
|
327
|
+
return { ok: errors.length === 0, errors, warnings, receiptAudit, structured: true }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── legacy literals 回退(无槽存量格式)──
|
|
299
331
|
const hasEvidence = (k) => {
|
|
300
332
|
const n = VERIFICATION_NEEDS[k]
|
|
301
333
|
if (!n || !n.literals || n.literals.length === 0) return true
|
|
@@ -335,5 +367,44 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification, op
|
|
|
335
367
|
}
|
|
336
368
|
}
|
|
337
369
|
|
|
338
|
-
return { ok: errors.length === 0, errors, warnings }
|
|
339
|
-
}
|
|
370
|
+
return { ok: errors.length === 0, errors, warnings, structured: false }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 单条回执一致性校验(FR-04 绿判据)。噪声剔除:签名行首匹配 + 剔除「0 errors」类良性行
|
|
375
|
+
* (本仓 verify-postcheck.js 测试输出解析先例同款)。fail-soft:校验依赖缺失(opts 无
|
|
376
|
+
* cwd/verifyStartAt)时只核 exitCode 与日志可读性,不假装跑了文件校验。
|
|
377
|
+
*/
|
|
378
|
+
function auditRuntimeReceipt(r, opts) {
|
|
379
|
+
const out = { claim: r && r.claim, logPath: r && r.logPath, green: false, logExists: null, mtimeInWindow: null, failSignatures: 0, exitCode: r && typeof r.exitCode === 'number' ? r.exitCode : null, reason: '' }
|
|
380
|
+
if (!r || !r.logPath) { out.reason = '回执缺 logPath'; return out }
|
|
381
|
+
let content = null
|
|
382
|
+
try {
|
|
383
|
+
const abs = opts.cwd ? join(opts.cwd, r.logPath) : r.logPath
|
|
384
|
+
content = readFileSync(abs, 'utf8')
|
|
385
|
+
out.logExists = true
|
|
386
|
+
} catch {
|
|
387
|
+
out.logExists = false
|
|
388
|
+
out.reason = `日志不存在:${r.logPath}`
|
|
389
|
+
return out
|
|
390
|
+
}
|
|
391
|
+
if (opts.verifyStartAt) {
|
|
392
|
+
try {
|
|
393
|
+
const startAt = new Date(opts.verifyStartAt).getTime()
|
|
394
|
+
out.mtimeInWindow = statSync(opts.cwd ? join(opts.cwd, r.logPath) : r.logPath).mtimeMs >= startAt - 60_000
|
|
395
|
+
if (!out.mtimeInWindow) { out.reason = `日志 mtime 早于 verifyStartAt(${r.logPath})`; return out }
|
|
396
|
+
} catch { out.mtimeInWindow = null }
|
|
397
|
+
}
|
|
398
|
+
// 失败签名扫描(日志尾 200 行;行首匹配 + 良性计数行剔除)
|
|
399
|
+
const tail = content.split('\n').slice(-200)
|
|
400
|
+
const SIGNATURE_RE = /^(?:error|exception|traceback|fatal)\b/i
|
|
401
|
+
const BENIGN_RE = /\b0\s+(?:errors?|exceptions?|failures?)\b|no\s+(?:errors?|exceptions?)/i
|
|
402
|
+
for (const line of tail) {
|
|
403
|
+
if (SIGNATURE_RE.test(line.trim()) && !BENIGN_RE.test(line)) out.failSignatures++
|
|
404
|
+
}
|
|
405
|
+
if (out.failSignatures > 0) { out.reason = `日志含 ${out.failSignatures} 行失败签名(行首 error/exception/traceback/fatal,已剔除良性计数行)`; return out }
|
|
406
|
+
if (out.exitCode !== 0) { out.reason = `exitCode=${out.exitCode}(非 0 不可用作在场证据)`; return out }
|
|
407
|
+
out.green = true
|
|
408
|
+
out.reason = '绿回执(log 存在 × mtime 窗口 × 无失败签名 × exit 0)'
|
|
409
|
+
return out
|
|
410
|
+
}
|
package/src/config-cat.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir } from 'os'
|
|
2
|
+
import { sep } from 'path'
|
|
1
3
|
/**
|
|
2
4
|
* config-cat.js — local.yaml 实际值读取的权威路径解析器(`sillyspec config cat` 数据源)。
|
|
3
5
|
*
|
|
@@ -39,9 +41,18 @@ export function resolveLocalYaml(dir, opts = {}) {
|
|
|
39
41
|
candidates.push({ path: key, source })
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
// home 拒绝守卫(e4f2855 resolveSpecDir 同款,2026-09-09 补到 local.yaml 候选链):
|
|
45
|
+
// 起点在 home 子树内(如 Temp 下的测试 fixture)时,祖先链走到 home 层不得命中
|
|
46
|
+
// ~/.sillyspec/local.yaml——那是用户全局配置,不是本项目配置;register-repo 写侧
|
|
47
|
+
// 经 resolveLocalYamlWriteTarget 同链解析,误命中会把项目注册静默写进全局配置
|
|
48
|
+
// (local-register.test.mjs 实证)。
|
|
49
|
+
const home = homedir()
|
|
50
|
+
const originBelowHome = start !== home && start.startsWith(home + sep)
|
|
42
51
|
if (opts.specBase) push(join(opts.specBase, 'local.yaml'), 'spec 根(--spec-dir/平台 pointer/最近 .sillyspec)')
|
|
43
52
|
for (let d = start; ; d = dirname(d)) {
|
|
44
|
-
|
|
53
|
+
if (!(originBelowHome && d === home)) {
|
|
54
|
+
push(join(d, '.sillyspec', 'local.yaml'), 'cwd 祖先链(worktree 内向上命中主仓;home 层受守卫跳过)')
|
|
55
|
+
}
|
|
45
56
|
const parent = dirname(d)
|
|
46
57
|
if (parent === d) break
|
|
47
58
|
}
|
package/src/constants.js
CHANGED
|
@@ -93,7 +93,9 @@ export const AUXILIARY_STAGES = Object.freeze([
|
|
|
93
93
|
// 供 command.js 判断 status/doctor 等查询型辅助阶段执行只读短路(FR-04 / D-005@v2)。
|
|
94
94
|
export const READONLY_AUXILIARY_STAGES = Object.freeze([
|
|
95
95
|
'status',
|
|
96
|
-
|
|
96
|
+
// doctor 移出(2026-09-09-doctor-noai D-001@v1):阶段形态走状态机(noAI 诊断步 +
|
|
97
|
+
// agent 修复步——只读短路使 _cliAction 不可达);顶层 sillyspec doctor 命令的只读
|
|
98
|
+
// 语义由 index.js case 头部拦截保真(--json/--status 先行分流)。
|
|
97
99
|
])
|
|
98
100
|
|
|
99
101
|
// ── IR 严格模式闸门(change: 2026-09-07-ir-hardening,D-001@v1)──
|
package/src/docs-check.js
CHANGED
|
@@ -1122,3 +1122,37 @@ export function runChangeNameAdvisory(opts) {
|
|
|
1122
1122
|
findings.sort((a, b) => a.doc.localeCompare(b.doc) || a.docLine - b.docLine || a.name.localeCompare(b.name))
|
|
1123
1123
|
return { empty: scanned === 0, scannedDocs: scanned, mentions, uniqueNames: seenNames.size, findings, exempted }
|
|
1124
1124
|
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* 行号漂移自动重锚(quick --done 接线面,2026-09-08 用户反馈②):活文档 file:line 硬编码
|
|
1128
|
+
* 随任何源码插入失效,每回手跑 `docs check --fix` 是机械活。本函数 = --fix 主链路的编程化
|
|
1129
|
+
* 封装:对给定文档集跑一次失效收集 → 取分类器判定确定性的条目(fixable 唯一/优选命中,
|
|
1130
|
+
* 与 index.js --fix 同源构造 fixes)→ applyFixes 写回 → 同口径复跑得回执。
|
|
1131
|
+
*
|
|
1132
|
+
* 边界(与 --fix 的既定红线一致):歧义/零命中条目不动(无 --force 逃生口);仅改写
|
|
1133
|
+
* fixable 条目的行号段,文件名与 token 原样。任何异常上抛由调用方 fail-open。
|
|
1134
|
+
*
|
|
1135
|
+
* @param {string} projectRoot 源码仓根
|
|
1136
|
+
* @param {string[]} docs 本次改动文档的相对路径列表(audit 口径:changedFiles 中的 .md)
|
|
1137
|
+
* @param {{ crossRepoRoots?: string[] }} [cfg] docs-check 配置(readDocsCheckConfig 产物)
|
|
1138
|
+
* @returns {{ applied: number, invalidBefore: number, invalidAfter: number, remaining: number }}
|
|
1139
|
+
* applied=0 时 invalidBefore/After 同值(无可自动修条目,调用方维持纯 advisory)
|
|
1140
|
+
*/
|
|
1141
|
+
export function autoReanchorDocRefs(projectRoot, docs, cfg = {}) {
|
|
1142
|
+
// 与 index.js --fix 完全同口径(skip/keywordAssert 透传;undefined 时 runDocsCheck 解构
|
|
1143
|
+
// 默认值兜底:paths 缺省 glob、keywordAssert=true、exempt=true)
|
|
1144
|
+
const scope = { projectRoot, docs, skip: cfg.skip, keywordAssert: cfg.keywordAssert, crossRepoRoots: cfg.crossRepoRoots };
|
|
1145
|
+
const before = runDocsCheck(scope);
|
|
1146
|
+
const fixes = [];
|
|
1147
|
+
for (const inv of before.invalid) {
|
|
1148
|
+
if (inv.fix && inv.fix.fixable === true && Number.isInteger(inv.fix.newLine)) {
|
|
1149
|
+
fixes.push({ doc: inv.doc, docLine: inv.docLine, ref: inv.ref, newRef: inv.ref.replace(/(\d+)(?:-\d+)?$/, String(inv.fix.newLine)) });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (fixes.length === 0) {
|
|
1153
|
+
return { applied: 0, invalidBefore: before.invalid.length, invalidAfter: before.invalid.length, remaining: before.invalid.length };
|
|
1154
|
+
}
|
|
1155
|
+
const applied = applyFixes(projectRoot, fixes);
|
|
1156
|
+
const after = runDocsCheck(scope);
|
|
1157
|
+
return { applied: applied.applied, invalidBefore: before.invalid.length, invalidAfter: after.invalid.length, remaining: after.invalid.length };
|
|
1158
|
+
}
|