sillyspec 3.32.9 → 3.32.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  2. package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
  5. package/AGENTS.md +1 -1
  6. package/assets/command-cards/flow.md +1 -1
  7. package/package.json +1 -1
  8. package/src/commit-suggest.js +45 -4
  9. package/src/config-schema.js +14 -0
  10. package/src/decision-distill.js +77 -21
  11. package/src/decision-renumber.js +157 -0
  12. package/src/docs-check.js +30 -15
  13. package/src/docs-debt.js +3 -2
  14. package/src/doctor-diagnostics.js +3 -1
  15. package/src/flow-parity.js +18 -2
  16. package/src/flow.js +75 -12
  17. package/src/fr-index.js +15 -1
  18. package/src/hooks/worktree-guard.js +3 -3
  19. package/src/index.js +7 -5
  20. package/src/knowledge-digest.js +23 -6
  21. package/src/knowledge-graph.js +92 -14
  22. package/src/knowledge-match.js +34 -9
  23. package/src/knowledge-stats.js +12 -13
  24. package/src/knowledge-vector.js +98 -7
  25. package/src/machine-interface.js +26 -0
  26. package/src/quicklog.js +6 -1
  27. package/src/run/archive-distill.js +3 -0
  28. package/src/run/command.js +3 -3
  29. package/src/run/complete-handlers.js +8 -3
  30. package/src/run/complete.js +113 -44
  31. package/src/run/gates.js +93 -89
  32. package/src/run/prompt.js +3 -3
  33. package/src/run/stage.js +1 -1
  34. package/src/sentinel-assertions.js +43 -1
  35. package/src/stage-contract.js +26 -3
  36. package/src/stage-review.js +44 -11
  37. package/src/stage-templates.js +2 -0
  38. package/src/stages/brainstorm.js +5 -1
  39. package/src/stages/knowledge.js +56 -5
  40. package/src/sync.js +28 -2
  41. package/src/task-tick.js +12 -0
  42. package/src/timeline.js +60 -19
  43. package/src/verify-postcheck.js +49 -1
  44. package/src/verify-probes.js +61 -18
  45. package/templates/agents-instruction.md +1 -1
