sillyspec 3.28.12 → 3.28.13

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.
@@ -45,4 +45,33 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
45
45
  } catch (e) {
46
46
  console.warn(`\n⚠️ 决策提炼执行异常,降级跳过(best-effort,不阻断归档——同旧步降级语义): ${e && e.message ? e.message : e}`)
47
47
  }
48
+
49
+ // ── FR 索引提炼(2026-09-18-fr-index-l1 L1:稳定 id 发号+承接翻链;同 best-effort 降级语义)──
50
+ try {
51
+ const { indexRequirements } = await import('../fr-index.js')
52
+ const fr = indexRequirements({ changeDir, knowledgeRoot, headHash })
53
+ const frWritten = Array.isArray(fr && fr.written) ? fr.written : []
54
+ if (fr && fr.skipped) {
55
+ console.log(`\nℹ️ FR 索引零输出:${fr.skipped}`)
56
+ } else if (frWritten.length > 0) {
57
+ console.log(`\n📌 FR 索引(CLI 机械执行,幂等):${frWritten.length} 条已发号入库——`)
58
+ for (const w of frWritten) console.log(` - ${w.id} → ${w.file}`)
59
+ }
60
+ for (const w of (fr && fr.warnings) || []) {
61
+ console.warn(` ⚠️ ${w}`)
62
+ }
63
+ // 遥测(L3 证据发生器指标,D-006/D-008):取代链跟随事件
64
+ const { appendKnowledgeHit } = await import('../knowledge-hits.js')
65
+ const runtimeRoot = join(specBase, '.runtime')
66
+ for (const s of (fr && fr.superseded) || []) {
67
+ appendKnowledgeHit(runtimeRoot, { type: 'fr-supersede', change: changeName, from: s.from, to: s.to })
68
+ }
69
+ // 删除缺口探针(D-008 护栏③):观察信号,不算 L3 门禁——L1 无删除声明义务,只采趋势数据
70
+ for (const u of (fr && fr.unreferenced) || []) {
71
+ appendKnowledgeHit(runtimeRoot, { type: 'fr-unreferenced', change: changeName, domain: u.domain, count: u.count })
72
+ console.log(` ℹ️ [观察信号·不算 L3 门禁] 触达域 ${u.domain} 有 ${u.count} 条 active FR 未被本次承接引用(fr-unreferenced 遥测,L3 裁决趋势数据)`)
73
+ }
74
+ } catch (e) {
75
+ console.warn(`\n⚠️ FR 索引执行异常,降级跳过(best-effort,不阻断归档): ${e && e.message ? e.message : e}`)
76
+ }
48
77
  }
package/src/run/prompt.js CHANGED
@@ -831,7 +831,38 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
831
831
  // fail-soft 同三件套;无 quick 占位符自然零输出。
