sillyspec 3.28.9 → 3.28.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/full.json +1 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/config-schema.js +30 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +56 -17
- package/src/index.js +18 -1
- package/src/knowledge-classify.js +20 -1
- package/src/probe7-anchor-check.js +69 -0
- package/src/quicklog.js +9 -4
- package/src/run/command.js +25 -2
- package/src/run/complete.js +70 -8
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +91 -7
- package/src/run/prompt.js +87 -6
- package/src/run/shared.js +71 -1
- package/src/scope-audit.js +101 -31
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +32 -5
- package/src/sync-noise.js +57 -1
- package/src/sync.js +8 -4
- package/src/task-review.js +75 -26
- package/src/verify-postcheck.js +192 -55
- package/src/verify-probes.js +390 -15
- package/src/worktree-apply.js +148 -5
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
package/src/run/prompt.js
CHANGED
|
@@ -316,6 +316,68 @@ export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, quer
|
|
|
316
316
|
|
|
317
317
|
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
318
318
|
// 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 等效验证先例库(坑 verify-precedent-only-in-prose,2026-09-15 EHS 生产实证:mvn test 被
|
|
322
|
+
* 公司框架 parent pom pluginManagement 硬编码 surefire skip=true、-D 覆盖无效——等效口径
|
|
323
|
+
* dependency:build-classpath+javac+JUnitCore 第二次复用靠 agent 翻旧 verify-result.md 正文
|
|
324
|
+
* 续命;先例只活在散文里 = 换个 agent/换个变更就断档)。结构化登记进 local.yaml
|
|
325
|
+
* verify_precedents 段,verify prompt 时点注入提示(与 test_strategy 预检提示同挂点)。
|
|
326
|
+
*
|
|
327
|
+
* 对象数组形态(行扫描,同 extractKnownFailures 免 YAML 依赖口径——注释/空行/块界容错):
|
|
328
|
+
* verify_precedents:
|
|
329
|
+
* - id: mvn-test-surefire-skip
|
|
330
|
+
* standard_command: mvn test
|
|
331
|
+
* reason: <标准命令为何不可用>
|
|
332
|
+
* equivalent: <已验证的等效口径>
|
|
333
|
+
* established_by: <确立该口径的变更名>
|
|
334
|
+
* notes: <可选补充>
|
|
335
|
+
* 残项(standard_command 与 equivalent 双缺)过滤——登记不完整的先例没有可执行口径。
|
|
336
|
+
* @param {string|null} yamlText local.yaml 全文
|
|
337
|
+
* @returns {Array<object>} 先例对象数组
|
|
338
|
+
*/
|
|
339
|
+
export function parseVerifyPrecedents(yamlText) {
|
|
340
|
+
const lines = String(yamlText || '').split(/\r?\n/)
|
|
341
|
+
const headIdx = lines.findIndex(l => /^verify_precedents:\s*(?:#.*)?$/.test(l))
|
|
342
|
+
if (headIdx < 0) return []
|
|
343
|
+
const items = []
|
|
344
|
+
let cur = null
|
|
345
|
+
const clean = (v) => String(v || '').replace(/\s+#.*$/, '').trim()
|
|
346
|
+
for (let i = headIdx + 1; i < lines.length; i++) {
|
|
347
|
+
const l = lines[i]
|
|
348
|
+
if (/^\S/.test(l)) break // 下一个顶层键 → 块结束
|
|
349
|
+
if (!l.trim() || /^\s*#/.test(l)) continue
|
|
350
|
+
const item = l.match(/^\s*-\s+([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
351
|
+
if (item) {
|
|
352
|
+
cur = {}
|
|
353
|
+
items.push(cur)
|
|
354
|
+
cur[item[1]] = clean(item[2])
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
const kv = l.match(/^\s*([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
358
|
+
if (kv && cur) cur[kv[1]] = clean(kv[2])
|
|
359
|
+
}
|
|
360
|
+
return items.filter(x => x && (x.standard_command || x.equivalent))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 先例提示渲染(verify prompt 注入用;空输入 → '',调用方零输出)。
|
|
365
|
+
* 行动指引刻意指向「把等效命令配进 commands.test 照常全量实测对账」而非 test_strategy: skip
|
|
366
|
+
* 放行——EHS 实证里被环境阻断 deferred 的集成测试正是 P1 缺陷藏身处,能实测就实测。
|
|
367
|
+
* @param {Array<object>|null} precedents
|
|
368
|
+
* @returns {string}
|
|
369
|
+
*/
|
|
370
|
+
export function renderVerifyPrecedentHint(precedents) {
|
|
371
|
+
if (!Array.isArray(precedents) || precedents.length === 0) return ''
|
|
372
|
+
const L = [`【验证先例提示】本仓已登记 ${precedents.length} 条等效验证先例(标准命令不可用时的既有替代口径):`]
|
|
373
|
+
for (const p of precedents.slice(0, 5)) {
|
|
374
|
+
L.push(`- ${p.id || '(无 id)'}:\`${p.standard_command || '?'}\` 不可用(${p.reason || '原因未注'})→ 等效口径 \`${p.equivalent || '?'}\`(确立于 ${p.established_by || '?'}${p.notes ? `;${p.notes}` : ''})`)
|
|
375
|
+
}
|
|
376
|
+
if (precedents.length > 5) L.push(`- …另有 ${precedents.length - 5} 条未展开(读 local.yaml verify_precedents 段看全量)`)
|
|
377
|
+
L.push('适用时把等效命令配进 local.yaml commands.test(或 modules.<m>.test)后照常全量实测对账,而不是只靠 test_strategy: skip 放行;先例不适用(环境已变/口径失效)可忽略并在 verify-result 注记。')
|
|
378
|
+
return L.join('\n')
|
|
379
|
+
}
|
|
380
|
+
|
|
319
381
|
/**
|
|
320
382
|
* 提示词路径根的单一解析:specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)。
|
|
321
383
|
* 治 execute review.json 提示路径写 worktree 副本、--done 校验读主仓的分裂(坑 execute-prompt-spec-drift):
|
|
@@ -1129,13 +1191,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1129
1191
|
// 推荐组合、无从否决——故在 prompt 时点经 {EVIDENCE_AUTO_RECOMMENDATION} 把 task-11
|
|
1130
1192
|
// resolveTestStrategy 的 evidence_auto_recommendation.summary 渲染进 step prompt(含推荐
|
|
1131
1193
|
// 理由、降级注记与「可在 verify-result.md 否决并改跑全量」路径),供用户否决(FR-11)。
|
|
1132
|
-
// 仅 test_strategy=evidence-auto 且 recommendation
|
|
1133
|
-
//
|
|
1194
|
+
// 仅 test_strategy=evidence-auto 且 recommendation 非空时渲染推荐;full/module/skip/已配置
|
|
1195
|
+
// 替换为空串零输出;**未配置**且 commands.test 已配时渲染 test_strategy 预检提示(2026-09-15
|
|
1196
|
+
// 复盘:环境硬伤的全量实测打回返工,提前在 prompt 时点给配置指引)。fail-soft:读取/解析异常降级单行说明不抛,
|
|
1134
1197
|
// 绝不阻断 verify prompt 输出(对齐上方 {WORKTREE_BASELINE_INFO} 注入先例)。
|
|
1135
1198
|
if (stageName === 'verify' && promptText.includes('{EVIDENCE_AUTO_RECOMMENDATION}')) {
|
|
1136
1199
|
let eaInjected = ''
|
|
1137
1200
|
try {
|
|
1138
|
-
const { resolveTestStrategy } = await import('../verify-postcheck.js')
|
|
1201
|
+
const { resolveTestStrategy, extractTestCommand, extractTestStrategy } = await import('../verify-postcheck.js')
|
|
1139
1202
|
const eaSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1140
1203
|
// 信任边界:local.yaml 只读 specBase(平台 specRoot / 漂移锚定主仓 / cwd 本地),与
|
|
1141
1204
|
// runVerifyTestCheck 的 specBase 来源同口径,不读 agent 可写的 worktree 副本。
|
|
@@ -1144,7 +1207,20 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1144
1207
|
const eaChangeDir = changeName ? join(eaSpecBase, 'changes', changeName) : null
|
|
1145
1208
|
const resolution = resolveTestStrategy({ yamlText: eaYamlText, changeDir: eaChangeDir })
|
|
1146
1209
|
const rec = resolution.evidence_auto_recommendation
|
|
1147
|
-
if (rec && rec.summary)
|
|
1210
|
+
if (rec && rec.summary) {
|
|
1211
|
+
eaInjected = rec.summary
|
|
1212
|
+
} else if (eaYamlText && extractTestCommand(eaYamlText) && !extractTestStrategy(eaYamlText)) {
|
|
1213
|
+
// test_strategy 预检提示(2026-09-15 复盘教训「test_strategy 该在进 verify 前配好,
|
|
1214
|
+
// 而不是被 verify-test gate 打回后才补」):已配 commands.test 但顶层 test_strategy
|
|
1215
|
+
// 未设(缺省=全量实测对账),prompt 时点先给配置指引——环境注定跑不了全量时省一轮
|
|
1216
|
+
// 「--done 实测打回 → 补配置 → 重跑」返工
|
|
1217
|
+
eaInjected = '【test_strategy 预检提示】local.yaml 已配置 commands.test 但未设顶层 test_strategy(缺省=全量实测对账)。若该命令在当前环境无法真实执行(框架/环境硬伤,如测试框架硬编码 skip、依赖外部服务),先在 local.yaml 显式配 `test_strategy: skip`(或 `module` + modules 映射收窄)并注释理由再继续——避免 verify --done 实测对账打回后返工。'
|
|
1218
|
+
}
|
|
1219
|
+
// 等效验证先例注入(verify-precedent-only-in-prose,2026-09-15 EHS 实证):先例与
|
|
1220
|
+
// 预检提示同挂点追加——标准命令不可用但已有验证过的等效口径时,指引优先配进
|
|
1221
|
+
// commands.test 实测对账而非 skip 放行。
|
|
1222
|
+
const _precHint = renderVerifyPrecedentHint(parseVerifyPrecedents(eaYamlText))
|
|
1223
|
+
if (_precHint) eaInjected = eaInjected ? `${eaInjected}\n\n${_precHint}` : _precHint
|
|
1148
1224
|
} catch (e) {
|
|
1149
1225
|
eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
|
|
1150
1226
|
}
|
|
@@ -1352,10 +1428,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1352
1428
|
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
1353
1429
|
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
1354
1430
|
const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
|
|
1355
|
-
// auto driver(2026-09-08-auto-driver D-002/P1
|
|
1431
|
+
// auto driver(2026-09-08-auto-driver D-002/P1-Ⅲ):auto 模式下命令一律 run auto 形态——
|
|
1356
1432
|
// 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
|
|
1357
1433
|
const cmdStage = autoMeta ? 'auto' : stageName
|
|
1358
|
-
|
|
1434
|
+
// 意图断言(坑 execute-concurrent-done-skips-next-wave 终解):execute 的 Wave 步骤是动态
|
|
1435
|
+
// 步骤表,并发 --done 会落到错误 Wave 且用旧摘要静默完成——完成命令带 --step "<本步名>"
|
|
1436
|
+
//(CLI 校验一致才放行),照抄命令即得并发防护。仅 execute 注入(quick/静态阶段测试断言
|
|
1437
|
+
// 提示格式,且静态步骤名稳定无漂移风险)。
|
|
1438
|
+
const stepAssertFlag = (stageName === 'execute' && step?.name) ? ` --step "${step.name}"` : ''
|
|
1439
|
+
const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${stepAssertFlag}${changeFlag} --output "你的摘要"`
|
|
1359
1440
|
if (requiresWait) {
|
|
1360
1441
|
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
1361
1442
|
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
package/src/run/shared.js
CHANGED
|
@@ -635,6 +635,71 @@ export function checkPlatformManaged(cwd) {
|
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
/**
|
|
639
|
+
* 平台模式产物落点指针(坑 platform-docs-dual-location,2026-09-15 EHS 生产实证:
|
|
640
|
+
* 变更 9 文档落 daemon specs 目录,工作树 .sillyspec/ 只剩旧变更残留、worktree 无
|
|
641
|
+
* .sillyspec——人类在工作树里找变更文档扑空,只有平台 changes files API 能读,双位置
|
|
642
|
+
* 造成「文件去哪了」困惑)。阶段 --done 完成时在工作树 .sillyspec/ 落一份人类可读指针
|
|
643
|
+
* PLATFORM-DOCS-POINTER.md:specRoot/runtimeRoot 物理路径 + workspaceId + 本变更文档
|
|
644
|
+
* 清单(.md 文件名+mtime + tasks/ 计数)+ 获取方式提示。
|
|
645
|
+
*
|
|
646
|
+
* 口径与 writePlatformPointer 三写同款豁免:本地模式(无 specRoot/runtimeRoot)零行为;
|
|
647
|
+
* 自指回环(repo-native junction 指回 cwd/.sillyspec)不写;temp 残留形态(specRoot 在
|
|
648
|
+
* 系统 temp 而项目不在)不写。fail-soft:任何写失败只 warn 不抛(对齐 user-inputs.md
|
|
649
|
+
* 追加先例)——指针是便利件,绝不阻断阶段完成。
|
|
650
|
+
*
|
|
651
|
+
* @param {string} cwd 项目根(指针落 cwd/.sillyspec/)
|
|
652
|
+
* @param {string|null} changeName 变更名(可空——无变更上下文时只写根路径头部)
|
|
653
|
+
* @param {{specRoot?: string|null, runtimeRoot?: string|null, workspaceId?: string|null}} [platformOpts]
|
|
654
|
+
* @param {string|null} [stageName] 触发阶段(展示用)
|
|
655
|
+
* @returns {{written: boolean, reason?: string, path?: string, error?: string}}
|
|
656
|
+
*/
|
|
657
|
+
export function writePlatformDocsPointer(cwd, changeName, platformOpts = {}, stageName = null) {
|
|
658
|
+
try {
|
|
659
|
+
const specRoot = platformOpts?.specRoot || null
|
|
660
|
+
const runtimeRoot = platformOpts?.runtimeRoot || null
|
|
661
|
+
if (!specRoot && !runtimeRoot) return { written: false, reason: 'local-mode' }
|
|
662
|
+
if (specRoot && isSelfReferentialSpecRoot(cwd, specRoot)) return { written: false, reason: 'self-referential' }
|
|
663
|
+
if (isTempResidueSpecRoot(cwd, specRoot)) return { written: false, reason: 'temp-residue' }
|
|
664
|
+
const localSpecDir = join(cwd, '.sillyspec')
|
|
665
|
+
mkdirSync(localSpecDir, { recursive: true })
|
|
666
|
+
const ts = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
667
|
+
const lines = [
|
|
668
|
+
'<!-- sillyspec 平台模式产物落点指针(CLI 在阶段 --done 完成时自动维护;本地模式不生成本文件) -->',
|
|
669
|
+
'# 变更文档实际落点(工作树 .sillyspec/ 不是本仓权威数据面)',
|
|
670
|
+
'',
|
|
671
|
+
`- 平台 spec 根(proposal/design/verify-result 等变更文档): ${specRoot || '(未指定)'}`,
|
|
672
|
+
`- 运行时根(worktrees/contract-artifacts/verify-logs): ${runtimeRoot || '(未指定)'}`,
|
|
673
|
+
platformOpts?.workspaceId ? `- 工作区 ID: ${platformOpts.workspaceId}` : null,
|
|
674
|
+
`- 最近更新: ${stageName || '?'} 阶段完成 @ ${ts}${changeName ? `(change: ${changeName})` : ''}`,
|
|
675
|
+
].filter(Boolean)
|
|
676
|
+
const changeDir = (specRoot && changeName) ? join(specRoot, 'changes', changeName) : null
|
|
677
|
+
if (changeDir && existsSync(changeDir)) {
|
|
678
|
+
const docs = []
|
|
679
|
+
for (const f of readdirSync(changeDir)) {
|
|
680
|
+
if (!f.endsWith('.md')) continue
|
|
681
|
+
try {
|
|
682
|
+
const mtime = statSync(join(changeDir, f)).mtime.toLocaleString('zh-CN', { hour12: false })
|
|
683
|
+
docs.push(`- ${f}(mtime ${mtime})`)
|
|
684
|
+
} catch { docs.push(`- ${f}`) }
|
|
685
|
+
}
|
|
686
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
687
|
+
if (existsSync(tasksDir)) {
|
|
688
|
+
const cards = readdirSync(tasksDir).filter(f => f.endsWith('.md'))
|
|
689
|
+
if (cards.length > 0) docs.push(`- tasks/(${cards.length} 张任务卡)`)
|
|
690
|
+
}
|
|
691
|
+
if (docs.length > 0) lines.push('', `## 本变更文档(${changeName})`, ...docs)
|
|
692
|
+
}
|
|
693
|
+
lines.push('', '获取方式:直接读上行物理路径,或经平台 changes files API(/changes/{id}/files/raw?path=...)逐文件下载。')
|
|
694
|
+
const pointerPath = join(localSpecDir, 'PLATFORM-DOCS-POINTER.md')
|
|
695
|
+
writeFileSync(pointerPath, lines.join('\n') + '\n')
|
|
696
|
+
return { written: true, path: pointerPath }
|
|
697
|
+
} catch (e) {
|
|
698
|
+
console.warn('⚠️ 平台产物落点指针写入失败(不阻断):', e && e.message ? e.message : e)
|
|
699
|
+
return { written: false, reason: 'error', error: e && e.message }
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
638
703
|
/**
|
|
639
704
|
* 自指 specRoot 判定(repo-native junction 回环检测,变更 2026-08-23-repo-native-spec-backfill)。
|
|
640
705
|
*
|
|
@@ -1035,7 +1100,12 @@ export function isQuickMetadata(p, linkedChanges = []) {
|
|
|
1035
1100
|
const file = normalizeGitPath(p)
|
|
1036
1101
|
if (file.startsWith('.sillyspec/quicklog/')
|
|
1037
1102
|
|| file.startsWith('.sillyspec/.runtime/')
|
|
1038
|
-
|
|
1103
|
+
// 知识库整体白名单(2026-09-15 用户实测反馈:登记知识条目是 quick 收尾常规动作,此前
|
|
1104
|
+
// INDEX/known-issues/patterns/conventions/decisions 非元数据 → 危险门 blocked 只能 --force-baseline,
|
|
1105
|
+
// 且 perFileNotes 强制 --file-notes 覆盖):knowledge/ 是 CLI 自管面(classify append-only +
|
|
1106
|
+
// validate 守门),与 quicklog 同级。可追溯性靠 quicklog 结构化条目 + git diff,不靠文件行。
|
|
1107
|
+
// 前缀带尾斜杠:knowledge-base/ 等相近目录不命中。
|
|
1108
|
+
|| file.startsWith('.sillyspec/knowledge/')
|
|
1039
1109
|
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
|
|
1040
1110
|
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
|
|
1041
1111
|
if (file.startsWith('.sillyspec/changes/')) {
|
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
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync, readFileSync } from 'fs'
|
|
16
|
-
import { join } from 'path'
|
|
16
|
+
import { join, isAbsolute } from 'path'
|
|
17
17
|
|
|
18
18
|
// 提取 `npm|pnpm|yarn run <script>` 命令。
|
|
19
19
|
// 对齐 scan-postcheck:136 的 `/npm run (\S+)/g`,扩展包管理器 + 容忍多空白。
|
|
@@ -50,7 +50,12 @@ function readScripts(pkgPath) {
|
|
|
50
50
|
* 为单条命令计算候选 package.json 路径列表。
|
|
51
51
|
*
|
|
52
52
|
* 规则(blueprint task-02 + design §5 H2):
|
|
53
|
-
* - 有 `cd <subdir> &&` 前缀 → 仅查 `<
|
|
53
|
+
* - 有 `cd <subdir> &&` 前缀 → 仅查 `<subdir>/package.json`(cd 锁定子目录,不回退根)。
|
|
54
|
+
* subdir 为绝对路径(win 盘符 / posix 根)时不再拼 projectRoot——坑
|
|
55
|
+
* verify-cmd-absolute-cd-path(2026-09-15 wp 会话实证):跨仓 task 卡 verify 写
|
|
56
|
+
* `cd E:/PZwangge/sub-grid-security && npm run lint`,join(主仓根, 'E:/...') 拼出
|
|
57
|
+
* 病态路径 → package.json 恒 missing → 真实命令被误判死命令,agent 只能改写成
|
|
58
|
+
* ../ 相对路径绕过
|
|
54
59
|
* - 无前缀 + modules 块提供 → 查每个 module.path 的子包 + 根 package.json(多候选任一命中即视为存在)
|
|
55
60
|
* - 无前缀 + 无 modules → 仅查 `<projectRoot>/package.json`
|
|
56
61
|
*
|
|
@@ -65,7 +70,8 @@ function resolveCandidates(text, matchIndex, projectRoot, modules) {
|
|
|
65
70
|
const cdMatch = before.match(CD_PREFIX_RE)
|
|
66
71
|
if (cdMatch) {
|
|
67
72
|
const subdir = cdMatch[1].replace(/^["']|["']$/g, '')
|
|
68
|
-
|
|
73
|
+
const base = isAbsolute(subdir) ? subdir : join(projectRoot, subdir)
|
|
74
|
+
return [join(base, 'package.json')]
|
|
69
75
|
}
|
|
70
76
|
if (modules && typeof modules === 'object') {
|
|
71
77
|
const candidates = []
|
package/src/stages/knowledge.js
CHANGED
|
@@ -76,10 +76,15 @@ function extractTitle(content) {
|
|
|
76
76
|
|
|
77
77
|
export async function cmdSearch(dir, args, opts = {}) {
|
|
78
78
|
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
79
|
+
// flag-as-value 防护(坑 knowledge-flag-as-value):值以 '--' 开头视为漏传(只读命令,
|
|
80
|
+
// 按缺参处理落入既有 required 校验/默认值,不静默拿 flag 字面量当查询词)
|
|
79
81
|
const queryIdx = args.indexOf('--query')
|
|
80
|
-
const
|
|
82
|
+
const queryRaw = queryIdx >= 0 ? args[queryIdx + 1] : ''
|
|
83
|
+
const query = queryRaw && !queryRaw.startsWith('--') ? queryRaw : ''
|
|
81
84
|
const limitIdx = args.indexOf('--limit')
|
|
82
|
-
const
|
|
85
|
+
const limitRaw = limitIdx >= 0 ? args[limitIdx + 1] : ''
|
|
86
|
+
const parsedLimit = limitRaw && !limitRaw.startsWith('--') ? parseInt(limitRaw, 10) : NaN
|
|
87
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? parsedLimit : 10
|
|
83
88
|
|
|
84
89
|
if (!query) {
|
|
85
90
|
output(false, {}, '--query is required')
|
|
@@ -130,8 +135,10 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
130
135
|
|
|
131
136
|
export async function cmdInspect(dir, args, opts = {}) {
|
|
132
137
|
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
138
|
+
// flag-as-value 防护同 cmdSearch(坑 knowledge-flag-as-value)
|
|
133
139
|
const idIdx = args.indexOf('--id')
|
|
134
|
-
const
|
|
140
|
+
const idRaw = idIdx >= 0 ? args[idIdx + 1] : ''
|
|
141
|
+
const id = idRaw && !idRaw.startsWith('--') ? idRaw : ''
|
|
135
142
|
|
|
136
143
|
if (!id) {
|
|
137
144
|
output(false, {}, '--id is required')
|
|
@@ -215,6 +222,20 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
215
222
|
})
|
|
216
223
|
}
|
|
217
224
|
}
|
|
225
|
+
|
|
226
|
+
// 2b. flag 形态关键词(坑 knowledge-flag-as-value):classify 曾把 flag-as-value 的
|
|
227
|
+
// 字面量 '--file' 写进路由行——目标文件存在、行格式合法,既有检查放行,此处补侦测
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const flagKw = entry.keywords.find(kw => kw.startsWith('--'))
|
|
230
|
+
if (flagKw) {
|
|
231
|
+
warnings.push({
|
|
232
|
+
code: 'flag_like_keyword',
|
|
233
|
+
path: 'knowledge/INDEX.md',
|
|
234
|
+
display: entry.display,
|
|
235
|
+
keyword: flagKw,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
// 3. 扫描所有 .md 文件,检查是否在 INDEX 中注册
|