@@ -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
- // 多条路由行指向同一域文件时按 file+id+变更 去重(完全重复折叠);键含变更名——
179
- // 跨变更同号条目(D-xxx 是变更内局部序号,域文件内同 id 不同变更常见)不再被先到者
180
- // 遮蔽(坑 decision-cross-change-id-shadow,2026-09-11 dogfood 实证:runtime.md 七个
181
- // D-001@v1 分属七变更,旧键只放行首个——同号 rejected 决策在 {DECISION_HITS} 防复潮
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
- const key = `${h.file}#${h.id}#${h.change || ''}`
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) {
@@ -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
- lines.push(' 裁决候选 Top-10(suspect×unref 条目级聚合——候选非裁决,确认无人承接→承接翻链或人工退休):')
329
- for (const c of cand.slice(0, 10)) {
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 裁决仪表盘——证伪条款出数面;裁决时建议 --since-days 大窗口读全口径)
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
- // 裁决候选视图(2026-10-03-fr-governance-telemetry):条目级 id 聚合(事件 frIds/ids,帽 20 抽样
392
- // 为下界)。候选非裁决——高 suspect 天然来自热文件;处置出口:确认无人承接 → 承接翻链或人工退休。
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(' 裁决候选 Top-10(suspect×unref 条目级聚合——候选非裁决,确认无人承接→承接翻链或人工退休):')
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
 
@@ -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: off)。 */
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 = 缺省 auto */ }
47
- return 'auto'
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 === 'off' || env === 'auto') return env === 'auto'
53
- return vectorSwitchFromLocalYaml(cwd) !== 'off'
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(`${String(cfg.url).replace(/\/+$/, '')}${ENDPOINT_PATH}`, {
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')
@@ -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/quicklog.js CHANGED
@@ -124,13 +124,18 @@ export function sanitizeDesc(description) {
124
124
  * 「需求:/动机:」类标签前缀,空白折叠单行,超长截断加 …;空输入返回 ''(调用方兜底
125
125
  * change 名)。agent 在 --input 首行写一句中文概括即成标题(flow start 横幅有指引),
126
126
  * --title 显式覆盖一切推导。
127
+ * 段头行防御(2026-10-15-title-label-skip):过门格式示例曾被读成段头形态——agent 把
128
+ * 「动机与背景:」独立成行照抄,白名单无复合词致整行成标题(进度库 6 变更实证)。两道
129
+ * 防御:白名单补复合词(动机与背景/背景与动机);任意短段头行(≤12 字非冒号内容+行尾
130
+ * 冒号,如「变更背景:」「详细说明:」)整行跳过取次非空行——冒号后无内容不成标题。
127
131
  */
128
132
  export function deriveChangeTitle(input, max = 50) {
129
133
  for (const l of String(input || '').replace(/\r\n/g, '\n').split('\n')) {
130
134
  const t = l.trim()
131
135
  if (!t) continue
132
136
  if (/^成功标准\s*[::]/.test(t)) break
133
- const stripped = t.replace(/^(?:需求|动机|背景|任务|标题)\s*[::]\s*/, '').trim()
137
+ if (/^[^::]{1,12}[::]$/.test(t)) continue
138
+ const stripped = t.replace(/^(?:需求|动机|背景|任务|标题|动机与背景|背景与动机)\s*[::]\s*/, '').trim()
134
139
  if (!stripped) continue
135
140
  const one = stripped.replace(/\s+/g, ' ')
136
141
  return one.length > max ? one.slice(0, max) + '…' : one
@@ -39,6 +39,9 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
39
39
  console.warn(`\n⚠️ rejected 决策缺否决理由/复潮条件(needsWait)——这些条目未写盘,其余条目照常提炼。`)
40
40
  console.warn(` 裁决指引(确认归档 --confirm 前处理):`)
41
41
  console.warn(` ① 补录:把缺失字段补进 ${join(changeDir, 'decisions.md')} 后重跑提炼(幂等)——`)
42
+ console.warn(` 可复制补丁样例(在对应 D-xxx@vN 条目末尾追加两行,字段名与值之间用冒号+空格):`)
43
+ console.warn(` - 否决理由: <一句话核心劣势>`)
44
+ console.warn(` - 复潮条件: <何种信号下可重议本方案>`)
42
45
  console.warn(` node --input-type=module -e "import { distillIntoKnowledge } from '<sillyspec 源码路径>/src/decision-distill.js'; console.log(JSON.stringify(distillIntoKnowledge(${JSON.stringify(changeDir)}, ${JSON.stringify(knowledgeRoot)}, ${JSON.stringify(headHash)}), null, 2))"`)
43
46
  console.warn(` ② 跳过:经用户裁决不入库该条,并在「确认归档」--output 注记跳过原因。`)
44
47
  }
@@ -232,7 +232,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
232
232
  function emitReadonlyEmptyGuide() {
233
233
  console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
234
234
  console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
235
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
235
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
236
236
  console.log('')
237
237
  console.log(' 成功标准:')
238
238
  console.log(' - <可验证标准>"')
@@ -858,7 +858,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
858
858
  quickFallbackUsed = true
859
859
  } else {
860
860
  console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
861
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
861
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
862
862
  console.log('')
863
863
  console.log(' 成功标准:')
864
864
  console.log(' - <可验证标准>"')
@@ -2076,7 +2076,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
2076
2076
  if (classification.mode === 'quick') {
2077
2077
  console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
2078
2078
  console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
2079
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
2079
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2080
2080
  console.log('')
2081
2081
  console.log(' 成功标准:')
2082
2082
  console.log(' - <可验证标准>"')
@@ -816,9 +816,14 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
816
816
  const { readWatcherEvents, watcherEventsPath } = await import('../watcher.js')
817
817
  const res = readWatcherEvents({ runtimeRoot, change: changeName })
818
818
  if (!res.exists) return { ok: true, skipped: true, reason: `无事件流(${watcherEventsPath(runtimeRoot, changeName)})` }
819
- const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, readBirthTs } = await import('../timeline.js')
819
+ // 侧车透传(2026-10-14-timeline-ticktime-anchor-scope):destDir 内 task-ticks.jsonl 已随归档包
820
+ // ——烤制面读包内侧车(与 tasks.md 同源同路径形态),精确勾选时刻跨机可读;锚收敛 changeName
821
+ // 进解析(消息不含变更名的提交不锚任务行)。
822
+ const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, parseTickLog, readBirthTs } = await import('../timeline.js')
820
823
  let tasks = null
821
824
  try { tasks = parseTaskLines(readRawImpl(join(destDir, 'tasks.md'), 'utf8')) } catch { /* 任务面缺失照烤(渲染层有降级段) */ }
825
+ let tickLog = []
826
+ try { tickLog = parseTickLog(readRawImpl(join(destDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失照烤(时刻退推断面) */ }
822
827
  let tier = null
823
828
  try { const m = /^tier:\s*(\S+)/m.exec(readRawImpl(join(destDir, 'flow-state.yaml'), 'utf8')); if (m) tier = m[1] } catch { /* 头脑风暴预段等无 flow-state 按未知 */ }
824
829
  const look = gitLook || ((hash) => {
@@ -829,12 +834,12 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
829
834
  const sep2 = rest.indexOf('\x1f')
830
835
  return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) }
831
836
  })
832
- const anchors = resolveCommitAnchors(res.events, look)
837
+ const anchors = resolveCommitAnchors(res.events, look, changeName)
833
838
  let eventsRaw = null
834
839
  try { eventsRaw = readRawImpl(watcherEventsPath(runtimeRoot, changeName), 'utf8') } catch { /* 副本缺源只烤渲染面 */ }
835
840
  const oversize = eventsRaw != null && Buffer.byteLength(eventsRaw, 'utf8') > eventsCopyMaxBytes
836
841
  // 评审 P3-1 清偿:副本未落盘(读源失败或超帽)时头注记必须显式「未随包」,不虚报在场
837
- const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize })
842
+ const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize, tickLog })
838
843
  writeImpl(join(destDir, 'timeline.md'), timelineMd)
839
844
  const files = ['timeline.md']
840
845
  if (eventsRaw != null && !oversize) {