832
832
  if (stageName === 'quick' && promptText.includes('{QUICK_CONTEXT_DIGEST}')) {
833
833
  const digestSpecBase = resolvePromptSpecBase(platformOpts, cwd)
834
- promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST\}/g, buildQuickContextDigest(digestSpecBase, projectName))
834
+ promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST}/g, buildQuickContextDigest(digestSpecBase, projectName))
835
+ }
836
+
837
+ // ①c {FR_INDEX_DIGEST}(2026-09-18-fr-index-l1 L1,D-004):brainstorm step8 的触达域现行 FR 注入
838
+ // ——写作期防重复 FR 的确定性清单(superseded 默认藏);无触达域索引/索引空 → 段消隐。
839
+ // fr-inject 遥测(L3 证据发生器指标①)fail-soft;token 本体在 stages/brainstorm.js step8 模板。
840
+ if (stageName === 'brainstorm' && promptText.includes('{FR_INDEX_DIGEST}')) {
841
+ try {
842
+ const frSpecBase = resolvePromptSpecBase(platformOpts, cwd)
843
+ const { discoverModuleIndex } = await import('../decision-distill.js')
844
+ const { resolveTouchedDomains, readActiveFrDigest } = await import('../fr-index.js')
845
+ const frChangeDir = join(frSpecBase, 'changes', String(changeName || ''))
846
+ const knowledgeRoot = join(frSpecBase, 'knowledge')
847
+ const domains = resolveTouchedDomains(frChangeDir, discoverModuleIndex(knowledgeRoot))
848
+ const entries = readActiveFrDigest(knowledgeRoot, domains)
849
+ if (entries.length === 0) {
850
+ promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
851
+ } else {
852
+ const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''})`)
853
+ lines.push('')
854
+ lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。')
855
+ promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
856
+ }
857
+ try {
858
+ const { appendKnowledgeHit } = await import('../knowledge-hits.js')
859
+ appendKnowledgeHit(join(frSpecBase, '.runtime'), {
860
+ type: 'fr-inject', change: changeName, domains, count: entries.length,
861
+ })
862
+ } catch { /* 遥测 fail-soft */ }
863
+ } catch (e) {
864
+ promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, `(FR 索引注入失败:${e && e.message ? e.message : e}——可自行读 {SPEC_ROOT}/knowledge/fr/)`)
865
+ }
835
866
  }
836
867
 
837
868
  // ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
@@ -17,6 +17,11 @@ import { evaluateRules } from './stage-contract-engine.js'
17
17
  import { getRule } from './stage-contract-spec.js'
18
18
  import { backfillFrontmatter } from './scan-postcheck.js'
19
19
  import { parseAllowedPaths } from './stages/plan-postcheck.js'
20
+ // FR 索引消费(2026-09-18-fr-index-l1 L1):重复嫌疑软门(advisory)——fr-index 是纯 fs+decision-distill
21
+ // 叶子链,静态引入无环;knowledge-hits 同为叶子。
22
+ import { parseChangeRequirements, resolveTouchedDomains, readActiveFrDigest, frTitleOverlap } from './fr-index.js'
23
+ import { discoverModuleIndex } from './decision-distill.js'
24
+ import { appendKnowledgeHit } from './knowledge-hits.js'
20
25
  // design.md 文件清单解析(2026-09-17-pass-cap-semantics task-02 事实③):change-list 是纯 fs
21
26
  // 叶子模块(design-facts / scope-audit / verify-probes 等同款直连惯例),静态引入无环。
22
27
  import { parseFileChangeListDetailed } from './change-list.js'
@@ -321,6 +326,37 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
321
326
  const errors = [...engineResult.errors]
322
327
  const warnings = [...engineResult.warnings]
323
328
 
329
+ // ── FR 重复嫌疑软门(2026-09-18-fr-index-l1 L1,D-005:advisory 永不阻断)──
330
+ // 新 requirements 的无承接 FR × 同域 active 索引条目标题 bigram 重叠 ≥0.6 → warning
331
+ // (双出路:加承接行或改标题区分)+ fr-duplicate-warning 遥测(L3 证据发生器指标③)。
332
+ // fail-soft 全包:索引不存在/解析失败零打扰(同步实现——全部叶子静态导入)。
333
+ try {
334
+ const frKnowledgeRoot = specRoot ? join(specRoot, 'knowledge') : join(cwd, '.sillyspec', 'knowledge')
335
+ const req = parseChangeRequirements(changeDir)
336
+ if (!req.missing && req.frs.length > 0) {
337
+ const domains = resolveTouchedDomains(changeDir, discoverModuleIndex(frKnowledgeRoot))
338
+ const active = readActiveFrDigest(frKnowledgeRoot, domains)
339
+ if (active.length > 0) {
340
+ for (const fr of req.frs) {
341
+ if (fr.supersedes.length > 0 || !fr.title) continue
342
+ let hit = null
343
+ for (const a of active) {
344
+ const o = frTitleOverlap(fr.title, a.title)
345
+ if (!hit || o > hit.o) hit = { a, o }
346
+ }
347
+ if (hit && hit.o >= 0.6) {
348
+ warnings.push(`疑似重复 FR:新「${fr.local} ${fr.title}」与现行 ${hit.a.id}「${hit.a.title}」标题重叠度 ${(hit.o * 100).toFixed(0)}%——若为改写/取代请在该 FR 块加承接行 \`承接: ${hit.a.id}\`,若为不同需求请改标题区分(advisory,L1 观察指标)`)
349
+ try {
350
+ appendKnowledgeHit(join(specRoot || join(cwd, '.sillyspec'), '.runtime'), {
351
+ type: 'fr-duplicate-warning', change: changeName, title: fr.title, candidate: hit.a.id, overlap: Number(hit.o.toFixed(2)),
352
+ })
353
+ } catch { /* 遥测 fail-soft */ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ } catch { /* FR 软门 fail-soft:索引/解析异常零打扰 */ }
359
+
324
360
  if (designContent) {
325
361
  const content = designContent
326
362
 
@@ -1344,10 +1380,12 @@ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
1344
1380
  //
1345
1381
  // 「要不要集成证据」(validateVerifyOutputs 的 requiresEvidence)与「能不能写 PASS」(本
1346
1382
  // validator)解耦:结论=PASS 时四个已知未验证区任一在场即 error——
1347
- // ① 集成实测未跑(facts.integrationRan=not-ran,D-006 判定表)
1348
- // ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking」)
1383
+ // ① 集成实测未跑(facts.integrationRan≠ran,D-006 判定表——字段不在场按未跑处理,与⑤同族口径)
1384
+ // ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking」;
1385
+ // facts.handover 不在场=无法核对 blocking 行,fail-closed 拦下)
1349
1386
  // ③ db 脚本未声明执行(verify 时点文件集 ∩ db/**/*.sql 对账 facts.dbScriptDeclarations,D-012)
1350
- // ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover)
1387
+ // ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover——
1388
+ // matrixPartialRows 不在场=无法核对矩阵,按含 partial 处理,仍以「零有效移交行」为触发前提)
1351
1389
  // 附加:runtimeEndpointExcluded=true 且判级 integration/deployment-critical 且 handover 零行
1352
1390
  // → 计入①事实面(D-004/FR-03,仅判级时计入)。
1353
1391
  // ⑤ 接口冒烟未跑(facts.smokeRan≠ran,仅判级 integration/deployment-critical 时计入,
@@ -1417,6 +1455,8 @@ export function evaluatePassEligibility(args) {
1417
1455
  const factsMissing = !a.facts || typeof a.facts !== 'object'
1418
1456
  const facts = factsMissing ? {} : a.facts
1419
1457
  const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
1458
+ const handoverFieldMissing = !factsMissing && !(facts.handover && Array.isArray(facts.handover.items))
1459
+ const matrixPartialMissing = !factsMissing && facts.matrixPartialRows == null
1420
1460
  const blockingRows = handoverItems.filter(it => it && it.severity === 'blocking')
1421
1461
  const criticalLevel = a.changeRiskProfile && ['integration-critical', 'deployment-critical'].includes(a.changeRiskProfile.level)
1422
1462
  const NOTES_FIX = '改写结论为 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)'
@@ -1429,24 +1469,30 @@ export function evaluatePassEligibility(args) {
1429
1469
  )
1430
1470
  }
1431
1471
 
1432
- // ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES,不算失败)
1433
- if (factsMissing || facts.integrationRan === 'not-ran') {
1434
- triggered.push({ fact: 'integration-not-run', detail: factsMissing ? 'facts 缺失(fail-closed 按未跑处理)' : 'facts.integrationRan=not-ran' })
1472
+ // ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES,不算失败;
1473
+ // 字段不在场 fail-closed 按未跑处理——⑤同族口径,防存量底稿缺字段静默 PASS)
1474
+ if (factsMissing || facts.integrationRan !== 'ran') {
1475
+ const integrationDetail = factsMissing
1476
+ ? 'facts 缺失(fail-closed 按未跑处理)'
1477
+ : `facts.integrationRan=${facts.integrationRan == null ? '不在场(按未跑处理)' : facts.integrationRan}`
1478
+ triggered.push({ fact: 'integration-not-run', detail: integrationDetail })
1435
1479
  errors.push(
1436
- `[fact integration-not-run] 集成实测未跑(${factsMissing ? 'verify-facts.json 缺失,fail-closed 按未跑处理' : 'facts.integrationRan=not-ran'})——结论=PASS 须集成实测已跑(D-006 判定表:quality-scan 实测记录(skip 除外)或跨层回执条目)。` +
1437
- `出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts;仍未跑则${NOTES_FIX}承载(不算失败)。`
1480
+ `[fact integration-not-run] 集成实测未跑(${integrationDetail})——结论=PASS 须集成实测已跑(D-006 判定表:quality-scan 实测记录(skip 除外)或跨层回执条目)。` +
1481
+ `出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts(字段不在场多为存量底稿未含新字段,重跑即刷新);仍未跑则${NOTES_FIX}承载(不算失败)。`
1438
1482
  )
1439
1483
  }
1440
1484
 
1441
- // ② blocking 级移交项在场(blocking 移交项与结论 PASS 互斥)
1442
- if (factsMissing || blockingRows.length > 0) {
1485
+ // ② blocking 级移交项在场(blocking 移交项与结论 PASS 互斥;facts.handover 不在场=无法核对,fail-closed)
1486
+ if (factsMissing || handoverFieldMissing || blockingRows.length > 0) {
1443
1487
  const list = blockingRows.map(it => `${it.type || '未知类型'}|${String(it.item || '').slice(0, 60)}`).join(';')
1444
- triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : `${blockingRows.length} 条 blocking 行:${list}` })
1488
+ triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : handoverFieldMissing ? 'facts.handover 不在场(fail-closed 无法核对)' : `${blockingRows.length} 条 blocking 行:${list}` })
1445
1489
  errors.push(
1446
1490
  `[fact blocking-handover-present] ${factsMissing
1447
1491
  ? '移交项 blocking 级行无法核对(verify-facts.json 缺失,fail-closed 按在场触发)'
1448
- : `移交项含 ${blockingRows.length} 条 blocking 级行——承认有必须兜底的未竟事项即不是 PASS(④管「有去向」、②管「去向是否 blocking」)`}。` +
1449
- `触发行:${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : list}。修复:${NOTES_FIX};确属 advisory 的按降级文法「(降级:<理由>,依据 <file:line 或 D-xxx>)」显式降级。`
1492
+ : handoverFieldMissing
1493
+ ? '移交项 blocking 级行无法核对(facts.handover 不在场——存量底稿未含该字段或管线故障,fail-closed 拦下;重跑 verify-probes 幂等刷新 facts 即可消解)'
1494
+ : `移交项含 ${blockingRows.length} 条 blocking 级行——承认有必须兜底的未竟事项即不是 PASS(④管「有去向」、②管「去向是否 blocking」)`}。` +
1495
+ `触发行:${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : (list || '(handover 不可核对,无行可列)')}。修复:${NOTES_FIX};确属 advisory 的按降级文法「(降级:<理由>,依据 <file:line 或 D-xxx>)」显式降级。`
1450
1496
  )
1451
1497
  }
1452
1498
 
@@ -1462,11 +1508,18 @@ export function evaluatePassEligibility(args) {
1462
1508
  )
1463
1509
  }
1464
1510
 
1465
- // ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity——部分实现必须有移交去向)
1466
- if (factsMissing || ((facts.matrixPartialRows || 0) > 0 && handoverItems.length === 0)) {
1467
- triggered.push({ fact: 'matrix-partial-no-handover', detail: factsMissing ? 'facts 缺失(fail-closed)' : `matrixPartialRows=${facts.matrixPartialRows} 且 handover 零有效行` })
1511
+ // ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity——部分实现必须有移交去向;
1512
+ // matrixPartialRows 不在场=无法核对矩阵,fail-closed 按含 partial 处理,触发前提仍是零有效移交行——
1513
+ // 有真移交行在场即「去向已承载」,不因字段缺失误拦)
1514
+ if (factsMissing || ((matrixPartialMissing || facts.matrixPartialRows > 0) && handoverItems.length === 0)) {
1515
+ const matrixDesc = factsMissing
1516
+ ? '(facts 缺失,无法核对——fail-closed)'
1517
+ : matrixPartialMissing
1518
+ ? '(facts.matrixPartialRows 不在场,无法核对——fail-closed 按含 partial 处理;重跑 verify-probes 刷新 facts 即可消解)'
1519
+ : `${facts.matrixPartialRows} 行 partial/uncovered`
1520
+ triggered.push({ fact: 'matrix-partial-no-handover', detail: factsMissing ? 'facts 缺失(fail-closed)' : matrixPartialMissing ? 'facts.matrixPartialRows 不在场(fail-closed 按含 partial 处理)且 handover 零有效行' : `matrixPartialRows=${facts.matrixPartialRows} 且 handover 零有效行` })
1468
1521
  errors.push(
1469
- `[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : `${facts.matrixPartialRows} 行 partial/uncovered`} 且「## 移交项(结构化)」零有效行——部分实现必须有移交去向。修复:${NOTES_FIX}承载(任意 severity 均可,blocking 级另受条件②约束)。`
1522
+ `[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${matrixDesc} 且「## 移交项(结构化)」零有效行——部分实现必须有移交去向。修复:${NOTES_FIX}承载(任意 severity 均可,blocking 级另受条件②约束)。`
1470
1523
  )
