sillyspec 3.32.9 → 3.33.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-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +58 -0
- package/.husky/pre-push +7 -6
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/ceremony-tier.js +2 -1
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/decision-distill.js +106 -24
- package/src/decision-redomain.js +148 -0
- package/src/decision-renumber.js +157 -0
- package/src/design-facts.js +3 -3
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +6 -5
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-parity.js +28 -4
- package/src/flow.js +76 -13
- package/src/forensics.js +236 -0
- package/src/fr-index.js +17 -2
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +41 -6
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +95 -18
- 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/module-impact.js +2 -5
- package/src/modules.js +31 -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 +100 -103
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +43 -28
- package/src/run/stage.js +1 -1
- package/src/run/verify-quality-scan.js +10 -1
- package/src/sentinel-assertions.js +43 -1
- package/src/spec-sync.js +81 -2
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +46 -12
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +76 -21
- package/src/stages/knowledge.js +56 -5
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/sync.js +28 -2
- package/src/task-tick.js +12 -0
- package/src/test-bindings.js +32 -3
- package/src/timeline.js +60 -19
- package/src/ui-visual.js +119 -4
- package/src/verify-postcheck.js +166 -18
- package/src/verify-probes.js +61 -18
- package/src/worktree-apply.js +26 -2
- package/templates/agents-instruction.md +1 -1
package/src/knowledge-graph.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { parseKnowledgeIndex, parseDecisionEntries, anchorFilePaths } from './knowledge-match.js'
|
|
24
|
-
import { parseModuleMapSimple } from './modules.js'
|
|
24
|
+
import { modulePathMatches, parseModuleMapSimple } from './modules.js'
|
|
25
25
|
|
|
26
26
|
// ── 本体常量(design.md 总体方案 §1 的代码化) ──
|
|
27
27
|
|
|
@@ -103,6 +103,7 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
103
103
|
|
|
104
104
|
// ── ② 决策库(parseDecisionEntries:去重键 file+id+change 跨变更同号不折叠) ──
|
|
105
105
|
const decisionHits = existsSync(join(knowledgeDir, 'INDEX.md')) ? parseDecisionEntries(knowledgeDir) : []
|
|
106
|
+
const supersedesRefs = []
|
|
106
107
|
for (const h of decisionHits) {
|
|
107
108
|
const id = `decision:${h.file}#${h.id}${h.change ? `@${h.change}` : ''}`
|
|
108
109
|
addNode(id, 'decision', `${h.id} ${h.title || ''}`.trim(), {
|
|
@@ -123,17 +124,45 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
123
124
|
else addNode(h.change, 'change', h.change)
|
|
124
125
|
addEdge(id, h.change, 'from-change')
|
|
125
126
|
}
|
|
126
|
-
// supersedes:同域 D-id 解析(「supersedes:无(…)」等非 id
|
|
127
|
-
|
|
127
|
+
// supersedes:同域 D-id 解析(「supersedes:无(…)」等非 id 值被正则白名单滤掉)。目标必须同变更
|
|
128
|
+
// (「变更:」行一致,含双方均无该行的 legacy 同源)——D 号是变更内局部序号,跨变更同号互不相干;
|
|
129
|
+
// v2 蒸馏落地时同变更 v1 段已被就地替换,此时无目标即不建边(2026-10-14-supersedes-mislink-and-change-display:
|
|
130
|
+
// 旧「再退同文件首个」回退几乎必然连到他变更同号条目,实测 33 条误连,回退已除)。epoch 形态段
|
|
131
|
+
// (2026-10-14-decision-global-id-epoch)id 为全局号——局部号引用经「原编号」字段匹配(originId)。
|
|
132
|
+
const supIds = String(h.supersedesText || '').split(/[,,、\s]+/).filter((s) => /^D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+$/.test(s))
|
|
128
133
|
for (const sid of supIds) {
|
|
129
|
-
const target = decisionHits.find((x) => x.id === sid && x.file === h.file && (x.change || '') === (h.change || ''))
|
|
130
|
-
|| decisionHits.find((x) => x.id === sid && x.file === h.file)
|
|
134
|
+
const target = decisionHits.find((x) => (x.id === sid || x.originId === sid) && x.file === h.file && (x.change || '') === (h.change || ''))
|
|
131
135
|
if (target) {
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
supersedesRefs.push([id, `decision:${target.file}#${target.id}${target.change ? `@${target.change}` : ''}`])
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
// 幽灵版本节点(2026-10-15-supersedes-ghost-nodes):同号版本取代是蒸馏就地替换(旧段被删,
|
|
140
|
+
// 跨文件同谓词同删),自身旧版本引用的目标从不在场——真实库实证 39 条引用 0 条可解析、
|
|
141
|
+
// 703 决策节点全图仅 1 条 supersedes 边(边体系闲置根因)。合成口径**只限自身版本链**:
|
|
142
|
+
// sid 基号 === 本条目 id 基号(legacy 同号自链)或 === originId 基号(epoch 原编号自链,
|
|
143
|
+
// 迁移后真实库 36/36 全为此形态);跨号/跨变更缺失引用不合成(2026-10-14-supersedes-mislink
|
|
144
|
+
// 的跨变更误连防护不因幽灵形态复活:为他号凭空铸造节点=误连换皮)。ghost 只带 supersedes
|
|
145
|
+
// 入边(非孤儿)、不参与 anchors/dangling 面(supersedes 不在悬空检测边型集),impact 闭包
|
|
146
|
+
// 经传递边自然延伸。
|
|
147
|
+
const ghostBase = (s) => (/^D-((?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+)@v\d+$/.exec(String(s || '')) || [])[1]
|
|
148
|
+
const selfBase = ghostBase(h.id)
|
|
149
|
+
const originBase = h.originId ? ghostBase(h.originId) : null
|
|
150
|
+
const sidBase = ghostBase(sid)
|
|
151
|
+
if ((selfBase && sidBase === selfBase) || (originBase && sidBase === originBase)) {
|
|
152
|
+
const gid = `decision:${h.file}#${sid}${h.change ? `@${h.change}` : ''}`
|
|
153
|
+
if (!nodes.has(gid)) {
|
|
154
|
+
addNode(gid, 'decision', `${sid}(已被 ${h.id} 取代)`, {
|
|
155
|
+
ghost: true, status: 'superseded', supersededBy: h.id,
|
|
156
|
+
domain: String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, ''),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
supersedesRefs.push([id, gid])
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
}
|
|
163
|
+
// 目标节点可能晚于引用者定义(同文件行序)——addEdge 要求两端节点在场,引用者先行遍历会静默丢边;
|
|
164
|
+
// 边统一在节点面建齐后补挂(2026-10-14-supersedes-mislink-and-change-display 测试实证)
|
|
165
|
+
for (const [s, t] of supersedesRefs) addEdge(s, t, 'supersedes')
|
|
137
166
|
|
|
138
167
|
// ── ③ FR 索引(fr/<域>.md:变更/状态/superseded_by/取代链/测试绑定子块) ──
|
|
139
168
|
const frDir = join(knowledgeDir, 'fr')
|
|
@@ -244,9 +273,8 @@ export function buildKnowledgeGraph(specRoot) {
|
|
|
244
273
|
const moduleOf = (p) => {
|
|
245
274
|
const v = posix(p)
|
|
246
275
|
for (const [mid, mp] of modulePathIndex) {
|
|
247
|
-
// 模块 paths
|
|
248
|
-
|
|
249
|
-
if (v === stem || v.startsWith(stem + '/')) return mid
|
|
276
|
+
// 模块 paths 目录/glob 语义——modulePathMatches 统一入口(**/*.tsx 等扫描卡形态可命中)
|
|
277
|
+
if (modulePathMatches(v, mp)) return mid
|
|
250
278
|
}
|
|
251
279
|
return null
|
|
252
280
|
}
|
|
@@ -542,6 +570,27 @@ export function graphChangelogDanglings(graph, { existsFn = existsSync } = {}) {
|
|
|
542
570
|
return out
|
|
543
571
|
}
|
|
544
572
|
|
|
573
|
+
/** 登记仓前缀(graph 惰性缓存):<specRoot>/projects/*.yaml 的 name: 字段——本 spec 树管理
|
|
574
|
+
* 多仓时(register-repo 机制),跨仓交付路径(backend/**、frontend/**…)按前缀归到登记仓名下,
|
|
575
|
+
* 不再落 _unmapped 大簇(2026-10-15-graph-registry-domain-split:真图 2183 未挂模块文件节点
|
|
576
|
+
* 实测 1220 为跨仓前缀、421 为本仓 test 面)。无 projects 目录/解析失败 = 空集(fail-soft)。 */
|
|
577
|
+
function registryProjectPrefixes(graph) {
|
|
578
|
+
if (graph._projPrefixes) return graph._projPrefixes
|
|
579
|
+
const out = []
|
|
580
|
+
try {
|
|
581
|
+
const dir = join(String(graph.root || ''), 'projects')
|
|
582
|
+
if (existsSync(dir)) {
|
|
583
|
+
for (const f of readdirSync(dir)) {
|
|
584
|
+
if (!f.endsWith('.yaml')) continue
|
|
585
|
+
const m = /^name:\s*([\w-]+)\s*$/m.exec(readFileSync(join(dir, f), 'utf8'))
|
|
586
|
+
if (m) out.push(m[1])
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} catch { /* projects 缺席/不可读 = 无登记前缀 */ }
|
|
590
|
+
graph._projPrefixes = out
|
|
591
|
+
return out
|
|
592
|
+
}
|
|
593
|
+
|
|
545
594
|
/** 节点 → 簇域(lite 总览聚类键 type×domain;缺省占位防散簇)。 */
|
|
546
595
|
function graphNodeDomain(graph, n) {
|
|
547
596
|
switch (n.type) {
|
|
@@ -550,7 +599,20 @@ function graphNodeDomain(graph, n) {
|
|
|
550
599
|
case 'file': {
|
|
551
600
|
// module-files 精确挂接反查(多挂取最长模块 id——窄域优先)
|
|
552
601
|
const mods = graph.edges.filter((e) => e.type === 'module-files' && e.t === n.id).map((e) => e.s.replace(/^module:/, ''))
|
|
553
|
-
return mods.sort((a, b) => b.length - a.length)[0]
|
|
602
|
+
if (mods.length > 0) return mods.sort((a, b) => b.length - a.length)[0]
|
|
603
|
+
// 登记仓前缀归属(2026-10-15-graph-registry-domain-split):跨仓交付路径按 projects 登记
|
|
604
|
+
// 名归簇(最长前缀优先、大小写不敏感——登记名 SillyHub vs 路径 sillyhub/ 实证);本仓
|
|
605
|
+
// 测试面归 _tests——残余 _unmapped 只剩真无归属面(module-map 覆盖缺口,可行动)
|
|
606
|
+
const id = String(n.id)
|
|
607
|
+
const idLower = id.toLowerCase()
|
|
608
|
+
let best = ''
|
|
609
|
+
for (const p of registryProjectPrefixes(graph)) {
|
|
610
|
+
const pl = p.toLowerCase()
|
|
611
|
+
if (idLower.startsWith(`${pl}/`) && pl.length > best.length) best = p
|
|
612
|
+
}
|
|
613
|
+
if (best) return best
|
|
614
|
+
if (/^(test|tests)\//.test(id)) return '_tests'
|
|
615
|
+
return '_unmapped'
|
|
554
616
|
}
|
|
555
617
|
case 'module':
|
|
556
618
|
return n.id.replace(/^module:/, '')
|
|
@@ -573,12 +635,18 @@ export function graphSummary(graph, { existsFn = existsSync, clustersLimit = 0 }
|
|
|
573
635
|
const orphans = graphOrphans(graph).length
|
|
574
636
|
const dangling = graphDangling(graph, { existsFn })
|
|
575
637
|
const danglingRefs = dangling.length
|
|
638
|
+
// 跨仓口径分流(2026-10-15-graph-dangling-crossrepo-split):知识库集中管理仓、代码分仓存放
|
|
639
|
+
// 是本体系常态拓扑——跨仓强边锚点(顶级目录本仓不存在)永不可在本仓解析却全数计入 dangling_refs,
|
|
640
|
+
// 健康数字长期失真(本仓实证 2537 中 2472 为跨仓注记)。主计数与守恒口径不动(消费方零回归),
|
|
641
|
+
// 增加分流字段:breakdown.cross_repo=跨仓注记数、dangling_refs_local=本仓可行动悬空数。
|
|
642
|
+
const danglingCrossRepo = dangling.filter((d) => d.crossRepo).length
|
|
576
643
|
// 分强度计数(附加字段——dangling_refs 是两类之和,消费方对账 doctor 时按 breakdown 拆)
|
|
577
644
|
const danglingBreakdown = dangling.reduce((a, d) => {
|
|
578
645
|
if (EDGE_STRENGTH[d.edge.type] === 'strong') a.strong_anchors++
|
|
579
646
|
else if (EDGE_STRENGTH[d.edge.type] === 'medium') a.medium_doc_refs++
|
|
580
647
|
return a
|
|
581
648
|
}, { strong_anchors: 0, medium_doc_refs: 0 })
|
|
649
|
+
danglingBreakdown.cross_repo = danglingCrossRepo
|
|
582
650
|
const moduleDocGaps = graphModuleDocGaps(graph).length
|
|
583
651
|
const changelogDanglings = graphChangelogDanglings(graph, { existsFn }).length
|
|
584
652
|
// clusters:type×domain 聚簇,representatives 度数 top-5(同度按 id 字典序稳序)
|
|
@@ -612,7 +680,10 @@ export function graphSummary(graph, { existsFn = existsSync, clustersLimit = 0 }
|
|
|
612
680
|
module_doc_gaps: moduleDocGaps,
|
|
613
681
|
changelog_danglings: changelogDanglings,
|
|
614
682
|
dangling_refs: danglingRefs,
|
|
615
|
-
// dangling_refs
|
|
683
|
+
// 本仓可行动悬空(附加字段,跨仓口径分流):dangling_refs − 跨仓注记——治理动作只看这个数
|
|
684
|
+
dangling_refs_local: danglingRefs - danglingCrossRepo,
|
|
685
|
+
// dangling_refs 构成(附加字段,向后兼容):doctor graph-dangling-anchor(强)+ graph-doc-dangling-ref(中)之和;
|
|
686
|
+
// cross_repo=跨仓注记数(顶级目录本仓不存在——目标活在独立仓,单列不对账)
|
|
616
687
|
dangling_refs_breakdown: danglingBreakdown,
|
|
617
688
|
clusters,
|
|
618
689
|
}
|
|
@@ -791,7 +862,7 @@ export function impactFromDecisionsMd(graph, text) {
|
|
|
791
862
|
// 输出沿知识命令族全 JSON 约定({ok,...data});人类可读摘要行内嵌 summary 字段。
|
|
792
863
|
// ═══════════════════════════════════════════════════════════════
|
|
793
864
|
|
|
794
|
-
const GRAPH_USAGE = '用法:sillyspec knowledge graph <summary|nodes|dump|neighbors|path|impact|orphans|dangling> [锚点...] [--search <模糊>] [--limit N] [--edges <边型>] [--depth N];dump 需 --layout'
|
|
865
|
+
const GRAPH_USAGE = '用法:sillyspec knowledge graph <summary|nodes|dump|neighbors|path|impact|orphans|dangling> [锚点...] [--search <模糊>] [--limit N] [--edges <边型>] [--depth N] [--local];dump 需 --layout;dangling --local 只列本仓悬空(跨仓注记单列不对账)'
|
|
795
866
|
|
|
796
867
|
export async function cmdKnowledgeGraph(dir, args, opts = {}) {
|
|
797
868
|
const out = (ok, data, error) => console.log(JSON.stringify({ ok, ...(data || {}) , ...(error ? { error } : {}) }, null, 2))
|
|
@@ -822,7 +893,7 @@ export async function cmdKnowledgeGraph(dir, args, opts = {}) {
|
|
|
822
893
|
stats: s,
|
|
823
894
|
summary: [
|
|
824
895
|
`节点 ${s.nodes} · 边 ${s.edges}`,
|
|
825
|
-
`孤儿 ${s.orphans} · 模块文档缺口 ${s.module_doc_gaps} · changelog 悬空 ${s.changelog_danglings} · 悬空引用 ${s.dangling_refs}
|
|
896
|
+
`孤儿 ${s.orphans} · 模块文档缺口 ${s.module_doc_gaps} · changelog 悬空 ${s.changelog_danglings} · 悬空引用 ${s.dangling_refs}(本仓 ${s.dangling_refs_local} · 跨仓注记 ${s.dangling_refs_breakdown.cross_repo}——目标仓不在本仓,单列不对账)`,
|
|
826
897
|
`簇 ${s.clusters.length} 个(最大 ${s.clusters[0]?.key ?? '—'} × ${s.clusters[0]?.count ?? 0})`,
|
|
827
898
|
],
|
|
828
899
|
})
|
|
@@ -907,12 +978,18 @@ export async function cmdKnowledgeGraph(dir, args, opts = {}) {
|
|
|
907
978
|
orphans: list.map((f) => ({ id: f.node.id, type: f.node.type, kind: f.kind })),
|
|
908
979
|
})
|
|
909
980
|
}
|
|
910
|
-
// dangling
|
|
911
|
-
const
|
|
981
|
+
// dangling(--local 过滤跨仓注记:治理动作只看本仓可解析面——跨仓目标活在独立仓)
|
|
982
|
+
const localOnly = args.includes('--local')
|
|
983
|
+
const allDangling = graphDangling(graph)
|
|
984
|
+
const crossRepoCount = allDangling.filter((d) => d.crossRepo).length
|
|
985
|
+
const list = localOnly ? allDangling.filter((d) => !d.crossRepo) : allDangling
|
|
912
986
|
return out(true, {
|
|
913
|
-
query: { sub },
|
|
987
|
+
query: { sub, local: localOnly || undefined },
|
|
914
988
|
count: list.length,
|
|
915
|
-
summary: list.map((d) => `⚠️ ${d.edge.type}(${d.strength}) ${d.edge.s} → ${d.missing}`),
|
|
989
|
+
summary: list.map((d) => `⚠️ ${d.edge.type}(${d.strength}) ${d.edge.s} → ${d.missing}${d.crossRepo ? ' 〔跨仓注记〕' : ''}`),
|
|
916
990
|
dangling: list.map((d) => ({ edge: d.edge, missing: d.missing, strength: d.strength })),
|
|
991
|
+
// 口径分流(附加字段):本仓可行动数 + 跨仓注记数单列不对账;--local 时 count 即本仓数
|
|
992
|
+
dangling_refs_local: allDangling.length - crossRepoCount,
|
|
993
|
+
cross_repo: crossRepoCount,
|
|
917
994
|
})
|
|
918
995
|
}
|
package/src/knowledge-match.js
CHANGED
|
@@ -61,7 +61,7 @@ function isDecisionRoute(entry) {
|
|
|
61
61
|
return /^decisions\//.test(String(entry.file || '').replace(/\\/g, '/'))
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const DECISION_HEADER_RE = /^##\s+(D-\d+@v\d+)\s*(.*)$/
|
|
64
|
+
const DECISION_HEADER_RE = /^##\s+(D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+)\s*(.*)$/
|
|
65
65
|
const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supersedes)\s*[::]\s*(.*)$/
|
|
66
66
|
// 「锚点:」单独标签精确匹配读入(仿 docs-check.js parseDecisionEntries 先例)——绝不并入
|
|
67
67
|
// DECISION_FIELD_RE:其 else-if 链末尾的 reason 回填(else if (!cur.reason))会把锚点值
|
|
@@ -69,6 +69,12 @@ const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supe
|
|
|
69
69
|
const DECISION_ANCHOR_RE = /^锚点\s*[::]\s*(.*)$/
|
|
70
70
|
// 变更:单独标签(变更名限定——D-xxx 是变更内局部序号,跨变更同号互不相干,去重键依赖它)
|
|
71
71
|
const DECISION_CHANGE_RE = /^变更\s*[::]\s*(.*)$/
|
|
72
|
+
// 来源:兜底标签(2026-10-14-decision-id-p2-hardening):2026-08-24 首修形态残留(后被「变更:」
|
|
73
|
+
// 取代,现存 20 行)——变更行无条件覆盖在前,来源仅补缺(任意行序下变更优先)
|
|
74
|
+
const DECISION_ORIGIN_RE = /^来源\s*[::]\s*(.*)$/
|
|
75
|
+
// 原编号:溯源标签(2026-10-14-decision-global-id-epoch):epoch 形态条目的局部号——graph
|
|
76
|
+
// supersedes 经它把「supersedes:D-011@v1」解析到同变更全局号段
|
|
77
|
+
const DECISION_ORIGINID_RE = /^原编号\s*[::]\s*(.*)$/
|
|
72
78
|
|
|
73
79
|
/** 「文件:」值切分 + 逐项反斜杠归一 POSIX(与 decision-distill 的 entry.files 契约对齐:
|
|
74
80
|
* split /[,,、\s]+/ + replace(/\\/g,'/'))。 */
|
|
@@ -133,7 +139,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
133
139
|
const h = line.match(DECISION_HEADER_RE)
|
|
134
140
|
if (h) {
|
|
135
141
|
flush()
|
|
136
|
-
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '' }
|
|
142
|
+
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '', originId: '' }
|
|
137
143
|
continue
|
|
138
144
|
}
|
|
139
145
|
if (!cur) continue
|
|
@@ -144,6 +150,10 @@ function parseDecisionFile(filePath, file) {
|
|
|
144
150
|
// 域文件跨变更同号条目常见(D-xxx 是变更内局部序号),去重键须含变更名否则先到条目遮蔽后来者)
|
|
145
151
|
const c = line.match(DECISION_CHANGE_RE)
|
|
146
152
|
if (c) { cur.change = c[1].trim(); continue }
|
|
153
|
+
const o = line.match(DECISION_ORIGIN_RE)
|
|
154
|
+
if (o) { if (!cur.change) cur.change = o[1].trim(); continue }
|
|
155
|
+
const oi = line.match(DECISION_ORIGINID_RE)
|
|
156
|
+
if (oi) { cur.originId = oi[1].trim(); continue }
|
|
147
157
|
const f = line.match(DECISION_FIELD_RE)
|
|
148
158
|
if (!f) continue
|
|
149
159
|
const value = f[2].trim()
|
|
@@ -170,20 +180,25 @@ function parseDecisionFile(filePath, file) {
|
|
|
170
180
|
export function parseDecisionEntries(indexDir, indexEntries = null) {
|
|
171
181
|
const routes = (indexEntries || parseKnowledgeIndex(indexDir)).filter(isDecisionRoute)
|
|
172
182
|
const hits = []
|
|
183
|
+
// 路由行按文件去重单解析(2026-10-14-decision-id-p2-hardening):多条路由行指向同一域文件只解析
|
|
184
|
+
// 一次——此前靠条目级折叠兜完全重复,legacy 不折叠后从源头消除重复
|
|
185
|
+
const parsed = new Set()
|
|
173
186
|
for (const route of routes) {
|
|
187
|
+
const fileKey = String(route.file || '').replace(/\\/g, '/')
|
|
188
|
+
if (parsed.has(fileKey)) continue
|
|
189
|
+
parsed.add(fileKey)
|
|
174
190
|
const filePath = join(indexDir, route.file)
|
|
175
191
|
if (!existsSync(filePath)) continue // 路由行失效(域文件已删)→ 跳过不阻断
|
|
176
192
|
hits.push(...parseDecisionFile(filePath, route.file))
|
|
177
193
|
}
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// 注入同样被遮蔽)。无「变更:」行的 legacy 条目 change='' 与同号 legacy 条目互折叠
|
|
183
|
-
// (维持旧行为,不放大历史条目数)。
|
|
194
|
+
// 带 change 条目按 file+id+change 去重(坑 decision-cross-change-id-shadow,2026-09-11 dogfood 实证:
|
|
195
|
+
// 域文件跨变更同号条目常见,键须含变更名否则先到遮蔽——runtime.md 七个 D-001@v1 分属七变更)。
|
|
196
|
+
// 无「变更:/来源:」行的 legacy 条目不再互折叠(2026-10-14-decision-id-p2-hardening:旧互折叠致
|
|
197
|
+
// 同号 legacy 互相遮蔽——实测 2 条 implemented 条目永久隐身;路由单解析后完全重复已从源头消除)。
|
|
184
198
|
const seen = new Set()
|
|
185
199
|
return hits.filter(h => {
|
|
186
|
-
|
|
200
|
+
if (!h.change) return true
|
|
201
|
+
const key = `${h.file}#${h.id}#${h.change}`
|
|
187
202
|
if (seen.has(key)) return false
|
|
188
203
|
seen.add(key)
|
|
189
204
|
return true
|
|
@@ -221,6 +236,16 @@ export function matchDecisionsByFiles(indexDir, files) {
|
|
|
221
236
|
return result
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
/** 决策展示行变更名限定词(2026-10-14-supersedes-mislink-and-change-display):D-xxx 是变更内
|
|
240
|
+
* 局部序号,跨变更同号条目常见(知识库 D-001@v1 ×296、其中 192 条同题)——四处展示面
|
|
241
|
+
* ({DECISION_HITS}/轻量道防复潮/knowledge-gate/decision-touch)经本函数统一渲染限定词消歧;
|
|
242
|
+
* change 缺失(legacy 无「变更:」行)固定降级「变更未记录」——不编造、不省略。
|
|
243
|
+
* 单一源防四处文案漂移(与 deathPathNote 同址同规约)。 */
|
|
244
|
+
export function decisionChangeQualifier(change) {
|
|
245
|
+
const c = String(change || '').trim()
|
|
246
|
+
return c ? `变更 ${c}` : '变更未记录'
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
/** 死路注记短句提取(消费方渲染用):理由里「死路:…」到首个句读为止;无标记时截前 60 字。
|
|
225
250
|
* 单一源供 flow 知识注入段与 complete.js knowledge-gate 回显共用(2026-09-28-knowledge-inject-ranking)。 */
|
|
226
251
|
export function deathPathNote(reason) {
|
package/src/knowledge-stats.js
CHANGED
|
@@ -99,6 +99,10 @@ export function buildHitMatrix(knowledgeDir, runtimeDir, { sinceDays = 30 } = {}
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// ── FR 索引实验聚合(2026-09-18-fr-index-l1 后续钩子·L3 裁决仪表盘)──
|
|
102
|
+
// L3 已裁决冻结(2026-10-15-fr-chain-l3-adjudication D-001@v1):证伪三条件全中(承接引用率
|
|
103
|
+
// 1/452≈0% / fr-supersede 窗口 2 次 / fr-duplicate-warning 24 次零候选),条款明文合法结局——
|
|
104
|
+
// 活规格树不建,fr 索引降级保留检索/注入面;本区指标转历史观察,候选视图退役为冻结注记。
|
|
105
|
+
const FR_L3_ADJUDICATED_NOTE = '已裁决(2026-10-15 D-001@v1):L3 冻结不建,fr 索引保留检索/注入面——本区指标转历史观察(复潮条件见裁决条目)'
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* 聚合 L1 四类 fr-* 遥测事件 + knowledge/fr/ 索引面统计——L3 裁决(design 实验证伪条款)
|
|
@@ -314,7 +318,7 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
314
318
|
if (frIndex.present) {
|
|
315
319
|
const e = frIndex.events
|
|
316
320
|
const i = frIndex.index
|
|
317
|
-
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design
|
|
321
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)|${FR_L3_ADJUDICATED_NOTE}`)
|
|
318
322
|
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateWarningCandidates || e.frDuplicateCandidates || 0} 候选)`)
|
|
319
323
|
lines.push(` 腐烂 suspect fr-rot-suspect ${e.frRotSuspect || 0} 次(${e.frRotSuspectChanges || 0} 变更${e.frRotSuspectByDomain && e.frRotSuspectByDomain.length > 0 ? `,按域 ${e.frRotSuspectByDomain.map(d => `${d.domain}×${d.count}`).join(',')}` : ''})`)
|
|
320
324
|
if (e.frUnreferenced.length > 0) {
|
|
@@ -325,10 +329,8 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
325
329
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
326
330
|
const cand = frIndex.adjudicationCandidates || []
|
|
327
331
|
if (cand.length > 0) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
331
|
-
}
|
|
332
|
+
// L3 冻结后候选视图退役(数据面 adjudicationCandidates 保留 JSON 可查)——显示冻结注记
|
|
333
|
+
lines.push(` 裁决候选视图已退役|${FR_L3_ADJUDICATED_NOTE}`)
|
|
332
334
|
}
|
|
333
335
|
} else {
|
|
334
336
|
lines.push('(FR 索引面不存在——knowledge/fr/ 目录缺失或无条目;归档完整流程变更后此处可查)')
|
|
@@ -375,12 +377,12 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
375
377
|
|
|
376
378
|
lines.push(`遥测计数(窗口内):注入 ${result.totalInjects} 次 | 归类 ${result.totalClassifies} 次${lastEventAt ? ` | 数据截至 ${lastEventAt.slice(0, 10)}` : ''}`)
|
|
377
379
|
|
|
378
|
-
// FR 索引实验(L3
|
|
380
|
+
// FR 索引实验(L3 裁决仪表盘——证伪条款出数面;L3 已裁决冻结 2026-10-15,指标转历史观察)
|
|
379
381
|
if (frIndex.present) {
|
|
380
382
|
const e = frIndex.events
|
|
381
383
|
const i = frIndex.index
|
|
382
384
|
lines.push('')
|
|
383
|
-
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design
|
|
385
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)|${FR_L3_ADJUDICATED_NOTE}`)
|
|
384
386
|
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateCandidates} 候选)`)
|
|
385
387
|
if (e.frUnreferenced.length > 0) {
|
|
386
388
|
const top = e.frUnreferenced.slice(0, 5).map((u) => `${u.domain}×${u.count}`).join(',')
|
|
@@ -388,14 +390,11 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
388
390
|
}
|
|
389
391
|
lines.push(` 索引面:${i.entries} 条(active ${i.active} / superseded ${i.superseded})| 来源变更 ${i.sourceChanges} 个 | 域 ${i.domains.join(', ') || '—'}`)
|
|
390
392
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
+
// 裁决候选视图退役(L3 冻结,2026-10-15-fr-chain-l3-adjudication D-001@v1)——数据面
|
|
394
|
+
// adjudicationCandidates 保留 JSON 可查,显示面只留冻结注记。
|
|
393
395
|
const cand = frIndex.adjudicationCandidates || []
|
|
394
396
|
if (cand.length > 0) {
|
|
395
|
-
lines.push(
|
|
396
|
-
for (const c of cand.slice(0, 10)) {
|
|
397
|
-
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
398
|
-
}
|
|
397
|
+
lines.push(` 裁决候选视图已退役|${FR_L3_ADJUDICATED_NOTE}`)
|
|
399
398
|
}
|
|
400
399
|
}
|
|
401
400
|
|
package/src/knowledge-vector.js
CHANGED
|
@@ -18,20 +18,65 @@
|
|
|
18
18
|
* "change": "2026-09-26-thin-agent-tasks", "score": 0.83 } ] }
|
|
19
19
|
* anchor = 决策条目 id(## D-xxx@vN);change = 条目「变更:」字段——同号条目跨变更常见
|
|
20
20
|
* (runtime.md 七个 D-001@v1 先例),change 在场时消歧;缺省时同号条目全量带回(候选诚实)。
|
|
21
|
+
* epoch 注记(2026-10-14-decision-global-id-epoch):新蒸馏条目 anchor 为全局号形态
|
|
22
|
+
* D-<域>-NNN@vN——等值匹配语义不变;平台重建索引窗口内向量召回对新形态条目降级
|
|
23
|
+
* (本地词面回退不受影响),重索引后恢复。
|
|
21
24
|
*
|
|
22
25
|
* 降级纪律:任何失败(未连接/开关关闭/网络/404/5xx/超时/响应形态不符)静默降级本地层
|
|
23
26
|
* ——检索是 advisory 面且高频(每方案步 --done),失败刷屏即狼来了;debug 排障可设
|
|
24
27
|
* SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG=1 打单行stderr。
|
|
28
|
+
*
|
|
29
|
+
* 开关契约(2026-10-15-vector-capability-autodetect 终态;前身 vector-gate-tighten 缺省关是
|
|
30
|
+
* 「端点不存在+无发现机制」时代的正确补丁,平台端点落地后 auto 成为诚实缺省):
|
|
31
|
+
* 缺省(未写键/auto)跟随平台能力自动探测——能力缓存 .runtime/vector-capability.json
|
|
32
|
+
* (404→unavailable 重试窗 1h 内跳过;成功→available;按平台 URL 匹配;跨进程持久),
|
|
33
|
+
* 平台部署端点后最迟 1h 全仓自动启用,消费仓零配置。local.yaml on=强制开跳过探测、
|
|
34
|
+
* off=强制关(用户逃生阀);env SILLYSPEC_KNOWLEDGE_VECTOR 同语义强制。
|
|
35
|
+
* 404 另有进程级死信短路(端点死信不再空转探测)。
|
|
25
36
|
*/
|
|
26
|
-
import { readFileSync } from 'fs'
|
|
37
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
27
38
|
import { join, dirname } from 'path'
|
|
28
39
|
import { readPlatformConfig } from './sync.js'
|
|
29
40
|
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries } from './knowledge-match.js'
|
|
30
41
|
|
|
31
42
|
const VECTOR_TIMEOUT_MS = 3000
|
|
32
43
|
const ENDPOINT_PATH = '/api/spec/knowledge/vector-search'
|
|
44
|
+
/** 能力缓存重试窗:unavailable 记录 1h 内不再探测(平台部署后最迟 1h 自动启用)。 */
|
|
45
|
+
const CAPABILITY_RETRY_MS = 60 * 60 * 1000
|
|
46
|
+
|
|
47
|
+
// ── 平台能力探测缓存(2026-10-15-vector-capability-autodetect)────────────────
|
|
48
|
+
// .runtime/ 已 gitignored 且被 daemon spec 上传排除(UPLOAD_EXCLUDE_TOP_BASE),是仓内
|
|
49
|
+
// 机器态的标准落点(knowledge-hits 同款)。写盘全 fail-soft;按平台 URL 匹配(换平台即失效)。
|
|
50
|
+
|
|
51
|
+
function capabilityCachePath(cwd) {
|
|
52
|
+
return join(cwd, '.sillyspec', '.runtime', 'vector-capability.json')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readCapabilityCache(cwd) {
|
|
56
|
+
try {
|
|
57
|
+
const obj = JSON.parse(readFileSync(capabilityCachePath(cwd), 'utf-8'))
|
|
58
|
+
const cfg = readPlatformConfig(cwd)
|
|
59
|
+
if (!cfg || !cfg.url) return null
|
|
60
|
+
if (obj.url !== String(cfg.url).replace(/\/+$/, '')) return null // 换平台 → 缓存失效
|
|
61
|
+
if (typeof obj.available !== 'boolean' || typeof obj.probedAt !== 'number') return null
|
|
62
|
+
return { available: obj.available, probedAt: obj.probedAt }
|
|
63
|
+
} catch { /* 无缓存/坏 JSON = 未探测 */ }
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeCapabilityCache(cwd, platformUrl, available) {
|
|
68
|
+
try {
|
|
69
|
+
const p = capabilityCachePath(cwd)
|
|
70
|
+
const cur = readCapabilityCache(cwd)
|
|
71
|
+
if (cur && cur.available === available && Date.now() - cur.probedAt < CAPABILITY_RETRY_MS) return // 值未变且未过期:零写盘
|
|
72
|
+
const dir = join(p, '..')
|
|
73
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
74
|
+
writeFileSync(p, JSON.stringify({ url: platformUrl, available, probedAt: Date.now() }), 'utf-8')
|
|
75
|
+
} catch { /* 缓存写失败不影响检索本体(fail-soft) */ }
|
|
76
|
+
}
|
|
33
77
|
|
|
34
|
-
/** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search
|
|
78
|
+
/** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search 值;
|
|
79
|
+
* 未写键/无 yaml → null=未表态——2026-10-15-vector-gate-tighten 起缺省关,显式落盘才启用)。 */
|
|
35
80
|
function vectorSwitchFromLocalYaml(cwd) {
|
|
36
81
|
try {
|
|
37
82
|
const lines = readFileSync(join(cwd, '.sillyspec', 'local.yaml'), 'utf-8').split(/\r?\n/)
|
|
@@ -43,16 +88,55 @@ function vectorSwitchFromLocalYaml(cwd) {
|
|
|
43
88
|
const kv = line.match(/^\s+vector_search\s*:\s*(\S+)/)
|
|
44
89
|
if (kv) return kv[1].toLowerCase()
|
|
45
90
|
}
|
|
46
|
-
} catch { /* 无 local.yaml =
|
|
47
|
-
return
|
|
91
|
+
} catch { /* 无 local.yaml = 未表态 */ }
|
|
92
|
+
return null
|
|
48
93
|
}
|
|
49
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 向量层开关(2026-10-15-vector-capability-autodetect 终态契约;前身 2026-10-15-vector-gate-tighten
|
|
97
|
+
* 缺省关是「端点不存在+无发现机制」时代的正确补丁,端点落地后 auto 成为诚实缺省):
|
|
98
|
+
* env SILLYSPEC_KNOWLEDGE_VECTOR=on|auto → 强制启用;off(其他值保守视为 off)→ 强制关;
|
|
99
|
+
* local.yaml knowledge.vector_search: on → 强制启用;off → 强制关(用户显式逃生阀);
|
|
100
|
+
* auto/未写键 → **跟随平台能力自动探测**:读能力缓存(.runtime/vector-capability.json,
|
|
101
|
+
* {url,available,probedAt})——unavailable 未过重试窗(1h)=关,否则乐观试(探测结果由
|
|
102
|
+
* platformVectorRecall 落缓存,404→unavailable / 成功→available;跨进程持久)。
|
|
103
|
+
* 「端点部署本身就是开关」——平台部署后最迟 1h 内全仓自动启用,消费仓零配置。
|
|
104
|
+
*/
|
|
50
105
|
function vectorEnabled(cwd) {
|
|
51
106
|
const env = String(process.env.SILLYSPEC_KNOWLEDGE_VECTOR || '').toLowerCase()
|
|
52
|
-
if (env === '
|
|
53
|
-
|
|
107
|
+
if (env === 'on' || env === 'auto') return true
|
|
108
|
+
if (env) return false
|
|
109
|
+
const sw = vectorSwitchFromLocalYaml(cwd)
|
|
110
|
+
if (sw === 'off') return false
|
|
111
|
+
if (sw === 'on') return true
|
|
112
|
+
// absent/auto → 能力缓存判定(无平台配置=没得探测=关)
|
|
113
|
+
if (!readPlatformConfig(cwd)) return false
|
|
114
|
+
const cap = readCapabilityCache(cwd)
|
|
115
|
+
if (cap && !cap.available && Date.now() - cap.probedAt < CAPABILITY_RETRY_MS) return false
|
|
116
|
+
return true
|
|
54
117
|
}
|
|
55
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 向量能力提示(2026-10-15-vector-capability-autodetect 语义随迁;前身 vector-switch-discoverability
|
|
121
|
+
* 的「未开请配置」提示随缺省 auto 退役——auto 模式下没有可执行配置,提示转为信息性):
|
|
122
|
+
* 仅当 auto 模式(无显式键/键=auto)且能力缓存记 unavailable 时给一行信息——「平台部署后
|
|
123
|
+
* 自动启用,无需本仓配置」。强制关(yaml off/env off=用户显式选择)与已启用 → ''(零噪音)。
|
|
124
|
+
* 平台未连接 → ''。
|
|
125
|
+
*/
|
|
126
|
+
export function vectorSwitchHint(cwd) {
|
|
127
|
+
if (!readPlatformConfig(cwd)) return ''
|
|
128
|
+
if (vectorEnabled(cwd)) return ''
|
|
129
|
+
const cap = readCapabilityCache(cwd)
|
|
130
|
+
if (cap && !cap.available) {
|
|
131
|
+
return `ℹ️ 平台向量语义检索未就绪(自动探测缓存生效中)——平台部署 vector-search 端点后最迟 1 小时内本仓自动启用,无需配置`
|
|
132
|
+
}
|
|
133
|
+
return ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 404 死信端点(进程级,按平台 URL 记):端点未实现=今天真实平台状态,标记后本进程不再
|
|
137
|
+
* 对同 URL 空转探测(CLI 短进程生命周期天然重置;测试各用独立端口互不污染)。 */
|
|
138
|
+
const vectorDeadEndpoints = new Set()
|
|
139
|
+
|
|
56
140
|
/**
|
|
57
141
|
* 平台向量召回(纯 HTTP 层)。未连接/失败/形态不符 → null(调用方降级)。
|
|
58
142
|
* @returns {Promise<{results: Array<{spec_path: string, anchor: string, score: number}>}|null>}
|
|
@@ -60,19 +144,26 @@ function vectorEnabled(cwd) {
|
|
|
60
144
|
export async function platformVectorRecall({ cwd, query, limit = 10, timeoutMs = VECTOR_TIMEOUT_MS }) {
|
|
61
145
|
const cfg = readPlatformConfig(cwd)
|
|
62
146
|
if (!cfg || !cfg.url || !cfg.token) return null
|
|
147
|
+
const base = String(cfg.url).replace(/\/+$/, '')
|
|
148
|
+
if (vectorDeadEndpoints.has(base)) return null
|
|
63
149
|
const q = String(query || '').trim().slice(0, 4000)
|
|
64
150
|
if (!q) return null
|
|
65
151
|
try {
|
|
66
|
-
const res = await fetch(`${
|
|
152
|
+
const res = await fetch(`${base}${ENDPOINT_PATH}`, {
|
|
67
153
|
method: 'POST',
|
|
68
154
|
headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
|
|
69
155
|
body: JSON.stringify({ query: q, limit }),
|
|
70
156
|
signal: AbortSignal.timeout(timeoutMs),
|
|
71
157
|
})
|
|
72
158
|
if (!res.ok) {
|
|
159
|
+
if (res.status === 404) {
|
|
160
|
+
vectorDeadEndpoints.add(base) // 端点未实现——本进程短路后续探测
|
|
161
|
+
writeCapabilityCache(cwd, base, false) // 跨进程持久:重试窗内不再探测(平台部署后自动启用)
|
|
162
|
+
}
|
|
73
163
|
if (process.env.SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG) console.error(`[knowledge-vector] ${res.status} ${ENDPOINT_PATH}——降级本地`)
|
|
74
164
|
return null
|
|
75
165
|
}
|
|
166
|
+
writeCapabilityCache(cwd, base, true) // 端点在场——缓存 available(后续进程零探测直用)
|
|
76
167
|
const body = await res.json().catch(() => null)
|
|
77
168
|
const results = body && Array.isArray(body.results)
|
|
78
169
|
? body.results.filter((r) => r && typeof r.spec_path === 'string' && typeof r.anchor === 'string')
|
package/src/machine-interface.js
CHANGED
|
@@ -187,10 +187,25 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
187
187
|
// 各 spawn 2 个 git 进程(gate execute 一次省 ≈ 60-200ms on Windows)。
|
|
188
188
|
const sharedEvidence = stage === 'execute' ? checkExecuteCodeEvidence(cwd, changeName) : undefined;
|
|
189
189
|
|
|
190
|
+
// ── gate 预检实况注入(2026-10-14-gate-verify-apiface-preflight FR-01~FR-03):facts.apiFace
|
|
191
|
+
// 唯一生产者是 run verify --done 收尾前置 backfill(run/gates.js),gate 路径不回填且 --init
|
|
192
|
+
// 刻意不落键 → 锚点核对 parseSet 空/旧 → design接口表# 锚点恒空指(平台 6 轮 gate 实证)。
|
|
193
|
+
// 此处以同一解析器实况解析 design.md 经 context 注入 validator(与 --done backfill 产物同形
|
|
194
|
+
// 同源,零落盘守 D-002 只读契约);design.md 缺失/不可读 helper 返 null、解析异常 fail-soft
|
|
195
|
+
// ——两种态都不注入,退回 facts 口径(现状行为;评审 P1:空面注入会覆写有效 facts 反致误阻断)。──
|
|
196
|
+
let apiFaceLive = null;
|
|
197
|
+
if (stage === 'verify') {
|
|
198
|
+
try {
|
|
199
|
+
const { computeApiFaceFromDesign } = await import('./verify-probes.js');
|
|
200
|
+
apiFaceLive = computeApiFaceFromDesign(join(specRoot, 'changes', changeName, 'design.md'));
|
|
201
|
+
} catch { /* fail-soft:不注入退回 facts 口径 */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
// ── a. artifacts:阶段产物校验(全部阶段)──
|
|
191
205
|
const r = runValidators(stage, cwd, changeName, {
|
|
192
206
|
projectName: progress.project,
|
|
193
207
|
specRoot,
|
|
208
|
+
...(apiFaceLive ? { apiFaceLive } : {}),
|
|
194
209
|
...(sharedEvidence ? { evidence: sharedEvidence } : {}),
|
|
195
210
|
});
|
|
196
211
|
checks.push({
|
|
@@ -752,10 +767,21 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
752
767
|
|
|
753
768
|
// ── d. artifacts:当前阶段产物校验 ──
|
|
754
769
|
// runValidators 需要 stage:用 progress.currentStage(若为空串则校验器内部处理)。
|
|
770
|
+
// verify 阶段同款实况注入(2026-10-14-gate-verify-apiface-preflight):与 runGate 同因——
|
|
771
|
+
// facts.apiFace 唯一生产者是 --done backfill,derive 不回填 → 锚点核对恒空指;零落盘 +
|
|
772
|
+
// fail-soft(缺失返 null/异常两态都不注入退回 facts 口径),守 D-002 只读契约。
|
|
755
773
|
case 'artifacts': {
|
|
774
|
+
let apiFaceLive = null;
|
|
775
|
+
if (currentStage === 'verify') {
|
|
776
|
+
try {
|
|
777
|
+
const { computeApiFaceFromDesign } = await import('./verify-probes.js');
|
|
778
|
+
apiFaceLive = computeApiFaceFromDesign(join(specRoot, 'changes', changeName, 'design.md'));
|
|
779
|
+
} catch { /* fail-soft:不注入退回 facts 口径 */ }
|
|
780
|
+
}
|
|
756
781
|
const r = runValidators(currentStage, cwd, changeName, {
|
|
757
782
|
projectName: progress.project,
|
|
758
783
|
specRoot,
|
|
784
|
+
...(apiFaceLive ? { apiFaceLive } : {}),
|
|
759
785
|
});
|
|
760
786
|
data = { ok: r.ok, errors: r.errors, warnings: r.warnings };
|
|
761
787
|
ok = r.ok;
|
package/src/module-impact.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* 归属分类同一套 module-map 前缀匹配,仅输入来源不同。
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
17
|
+
import { modulePathMatches } from './modules.js'
|
|
17
18
|
import { join } from 'path'
|
|
18
19
|
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
19
20
|
import { resolveSpecDir } from './run/shared.js'
|
|
@@ -87,11 +88,7 @@ function classifyFile(posixPath, modulePaths) {
|
|
|
87
88
|
for (const [moduleId, paths] of modulePaths) {
|
|
88
89
|
for (const p of paths) {
|
|
89
90
|
// 目录条目以 / 结尾 → 前缀匹配;文件条目 → 精确或子路径(与模块上下文索引读侧口径一致)
|
|
90
|
-
if (p
|
|
91
|
-
if (posixPath.startsWith(p)) return moduleId
|
|
92
|
-
} else if (posixPath === p || posixPath.startsWith(p + '/')) {
|
|
93
|
-
return moduleId
|
|
94
|
-
}
|
|
91
|
+
if (modulePathMatches(posixPath, p)) return moduleId
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
94
|
return null
|
package/src/modules.js
CHANGED
|
@@ -2,6 +2,37 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { gitQuiet } from './git-helper.js';
|
|
4
4
|
import { nowWallClock } from './datetime.js';
|
|
5
|
+
import { globMatch } from './change-list.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 模块 paths 条目 × 文件路径 的统一归属判定(2026-10-15-module-path-glob-match)。
|
|
9
|
+
*
|
|
10
|
+
* 背景:sillyspec-scan 生成的 module-map(消费仓)大量使用 glob 形态 paths——
|
|
11
|
+
* `app/modules/daemon/**`、`frontend/src/components/daemon/*.tsx`、多段通配与文件通配的
|
|
12
|
+
* 组合(`**` 与 `*.tsx` 同串)、括号路径等(平台仓实测 146 条)——而历史各消费点只做朴素
|
|
13
|
+
* `file === p || startsWith(p + '/')`,glob 条目恒不命中(恰好只有精确文件路径能命中),
|
|
14
|
+
* 导致消费仓全链路域路由失效:交付文件匹配不上模块 → 三级兜底全灭 → FR 铸伪域
|
|
15
|
+
* (auto-backend 等)、决策落 unmapped。
|
|
16
|
+
*
|
|
17
|
+
* 语义(本仓无 glob map 时与旧各点逐字等价——零回归):
|
|
18
|
+
* - 尾部 `/` 的目录条目:startsWith(旧 module-impact 目录分支语义)
|
|
19
|
+
* - 其余条目:相等 或 `p/` 前缀(旧 fr-index/ceremony-tier 等语义)
|
|
20
|
+
* - 含 `*`/`?` 的 glob 条目:追加 globMatch(双星跨零或多段、单星段内、单问号单字符;
|
|
21
|
+
* change-list.js 既有完备实现单一源复用)
|
|
22
|
+
* @param {string} filePath 仓根相对文件路径(含反斜杠容忍)
|
|
23
|
+
* @param {string} rawPattern module-map 的 paths/core_files 条目
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function modulePathMatches(filePath, rawPattern) {
|
|
27
|
+
const f = String(filePath || '').replace(/\\/g, '/');
|
|
28
|
+
const raw = String(rawPattern || '').replace(/\\/g, '/');
|
|
29
|
+
if (!f || !raw) return false;
|
|
30
|
+
if (raw.endsWith('/')) return f.startsWith(raw);
|
|
31
|
+
const p = raw.replace(/\/+$/, '');
|
|
32
|
+
if (!p) return false;
|
|
33
|
+
if (f === p || f.startsWith(p + '/')) return true;
|
|
34
|
+
return globMatch(f, p);
|
|
35
|
+
}
|
|
5
36
|
|
|
6
37
|
|
|
7
38
|
/**
|