1471
1524
  }
1472
1525
 
@@ -531,6 +531,10 @@ created_at: <now-datetime>
531
531
  - ...
532
532
  \`\`\`
533
533
 
534
+ ### 触达域现行 FR(FR 索引注入,2026-09-18-fr-index-l1)
535
+ {FR_INDEX_DIGEST}
536
+ 写作纪律(防重复 FR——L1 索引实验的承接数据源):改写/取代上列已有行为 → 对应新 FR 块**必须加承接行**引用全局 id(如 \`承接: FR-core-engine-003\`,多个逗号分隔);全新行为 → 新 FR 块不加承接。承接行在归档时由 CLI 翻旧条目 superseded 并建取代链。
537
+
534
538
  ### requirements.md 格式要求
535
539
  \`\`\`markdown
536
540
  ---
@@ -548,11 +552,12 @@ created_at: <now-datetime>
548
552
 
549
553
  ### FR-01: 需求名称
550
554
  覆盖决策:D-001@v1, D-002@v1(如适用)
555
+ 承接: FR-<域>-NNN(如适用——改写/取代上列注入清单中的已有行为时必填,全新行为省略本行)
551
556
  Given 前提条件
552
557
  When 触发动作
553
558
  Then 期望结果
554
559
 
555
- (每个边界条件独立 GWT 块)
560
+ (每个边界条件独立 GWT 块;场景名行 \`#### 场景:X\` 会被归档索引用作摘要,建议保留)
556
561
 
557
562
  ## 非功能需求
558
563
  - 兼容性:...