sillyspec 3.33.0 → 3.33.2

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 (72) hide show
  1. package/.claude/skills/sillyspec-flow/SKILL.md +18 -10
  2. package/.github/workflows/ci.yml +11 -3
  3. package/AGENTS.md +3 -3
  4. package/README.md +2 -2
  5. package/assets/command-cards/flow.md +9 -5
  6. package/package.json +1 -1
  7. package/src/archive-delta.js +4 -3
  8. package/src/change-delete.js +6 -5
  9. package/src/contract-matrix.js +2 -1
  10. package/src/cross-placement.js +5 -1
  11. package/src/cross-repo-reconcile.js +4 -1
  12. package/src/db.js +8 -2
  13. package/src/decision-distill.js +22 -8
  14. package/src/decision-values.js +34 -0
  15. package/src/decisions-io.js +5 -2
  16. package/src/deleted-ledger.js +2 -1
  17. package/src/doctor-diagnostics.js +8 -3
  18. package/src/flow-draft.js +17 -10
  19. package/src/flow-parity.js +93 -5
  20. package/src/flow-review-pack.js +183 -0
  21. package/src/flow-review.js +45 -3
  22. package/src/flow.js +365 -109
  23. package/src/friction-ledger.js +2 -1
  24. package/src/friction-tally.js +8 -4
  25. package/src/handoff.js +2 -1
  26. package/src/hooks/worktree-guard.js +5 -5
  27. package/src/index.js +62 -21
  28. package/src/init.js +7 -30
  29. package/src/knowledge-hits.js +2 -1
  30. package/src/knowledge-vector.js +2 -1
  31. package/src/progress/consistency-doctor.js +5 -2
  32. package/src/progress/stage-machine.js +6 -2
  33. package/src/progress.js +19 -8
  34. package/src/quicklog.js +194 -9
  35. package/src/review-dispatch.js +4 -3
  36. package/src/review-tier.js +2 -1
  37. package/src/route-hindsight.js +4 -3
  38. package/src/run/bg-sync.js +10 -3
  39. package/src/run/command.js +14 -12
  40. package/src/run/complete-handlers.js +40 -37
  41. package/src/run/complete.js +12 -8
  42. package/src/run/gate-snapshot-ledger.js +2 -1
  43. package/src/run/gate-snapshot.js +102 -23
  44. package/src/run/gates.js +32 -8
  45. package/src/run/prompt.js +6 -5
  46. package/src/run/scan-profile.js +2 -1
  47. package/src/run/shared.js +109 -5
  48. package/src/run/stage.js +5 -4
  49. package/src/run/test-ledger.js +2 -1
  50. package/src/run/verify-quality-scan.js +11 -5
  51. package/src/runtime-paths.js +189 -0
  52. package/src/scan-refresh.js +4 -3
  53. package/src/scope-audit.js +13 -7
  54. package/src/sentinel-assertions.js +37 -0
  55. package/src/spec-sync.js +38 -22
  56. package/src/stage-contract.js +24 -2
  57. package/src/stage-review.js +3 -2
  58. package/src/stage-templates.js +3 -2
  59. package/src/stages/brainstorm.js +6 -5
  60. package/src/sync-noise.js +3 -2
  61. package/src/sync.js +71 -15
  62. package/src/task-done.js +2 -1
  63. package/src/task-review.js +7 -5
  64. package/src/task-tick.js +45 -1
  65. package/src/validate-artifacts.js +2 -1
  66. package/src/verify-draft.js +2 -1
  67. package/src/verify-postcheck.js +11 -10
  68. package/src/verify-probes.js +157 -52
  69. package/src/watcher.js +15 -9
  70. package/src/worktree-apply.js +2 -1
  71. package/src/write-audit.js +2 -1
  72. package/templates/agents-instruction.md +3 -3
@@ -25,6 +25,7 @@
25
25
  * 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
26
26
  */
27
27
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, copyFileSync } from 'fs'
28
+ import { runtimeStatePath } from './runtime-paths.js'
28
29
  import { join, dirname, basename, resolve, isAbsolute, relative } from 'path'
29
30
  import { gitQuiet, unquoteGitPath } from './git-helper.js'
30
31
  import { hasUnconfirmedPrefill } from './prefill.js'
@@ -1721,9 +1722,33 @@ const PROBE7_EXEC_RUN_ID_RE = /^exec-\d{4}-\d{2}-\d{2}-\d{6}(?:-[a-z0-9]{1,8}){0
1721
1722
  // 关键词提取:≥3 字符标识符([A-Za-z_][A-Za-z0-9_]{2,})∪ ≥2 字连续 CJK 片段,按出现顺序交错
1722
1723
  const PROBE7_TERM_RE = /[A-Za-z_][A-Za-z0-9_]{2,}|[\u4e00-\u9fff]{2,}/g
1723
1724
  const PROBE7_TERM_CAP = 5
1725
+ // 候选帽(2026-10-16-probe7-prefill-noise-fix FR-04 / 缺陷四):归属候选 >5 且无测试名级
1726
+ // 收窄命中 → 判定交人工(uncovered),禁止目录级候选长清单进单元格
1727
+ const PROBE7_CANDIDATE_CAP = 5
1724
1728
  // 坑 probe7-prefill-evidence(ql-20260915-004):无归属时的 non-testable 判别词(机械保守:
1725
- // acceptance 文本命中即预填 non-testable,agent 复核可改写)
1729
+ // acceptance 文本命中即预填 non-testable,agent 复核可改写)。
1730
+ // 2026-10-16-probe7-prefill-noise-fix FR-01:升级为「条目级直判」——命中即 non-testable
1731
+ // (无论归属在场与否),且卡内全部 acceptance 命中时跳过 FR 关联回归候选池注入
1732
+ // (缺陷一实证:compose/Dockerfile 卡经 FR 池灌单格 130+ 不相干测试文件)。
1726
1733
  const PROBE7_NONTESTABLE_RE = /文档|部署|doc|deploy|manual|config/i
1734
+ // 测试名行形态(FR-02 name 级命中判定):Python def test_* / class Test*;JS 系 test|it|describe
1735
+ // (含 .each/.only/.skip 等链式变体)调用行。命中行含验收词 → 名级命中;证据锚点取该行行号。
1736
+ const PROBE7_PY_TESTNAME_RE = /^\s*(?:async\s+)?def\s+(test_\w+)|^\s*class\s+(Test\w+)/
1737
+ const PROBE7_JS_TESTNAME_RE = /^\s*(?:test|it|describe)(?:\.[A-Za-z]+)*\s*\(/
1738
+
1739
+ /**
1740
+ * 行级测试名判定:返回测试名(Python def/class 名;JS 系取首个引号串,取不到以空串标记
1741
+ * 「名行但名不可提取」由调用方以 term 兜底);非测试名行返回 null。
1742
+ */
1743
+ function probe7TestNameOfLine(line) {
1744
+ const m = String(line).match(PROBE7_PY_TESTNAME_RE)
1745
+ if (m) return m[1] || m[2]
1746
+ if (PROBE7_JS_TESTNAME_RE.test(line)) {
1747
+ const q = String(line).match(/(['"`])([^'"`]{1,80})\1/)
1748
+ return q ? q[2] : ''
1749
+ }
1750
+ return null
1751
+ }
1727
1752
 
1728
1753
  /**
1729
1754
  * 解析 task-NN.md frontmatter 的 acceptance(string/array 双形态归一为数组)。
@@ -1788,7 +1813,7 @@ export function isProbe7TestPath(p) {
1788
1813
  */
1789
1814
  function resolveExecuteRunIdInline(runtimeRoot, changeName) {
1790
1815
  try {
1791
- const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1816
+ const marker = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
1792
1817
  if (existsSync(marker)) {
1793
1818
  const c = readFileSync(marker, 'utf8').trim()
1794
1819
  if (c && PROBE7_EXEC_RUN_ID_RE.test(c)) return c
@@ -1810,17 +1835,22 @@ function resolveExecuteRunIdInline(runtimeRoot, changeName) {
1810
1835
  }
1811
1836
 
1812
1837
  /**
1813
- * 读当前 runId 下某 task 的 review.json changedFiles 中 test/ 前缀路径(归属第二源)。
1838
+ * 读当前 runId 下某 task 的 review.json changedFiles 中测试文件路径(归属第二源)。
1814
1839
  * review.json 读不到 / JSON 非法 / changedFiles 非数组 → 空集不报错(提示面,宁缺毋噪)。
1840
+ * opts.wide(2026-10-16-probe7-prefill-noise-fix FR-03 / 同波次提升信号):过滤口径从
1841
+ * `test/` 前缀换为 isProbe7TestPath——覆盖嵌套 tests/ 目录形态(平台侧实证
1842
+ * sillyhub-daemon/tests/、agent/tests/ 前缀不交,窄口径漏)。缺省窄口径不变(结构归属面
1843
+ * 零行为变化;宽口径仅供 waveFiles 提升信号消费)。
1815
1844
  */
1816
- function readReviewChangedTestFiles(runtimeRoot, runId, taskId) {
1845
+ function readReviewChangedTestFiles(runtimeRoot, runId, taskId, opts = {}) {
1817
1846
  try {
1818
1847
  const review = JSON.parse(readFileSync(join(runtimeRoot, 'execute-runs', runId, 'tasks', taskId, 'review.json'), 'utf8'))
1819
1848
  if (!review || !Array.isArray(review.changedFiles)) return []
1849
+ const wide = !!(opts && opts.wide)
1820
1850
  return review.changedFiles
1821
1851
  .filter(f => typeof f === 'string')
1822
1852
  .map(f => f.split('\\').join('/'))
1823
- .filter(f => f.startsWith('test/'))
1853
+ .filter(f => wide ? isProbe7TestPath(f) : f.startsWith('test/'))
1824
1854
  } catch {
1825
1855
  return []
1826
1856
  }
@@ -1850,11 +1880,16 @@ function extractAcceptanceTerms(text) {
1850
1880
  * 坑 probe7-prefill-evidence(ql-20260915-004):anchors 逐词记录首命中 file:line(grep -n
1851
1881
  * 语义——首个包含该词的测试文件内的首行号),供证据列机械预填;terms/files 键维持旧形态
1852
1882
  * (既有消费方/测试零回归)。
1883
+ * 2026-10-16-probe7-prefill-noise-fix FR-02(缺陷二/三,supersedes 上述 anchors 契约):
1884
+ * 关键词首现行号锚点废除(实证指向 docstring 里的 config 字样 = 空心证据)——改为
1885
+ * nameAnchors:逐行判定测试名行(probe7TestNameOfLine),行含该词且为 def/it/test/describe
1886
+ * 名行才记 {term, file, line, name}(name 不可提取时以 term 兜底)。covered 只认名级命中;
1887
+ * terms/files 语义不变(正文级命中仍在场,判定侧封顶 partial)。
1853
1888
  * @param {Array<string>} testFiles 归属测试文件(仓根相对路径)
1854
1889
  * @param {string} cwd 主仓 cwd
1855
1890
  * @param {string|null} wtRoot worktree 根
1856
1891
  * @param {string[]} [crossRoots] 跨仓仓根(local.yaml repos 注册根,绝对路径)
1857
- * @returns {Record<number, {terms: string[], files: string[], anchors: Array<{term: string, file: string, line: number}>}>}
1892
+ * @returns {Record<number, {terms: string[], files: string[], nameAnchors: Array<{term: string, file: string, line: number, name: string}>}>}
1858
1893
  * 普通对象,可 JSON 序列化
1859
1894
  */
1860
1895
  function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot, crossRoots = []) {
@@ -1871,27 +1906,32 @@ function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot, crossRoot
1871
1906
  cache.set(rel, content)
1872
1907
  return content
1873
1908
  }
1874
- // 首命中行号(1-based):indexOf 前缀行计数,CRLF 不影响(按 \n 切)
1875
- const firstHitLine = (content, term) => {
1876
- const idx = content.indexOf(term)
1877
- return idx === -1 ? null : content.slice(0, idx).split('\n').length
1878
- }
1879
1909
  acceptanceItems.forEach((item, idx) => {
1880
1910
  const hitTerms = []
1881
1911
  const hitFiles = new Set()
1882
- const anchors = []
1912
+ const nameAnchors = []
1883
1913
  for (const term of extractAcceptanceTerms(item)) {
1884
- const hits = testFiles.filter(f => { const c = readTest(f); return !!c && c.includes(term) })
1885
- if (hits.length > 0) {
1886
- hitTerms.push(term)
1887
- for (const f of hits) hitFiles.add(f)
1888
- // 首命中文件内的首命中行(单文件 grep -n 首命中即可,机械保守)
1889
- const firstFile = hits[0]
1890
- const line = firstHitLine(readTest(firstFile) || '', term)
1891
- if (line !== null) anchors.push({ term, file: firstFile, line })
1914
+ let hit = false
1915
+ for (const f of testFiles) {
1916
+ const content = readTest(f)
1917
+ if (!content || !content.includes(term)) continue
1918
+ hit = true
1919
+ hitFiles.add(f)
1920
+ if (nameAnchors.some(a => a.file === f && a.term === term)) continue
1921
+ // 名级判定:首个「含该词且为测试名行」的行(行号 1 基;CRLF 不影响——按 \n 切)
1922
+ const lines = content.split('\n')
1923
+ for (let i = 0; i < lines.length; i++) {
1924
+ if (!lines[i].includes(term)) continue
1925
+ const name = probe7TestNameOfLine(lines[i])
1926
+ if (name !== null) {
1927
+ nameAnchors.push({ term, file: f, line: i + 1, name: name || term })
1928
+ break
1929
+ }
1930
+ }
1892
1931
  }
1932
+ if (hit) hitTerms.push(term)
1893
1933
  }
1894
- if (hitTerms.length > 0) hints[idx] = { terms: hitTerms, files: [...hitFiles], anchors }
1934
+ if (hitTerms.length > 0) hints[idx] = { terms: hitTerms, files: [...hitFiles], nameAnchors }
1895
1935
  })
1896
1936
  return hints
1897
1937
  }
@@ -1904,14 +1944,22 @@ function mdEscapeCell(text, cap = 200) {
1904
1944
  }
1905
1945
 
1906
1946
  /**
1907
- * 探针 7 判定/证据两列机械预填(坑 probe7-prefill-evidence,ql-20260915-004)。
1947
+ * 探针 7 判定/证据两列机械预填(坑 probe7-prefill-evidence,ql-20260915-004;
1948
+ * 2026-10-16-probe7-prefill-noise-fix 判定链重写)。
1908
1949
  *
1909
1950
  * 背景:此前骨架判定列 `<待填:四选一>` + 证据列 `<TODO>` 全占位,24 格矩阵 agent 全量 probe1-noqa
1910
- * 手填——CLI 已算出归属/关键词命中却弃之不用。预填规则(机械保守,agent 逐格复核改写):
1911
- * - 无归属 → uncovered;acceptance 文本含 文档/部署/doc/deploy/manual/config 类词且无归属
1912
- * → non-testable
1913
- * - 有归属且命中 ≥1 → covered,证据 = 首命中 `file:line`(term)锚点(≤3 条防膨胀)
1914
- * - 有归属零命中 → partial,证据 = 无机械命中提示 + 反引号归属文件(满足门禁测试锚点口径)
1951
+ * 手填——CLI 已算出归属/关键词命中却弃之不用。预填规则(机械保守,agent 逐格复核改写)。
1952
+ * 四缺陷实证(消费仓 backend-mem-polling verify 收口)后固定为七级优先链(FR-05):
1953
+ * ① 测试名级命中(nameAnchors)→ covered,锚点 = 测试函数名行(file:def行 行号)并引用
1954
+ * 测试名(缺陷二:正文关键词命中即 covered、锚点取 docstring 行号 = 空心证据,根除);
1955
+ * ② 条目命中部署/文档类词(PROBE7_NONTESTABLE_RE)→ non-testable(缺陷一:compose 卡
1956
+ * 不该经关键词链冒充承接);
1957
+ * ③ 无归属 → uncovered;
1958
+ * ④ 候选 > PROBE7_CANDIDATE_CAP(且无①收窄)→ uncovered「交人工」(缺陷四:长清单灌格);
1959
+ * ⑤ 正文级命中(terms 非空无 nameAnchors)→ partial + 待人工注记(FR-02 封顶);
1960
+ * ⑥ 同波次新增/修改归属测试(waveFiles)零字面命中 → covered + 待人工复核(缺陷三:
1961
+ * 真 covered 因同义改写字面不交被误标 partial——本波次 diff 面提升信号);
1962
+ * ⑦ 其余有归属零命中 → partial(现状文案)。
1915
1963
  *
1916
1964
  * 门禁兼容(stage-contract.js extractAcceptanceMatrixSlots / MATRIX_VERDICT_WHITELIST):判定列
1917
1965
  * 须为纯枚举(精确 trim 匹配白名单)——行内尾注「(预填,复核后可改)」会破坏枚举解析,故
@@ -1919,24 +1967,47 @@ function mdEscapeCell(text, cap = 200) {
1919
1967
  * non-testable 证据须非空——预填值均按此口径构造(预填即可过门,复核责任在段头注记明示)。
1920
1968
  * @param {string} item acceptance 条目文本
1921
1969
  * @param {string[]|undefined} testFiles 归属测试文件
1922
- * @param {{terms: string[], files: string[], anchors?: Array<{term: string, file: string, line: number}>}|null} hint
1970
+ * @param {{terms: string[], files: string[], nameAnchors?: Array<{term: string, file: string, line: number, name: string}>}|null} hint
1971
+ * @param {{waveFiles?: string[]}} [opts] waveFiles = 同波次新增/修改归属测试(提升信号)
1923
1972
  * @returns {{verdict: string, evidence: string}}
1924
1973
  */
1925
- function prefillMatrixCells(item, testFiles, hint) {
1974
+ function prefillMatrixCells(item, testFiles, hint, opts = {}) {
1975
+ const waveFiles = Array.isArray(opts && opts.waveFiles) ? opts.waveFiles : []
1926
1976
  const hasAttribution = Array.isArray(testFiles) && testFiles.length > 0
1927
- if (!hasAttribution) {
1928
- return {
1929
- verdict: PROBE7_NONTESTABLE_RE.test(String(item || '')) ? 'non-testable' : 'uncovered',
1930
- evidence: '(无归属测试)',
1931
- }
1977
+ const nameAnchors = (hint && Array.isArray(hint.nameAnchors)) ? hint.nameAnchors : []
1978
+ // ① 测试名级命中 → covered(锚点=测试函数名行 + 测试名引用)
1979
+ if (nameAnchors.length > 0) {
1980
+ const evidence = nameAnchors.slice(0, 3)
1981
+ .map(a => `\`${mdEscapeCell(a.file, 120)}:${a.line}\`(${mdEscapeCell(a.name, 60)})`)
1982
+ .join('、')
1983
+ return { verdict: 'covered', evidence }
1984
+ }
1985
+ // ② 部署/文档类词 → non-testable(条目级直判,归属在场与否不影响)
1986
+ if (PROBE7_NONTESTABLE_RE.test(String(item || ''))) {
1987
+ return { verdict: 'non-testable', evidence: '(部署/文档类条目——如实际有测试承接请改写)' }
1932
1988
  }
1933
- const anchors = (hint && Array.isArray(hint.anchors)) ? hint.anchors : []
1934
- if (anchors.length > 0) {
1935
- const evidence = anchors.slice(0, 3)
1936
- .map(a => `\`${mdEscapeCell(a.file, 120)}:${a.line}\`(${mdEscapeCell(a.term, 40)})`)
1989
+ // ③ 无归属
1990
+ if (!hasAttribution) {
1991
+ return { verdict: 'uncovered', evidence: '(无归属测试)' }
1992
+ }
1993
+ // ④ 候选超帽(无测试名级收窄)→ 交人工
1994
+ if (testFiles.length > PROBE7_CANDIDATE_CAP) {
1995
+ return { verdict: 'uncovered', evidence: `(候选 ${testFiles.length} 个——归属不可判定,交人工核验)` }
1996
+ }
1997
+ // ⑤ 正文级命中 → partial(封顶;不再冒充 covered,锚点不含关键词行号)
1998
+ if (hint && Array.isArray(hint.terms) && hint.terms.length > 0) {
1999
+ const first = (Array.isArray(hint.files) && hint.files.length > 0) ? hint.files[0] : testFiles[0]
2000
+ return { verdict: 'partial', evidence: `(正文级命中——待人工核验 \`${mdEscapeCell(first, 120)}\`)` }
2001
+ }
2002
+ // ⑥ 同波次新增/修改归属测试(零字面命中)→ covered + 待人工复核注记
2003
+ const wave = testFiles.filter(f => waveFiles.includes(f))
2004
+ if (wave.length > 0) {
2005
+ const evidence = wave.slice(0, 3)
2006
+ .map(f => `\`${mdEscapeCell(f, 120)}\`(同波次新增/修改——待人工复核)`)
1937
2007
  .join('、')
1938
2008
  return { verdict: 'covered', evidence }
1939
2009
  }
2010
+ // ⑦ 有归属零命中
1940
2011
  return {
1941
2012
  verdict: 'partial',
1942
2013
  evidence: `(无机械命中——人工核验 \`${mdEscapeCell(testFiles[0], 120)}\`)`,
@@ -1959,7 +2030,7 @@ export function renderProbe7Lines(p7) {
1959
2030
  return L
1960
2031
  }
1961
2032
  L.push('<!-- 口径注记:探针 3 = 模块目录递归存在性面(allowed_paths 目录附近有没有测试);探针 7 = allowed_paths ∪ review changedFiles ∪ 直接下游卡测试 结构归属承接面(每条 acceptance 由哪些测试承接;下游消费卡的测试可承接上游 provider 的 acceptance——probe7-provider-tests-in-consumer-card);两者并排冲突以 7 为准。判定枚举(五选一):covered / covered-service / partial / uncovered / non-testable(covered-service 适用:端点行为由 service 层等非端点层测试锁定,证据附测试锚点;non-testable 是文档/部署类显式逃生门)。关键词命中只是提示,命中≠判定。 -->')
1962
- L.push('<!-- 预填说明(ql-20260915-004):判定列为 CLI 机械预填,agent 逐格复核改写——规则:无归属→uncovered(文档/部署/doc/deploy/manual/config 类词→non-testable);有归属且命中≥1→covered;有归属零命中→partial。covered/covered-service/partial 证据须含测试锚点三形态之一(file:line / `.test.` 测试文件名 / 反引号包裹的路径或测试名),行号可省;uncovered/non-testable 证据自由形态。预填≠结论:与事实不符的格子必须改写(枚举须保持 covered/covered-service/partial/uncovered/non-testable 纯值,备注写在证据列)。 -->')
2033
+ L.push('<!-- 预填说明(ql-20260915-004;2026-10-16-probe7-prefill-noise-fix 判定链修订):判定列为 CLI 机械预填,agent 逐格复核改写——规则优先级:①测试名级命中(def test_* / it(/test(/describe( 名行含验收词)→covered(锚点=测试函数名行+测试名);②部署/文档/doc/deploy/manual/config 类词条目→non-testable;③无归属→uncovered;④候选>5 且无测试名级收窄→uncovered(归属不可判定交人工);⑤正文级命中→partial(待人工核验);⑥同波次新增/修改的归属测试零命中→covered(待人工复核);⑦其余有归属零命中→partial。covered/covered-service/partial 证据须含测试锚点三形态之一(file:line / `.test.` 测试文件名 / 反引号包裹的路径或测试名),行号可省;uncovered/non-testable 证据自由形态。预填≠结论:与事实不符的格子必须改写(枚举须保持 covered/covered-service/partial/uncovered/non-testable 纯值,备注写在证据列)。 -->')
1963
2034
  // 零自动化承接计数(坑 review-zero-coverage-unwalked,2026-09-16 EHS 二次复核实证:execute
1964
2035
  // 期 review 走查面事实上跟着测试覆盖走——「新增-部门支线测试充分,编辑路径与相关方支线
1965
2036
  // 零覆盖」恰是 5 个 P1 的藏身处。矩阵渲染时顺带计数零承接条目,verify 复核强制显式走查
@@ -1991,13 +2062,15 @@ export function renderProbe7Lines(p7) {
1991
2062
  // 探针 3 既有 slice 截断先例收敛为前 5 + 「共 N 个」折叠;路径经 mdEscapeCell 防竖线破表)
1992
2063
  const attribCell = (t.testFiles && t.testFiles.length > 0)
1993
2064
  ? t.testFiles.slice(0, 5).map(f => `\`${mdEscapeCell(f, 120)}\``).join('<br>')
1994
- + (t.testFiles.length > 5 ? `<br>…共 ${t.testFiles.length} 个(目录级候选全量在 facts/trace,归属按关键词命中与证据锚点收窄)` : '')
2065
+ + (t.testFiles.length > 5 ? `<br>…共 ${t.testFiles.length} 个(归属不可判定——候选全量在 facts/trace,交人工)` : '')
1995
2066
  : '无归属测试——判定大概率 uncovered'
1996
2067
  const h = (t.hints && t.hints[i]) || null
2068
+ // 提示列展示帽(2026-10-16-probe7-prefill-noise-fix FR-04):正文级命中可跨多文件,
2069
+ // 命中文件同样收敛前 5 + 「共 N 个」折叠——长清单只进 facts/trace,不进单元格
1997
2070
  const hintCell = h
1998
- ? `${h.terms.map(x => mdEscapeCell(x)).join('、')}(${(h.files || []).map(f => `\`${f}\``).join('、')})`
2071
+ ? `${h.terms.map(x => mdEscapeCell(x)).join('、')}(${(h.files || []).slice(0, 5).map(f => `\`${mdEscapeCell(f, 120)}\``).join('、')}${(h.files || []).length > 5 ? `…共 ${h.files.length} 个` : ''})`
1999
2072
  : '—'
2000
- const pre = prefillMatrixCells(item, t.testFiles, h)
2073
+ const pre = prefillMatrixCells(item, t.testFiles, h, { waveFiles: t.waveFiles || [] })
2001
2074
  if (pre.verdict === 'uncovered' || pre.verdict === 'partial') zeroCoverage++
2002
2075
  L.push(`| ${mdEscapeCell(item)} | ${attribCell} | ${hintCell} | ${pre.verdict} | ${pre.evidence} |`)
2003
2076
  })
@@ -2244,8 +2317,13 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
2244
2317
  .map(p => String(p).replace(/^NEW:\s*/, '').trim())
2245
2318
  .filter(p => p && isProbe7TestPath(p))
2246
2319
  const fromReview = runId ? readReviewChangedTestFiles(runtimeRoot, runId, card.task) : []
2320
+ // 同波次提升信号(2026-10-16-probe7-prefill-noise-fix FR-03 / 缺陷三):review changedFiles
2321
+ // 的测试形态路径(wide 口径覆盖嵌套 tests/)——与结构归属分离保存,仅作「零字面命中 →
2322
+ // covered+待人工复核」的提升信号(prefillMatrixCells 第⑥级),不改变归属集本身。
2323
+ const fromReviewWave = runId ? readReviewChangedTestFiles(runtimeRoot, runId, card.task, { wide: true }) : []
2247
2324
  // 直接下游卡的归属测试并入(provider acceptance 由消费卡测试承接)
2248
2325
  const fromDependents = []
2326
+ const fromDependentsWave = []
2249
2327
  for (const other of cards) {
2250
2328
  if (other.task === card.task) continue
2251
2329
  const deps = dependsOnOf.get(other.task) || []
@@ -2254,13 +2332,24 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
2254
2332
  const t = String(p).replace(/^NEW:\s*/, '').trim()
2255
2333
  if (t && isProbe7TestPath(t) && !fromDependents.includes(t)) fromDependents.push(t)
2256
2334
  }
2335
+ // 下游卡同波次测试并入提升信号(provider acceptance 由消费卡本波次测试承接,同跨卡口径)
2336
+ if (runId) {
2337
+ for (const f of readReviewChangedTestFiles(runtimeRoot, runId, other.task, { wide: true })) {
2338
+ if (!fromDependentsWave.includes(f)) fromDependentsWave.push(f)
2339
+ }
2340
+ }
2257
2341
  }
2258
2342
  // 收集帽(坑 probe-noise-and-commit-suggest-quality 坑A):目录级 allowed_paths 递归可卷
2259
2343
  // 入整仓测试树(实证 82 个)——去重后截前 20(展示层另有 5 个硬帽;hints/trace 消费同集)
2260
2344
  const testFiles = [...new Set([...fromAllowed, ...fromReview, ...fromDependents])].slice(0, 20)
2345
+ // 全条目命中部署/文档类词的卡(docs/deploy 卡)不注入 FR 候选池(2026-10-16-probe7-prefill-noise-fix
2346
+ // FR-01 / 缺陷一:compose/Dockerfile 卡经 FR 回退池整池 join,单格灌 130+ 不相干测试文件)。
2347
+ // 代码卡(含至少一条非部署/文档类条目)注入行为不变。
2348
+ const allNonTestableEntries = card.acceptance.length > 0
2349
+ && card.acceptance.every(a => PROBE7_NONTESTABLE_RE.test(String(a || '')))
2261
2350
  // 无归属卡注入 FR 关联回归既有用例候选(task-02;有归属卡 existingTests 恒空——零噪音)
2262
2351
  const existingTests = []
2263
- if (frLinkedExisting && testFiles.length === 0) {
2352
+ if (frLinkedExisting && testFiles.length === 0 && !allNonTestableEntries) {
2264
2353
  for (const f of frLinkedExisting.files) {
2265
2354
  if (isProbe7TestPath(f) && !existingTests.includes(f)) existingTests.push(f)
2266
2355
  }
@@ -2272,6 +2361,7 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
2272
2361
  fmError: card.fmError || null,
2273
2362
  testFiles: finalTestFiles,
2274
2363
  existingTests,
2364
+ waveFiles: [...new Set([...fromReviewWave, ...fromDependentsWave])],
2275
2365
  hints: buildAcceptanceHints(card.acceptance, finalTestFiles, cwd, wtRoot, probe7CrossRoots),
2276
2366
  })
2277
2367
  }
@@ -3041,7 +3131,7 @@ function classifyReceiptCommandSource(command, sourceMark) {
3041
3131
  /**
3042
3132
  * facts.integrationRan 判定(D-006 判定表,2026-09-17-pass-cap-semantics task-01):
3043
3133
  * 已跑 = ①quality-scan 实测记录在场(路径规则同 run/verify-quality-scan.js:36
3044
- * qualityScanRecordPath:specBase/.runtime/verify-quality-scan-<changeName>.json,specBase/
3134
+ * qualityScanRecordPath:specBase/.runtime/state/verify-quality-scan-<changeName>.json,specBase/
3045
3135
  * changeName 自 factsPath 目录上推,不依赖调用方传参)且 commands.test 实跑(status 非
3046
3136
  * skipped)且 test_strategy ∈ {full, module, evidence-auto}(evidence-auto 降级 module 已跑
3047
3137
  * 子集按 module 档算已跑;local.yaml 未配置默认 full);或 ②回执槽存在命令来源含跨层调用的
@@ -3057,7 +3147,7 @@ function judgeIntegrationRan(factsPath, runtimeEvidence) {
3057
3147
  const specBase = dirname(dirname(changeDir))
3058
3148
  let record = null
3059
3149
  try {
3060
- const rec = JSON.parse(readFileSync(join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`), 'utf8'))
3150
+ const rec = JSON.parse(readFileSync(runtimeStatePath(join(specBase, '.runtime'), `verify-quality-scan-${changeName}.json`), 'utf8'))
3061
3151
  if (rec && rec.schemaVersion === 1 && rec.source === 'cli-noai') record = rec
3062
3152
  } catch { record = null }
3063
3153
  let strategy = 'full' // 未配置 test_strategy 默认全量(verify-postcheck resolveTestStrategy 同口径)
@@ -3076,7 +3166,7 @@ function judgeIntegrationRan(factsPath, runtimeEvidence) {
3076
3166
  }
3077
3167
  const notes = []
3078
3168
  if (!record) {
3079
- notes.push(`ℹ️ quality-scan 实测记录缺失(${join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`)})——facts.integrationRan 按 not-ran 判定(X-01 fail-open 注记,不阻断;--done 亲测替代扫描的场景记录时序不可得,出路=重跑质量扫描步或降级 NOTES)`)
3169
+ notes.push(`ℹ️ quality-scan 实测记录缺失(${runtimeStatePath(join(specBase, '.runtime'), `verify-quality-scan-${changeName}.json`)})——facts.integrationRan 按 not-ran 判定(X-01 fail-open 注记,不阻断;--done 亲测替代扫描的场景记录时序不可得,出路=重跑质量扫描步或降级 NOTES)`)
3080
3170
  }
3081
3171
  return { ran: 'not-ran', notes }
3082
3172
  }
@@ -3351,7 +3441,7 @@ export function ensureApiCoverageMatrixSection(mdPath, apiFace) {
3351
3441
 
3352
3442
  // ============ smoke 回执槽机器段(2026-09-17-api-coverage-smoke task-02 / FR-03,design §3) ============
3353
3443
  //
3354
- // 唯一事实源 = .runtime/verify-quality-scan-<change>.json 的 smokeResult 段(task-01 契约,
3444
+ // 唯一事实源 = .runtime/state/verify-quality-scan-<change>.json 的 smokeResult 段(task-01 契约,
3355
3445
  // R-06 双源兜底——md 被篡改不影响 facts 推导);本组函数把该记录 ensure 式渲染成
3356
3446
  // verify-result.md「## 集成验证回执」槽内 CLI 拥有的机器段行(source: cli-noai-smoke 标注
3357
3447
  // 识别),agent 只可追加段、不可改写机器段(改写/删除由 verify-postcheck
@@ -3365,12 +3455,12 @@ const SMOKE_ABSENT_LINE_RE = /^<!--\s*smoke 机器段缺态:(not-configured|no
3365
3455
 
3366
3456
  /**
3367
3457
  * 读 quality-scan 记录的 smokeResult 段(路径规则同 judgeIntegrationRan:specBase/
3368
- * .runtime/verify-quality-scan-<changeName>.json)。共享读取点——ensure 注入与 postcheck
3458
+ * .runtime/state/verify-quality-scan-<changeName>.json)。共享读取点——ensure 注入与 postcheck
3369
3459
  * 一致性对比同源取数,不各读各的。记录缺失/损坏/无 smokeResult 段 → null。
3370
3460
  */
3371
3461
  export function readSmokeResultRecord(specBase, changeName) {
3372
3462
  try {
3373
- const rec = JSON.parse(readFileSync(join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`), 'utf8'))
3463
+ const rec = JSON.parse(readFileSync(runtimeStatePath(join(specBase, '.runtime'), `verify-quality-scan-${changeName}.json`), 'utf8'))
3374
3464
  if (rec && rec.schemaVersion === 1 && rec.source === 'cli-noai'
3375
3465
  && rec.smokeResult && typeof rec.smokeResult === 'object') {
3376
3466
  return rec.smokeResult
@@ -3407,7 +3497,7 @@ function judgeSmokeRan(factsPath) {
3407
3497
  const changeDir = dirname(factsPath)
3408
3498
  const changeName = basename(changeDir)
3409
3499
  const specBase = dirname(dirname(changeDir))
3410
- const recordPath = join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`)
3500
+ const recordPath = runtimeStatePath(join(specBase, '.runtime'), `verify-quality-scan-${changeName}.json`)
3411
3501
  const smoke = readSmokeResultRecord(specBase, changeName)
3412
3502
  if (smoke) {
3413
3503
  if (smoke.configured === false) return { value: 'not-configured', notes: [] }
@@ -3491,7 +3581,7 @@ export function ensureSmokeReceiptSection(mdPath, opts = {}) {
3491
3581
  if (state === 'passed') {
3492
3582
  const command = String(smoke.command || '')
3493
3583
  const exitVal = typeof smoke.exitCode === 'number' ? smoke.exitCode : ''
3494
- const noteLine = `<!-- smoke 机器段(CLI ensure 注入,source: ${SMOKE_RECEIPT_SOURCE_MARK})——唯一事实源是 .runtime/verify-quality-scan-${changeName}.json 的 smokeResult,agent 不可改写/删除(verify 一致性抽查会打回);ranAt 实录:${smoke.ranAt || '未知'},durationMs:${typeof smoke.durationMs === 'number' ? smoke.durationMs : '未知'} -->`
3584
+ const noteLine = `<!-- smoke 机器段(CLI ensure 注入,source: ${SMOKE_RECEIPT_SOURCE_MARK})——唯一事实源是 .runtime/state/verify-quality-scan-${changeName}.json 的 smokeResult,agent 不可改写/删除(verify 一致性抽查会打回);ranAt 实录:${smoke.ranAt || '未知'},durationMs:${typeof smoke.durationMs === 'number' ? smoke.durationMs : '未知'} -->`
3495
3585
  targetLines = /[||]/.test(command)
3496
3586
  ? [
3497
3587
  noteLine,
@@ -3808,12 +3898,27 @@ function mergeProbeSection(oldLines, freshLines) {
3808
3898
  const cell = l.split('|')[1]
3809
3899
  return cell ? cell.trim() : null
3810
3900
  }
3901
+ // 占位/分隔单元格(坑 carry-orphan-rows,2026-10-16 下游实证):全占位行(所有单元格均为
3902
+ // 横线族/占位词/空)被当「已填行」携载,成为夹在报告中部、被门禁栽给最近 task 锚的孤儿
3903
+ // 残行。横线族含 em/en-dash 等全角形态;占位词不区分大小写。部分占位的真实行(有任一
3904
+ // 非占位格)不受影响,照常携载。
3905
+ const isPlaceholderCell = (c) => {
3906
+ const t = String(c ?? '').trim()
3907
+ if (t === '') return true
3908
+ if (/^[-–—―−\s]*$/.test(t)) return true
3909
+ return /^(?:待填|tbd|todo)$/i.test(t)
3910
+ }
3811
3911
  const oldFilled = new Map()
3812
3912
  for (const l of oldLines) {
3813
3913
  if (!/^\s*\|/.test(l)) continue
3814
3914
  if (/<待填|<TODO/.test(l)) continue
3815
3915
  const key = firstCell(l)
3816
- if (!key || /^-{3,}$/.test(key)) continue // 分隔行/空键不携载
3916
+ if (!key) continue // 空键不携载
3917
+ if (/^:?-{3,}:?$/.test(key)) continue // 分隔行(含 :--- / :---: 冒号对齐形态,与门禁 splitMatrixRowCells 文法同源)
3918
+ const cells = l.split('|').slice(1).map((c) => c.trim())
3919
+ if (cells.length > 0 && cells[cells.length - 1] === '') cells.pop() // 行尾管道的收尾空段
3920
+ if (cells.length === 5 && cells[3] === '判定' && cells[4] === '证据') continue // 表头行不是「已填行」(评审 P3-3:漂移键旧表头携载进注释块会被门禁表头武装重新武装)
3921
+ if (cells.length > 0 && cells.every(isPlaceholderCell)) continue // 全占位行(旧模板残形)不携载
3817
3922
  oldFilled.set(key, l)
3818
3923
  }
3819
3924
  if (oldFilled.size === 0) return { text: freshLines.join('\n'), carried: 0 }
package/src/watcher.js CHANGED
@@ -44,6 +44,7 @@ import { basename, dirname, join, resolve } from 'path';
44
44
  import { gitQuiet } from './git-helper.js';
45
45
  import { normalizePath, globMatch } from './change-list.js';
46
46
  import { resolveTestFileOwners } from './test-bindings.js';
47
+ import { runtimeStatePath, runtimeTempPath } from './runtime-paths.js';
47
48
 
48
49
  export const WATCHER_LOCK_FILENAME = 'watcher.lock';
49
50
  const WATCHER_LOG_FILENAME = 'watcher.log';
@@ -95,7 +96,8 @@ export function readWatcherLock(runtimeRoot) {
95
96
 
96
97
  function writeWatcherLock(runtimeRoot, lock) {
97
98
  mkdirSync(runtimeRoot, { recursive: true });
98
- const tmp = join(runtimeRoot, `${WATCHER_LOCK_FILENAME}.${process.pid}.tmp`);
99
+ try { mkdirSync(join(runtimeRoot, 'temp'), { recursive: true }); } catch { /* 锁写入 fail-open */ }
100
+ const tmp = runtimeTempPath(runtimeRoot, `${WATCHER_LOCK_FILENAME}.${process.pid}.tmp`);
99
101
  writeFileSync(tmp, JSON.stringify(lock) + '\n', 'utf8');
100
102
  renameSync(tmp, join(runtimeRoot, WATCHER_LOCK_FILENAME));
101
103
  }
@@ -280,7 +282,7 @@ export function buildSnapshot({ changeDir, cwd, runtimeRoot, changeName, readFil
280
282
  const out = gitQuiet(cwd, ['status', '--porcelain']);
281
283
  snap.dirtyCode = out == null ? null : parsePorcelainCodePaths(out);
282
284
  }
283
- const scanPath = join(runtimeRoot, `verify-quality-scan-${changeName}.json`);
285
+ const scanPath = runtimeStatePath(runtimeRoot, `verify-quality-scan-${changeName}.json`);
284
286
  try {
285
287
  const st = statSyncImpl(scanPath);
286
288
  snap.scan = { mtimeMs: Math.round(st.mtimeMs), size: st.size };
@@ -426,7 +428,7 @@ function newCommitsBetween(prev, next) {
426
428
  // append、内容去重不空转;archived 首拍早退路径不读不写。
427
429
 
428
430
  export function watcherSnapshotPath(runtimeRoot, changeName) {
429
- return join(runtimeRoot, `watcher-last-snapshot-${changeName}.json`);
431
+ return runtimeStatePath(runtimeRoot, `watcher-last-snapshot-${changeName}.json`);
430
432
  }
431
433
 
432
434
  /** 读水位(损坏/缺失/形状不对 → null)。 */
@@ -447,6 +449,7 @@ export function writeSnapshotWatermark(runtimeRoot, changeName, snap, cacheObj =
447
449
  try {
448
450
  const json = JSON.stringify(sentinel ? { ...snap, sentinel } : snap);
449
451
  if (cacheObj && cacheObj.last === json) return { written: false };
452
+ try { mkdirSync(join(runtimeRoot, 'state'), { recursive: true }); } catch { /* fail-open */ }
450
453
  writeFileSync(watcherSnapshotPath(runtimeRoot, changeName), json);
451
454
  if (cacheObj) cacheObj.last = json;
452
455
  return { written: true };
@@ -461,7 +464,7 @@ export function writeSnapshotWatermark(runtimeRoot, changeName, snap, cacheObj =
461
464
  // 子进程侧 appendFileSync 落盘名与此处读取名同锚(runWatcherFromEnv 内联字符串的唯一对应)。
462
465
 
463
466
  export function watcherEventsPath(runtimeRoot, changeName) {
464
- return join(runtimeRoot, `watcher-events-${changeName}.jsonl`);
467
+ return runtimeStatePath(runtimeRoot, `watcher-events-${changeName}.jsonl`);
465
468
  }
466
469
 
467
470
  /**
@@ -838,7 +841,8 @@ export async function pushEventsToPlatform({ specBase, changeName, events, env =
838
841
  /** 子进程起步截尾日志(>1MB 保后 512KB,整行边界起)。失败只影响日志体积。 */
839
842
  function truncateLogIfNeeded(runtimeRoot) {
840
843
  try {
841
- const p = join(runtimeRoot, WATCHER_LOG_FILENAME);
844
+ const p = runtimeTempPath(runtimeRoot, WATCHER_LOG_FILENAME);
845
+ try { mkdirSync(join(runtimeRoot, 'temp'), { recursive: true }); } catch { return; }
842
846
  if (statSync(p).size <= LOG_TRUNCATE_THRESHOLD) return;
843
847
  const buf = readFileSync(p);
844
848
  const keep = buf.subarray(Math.max(0, buf.length - LOG_TRUNCATE_KEEP));
@@ -869,13 +873,14 @@ export async function spawnWatcher(cwd, changeName, opts = {}) {
869
873
  const runtimeRoot = resolveRuntimeRoot(opts, specBase);
870
874
 
871
875
  const lock = readWatcherLock(runtimeRoot);
872
- if (isWatcherLeaseLive(lock)) return { status: 'coalesced', logPath: join(runtimeRoot, WATCHER_LOG_FILENAME) };
876
+ if (isWatcherLeaseLive(lock)) return { status: 'coalesced', logPath: runtimeTempPath(runtimeRoot, WATCHER_LOG_FILENAME) };
873
877
 
874
- const logPath = join(runtimeRoot, WATCHER_LOG_FILENAME);
878
+ const logPath = runtimeTempPath(runtimeRoot, WATCHER_LOG_FILENAME);
875
879
  let stdio = ['ignore', 'ignore', 'ignore'];
876
880
  let logFd = null;
877
881
  try {
878
882
  mkdirSync(runtimeRoot, { recursive: true });
883
+ mkdirSync(join(runtimeRoot, 'temp'), { recursive: true }); // temp/ 可整体删除,写前重建
879
884
  logFd = openSync(logPath, 'a');
880
885
  stdio = ['ignore', logFd, logFd];
881
886
  } catch {
@@ -929,8 +934,9 @@ export async function runWatcherFromEnv(env = process.env, opts = {}) {
929
934
  writeWatcherLock(runtimeRoot, { pid: process.pid, change: changeName, startedAt, heartbeatAt: startedAt });
930
935
  console.log(`[watcher] 监听开始 ${new Date().toISOString()}: ${changeName}`);
931
936
 
932
- const eventsPath = join(runtimeRoot, `watcher-events-${changeName}.jsonl`);
933
- const timingPath = join(runtimeRoot, `watcher-stage-timing-${changeName}.json`);
937
+ const eventsPath = runtimeStatePath(runtimeRoot, `watcher-events-${changeName}.jsonl`);
938
+ const timingPath = runtimeStatePath(runtimeRoot, `watcher-stage-timing-${changeName}.json`);
939
+ try { mkdirSync(join(runtimeRoot, 'state'), { recursive: true }); } catch { /* fail-open:写入侧自抛 */ }
934
940
  let allEvents = [];
935
941
 
936
942
  const refreshHeartbeat = () => {
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync, readdirSync, readFileSync, mkdirSync } from 'fs';
16
+ import { runtimeStatePath } from './runtime-paths.js'
16
17
  import { join, resolve } from 'path';
17
18
  import { tmpdir } from 'os';
18
19
  import { createHash } from 'crypto';
@@ -2179,7 +2180,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
2179
2180
  result.commitPathspec = commitFiles;
2180
2181
  const runtimeRoot = join(projectRoot, '.sillyspec', '.runtime');
2181
2182
  mkdirSync(runtimeRoot, { recursive: true });
2182
- const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`);
2183
+ const pathspecFile = runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`);
2183
2184
  writeFileSync(pathspecFile, commitFiles.join('\n') + '\n');
2184
2185
  result.pathspecFile = pathspecFile;
2185
2186
  } catch { /* pathspec 落盘失败不影响 apply 结果 */ }
@@ -12,6 +12,7 @@
12
12
  * 各自整行写入,Windows 下最坏交错按行丢失计,不损主行为)。
13
13
  */
14
14
  import { appendFileSync, mkdirSync } from 'fs'
15
+ import { runtimeTempPath } from './runtime-paths.js'
15
16
  import { join, dirname } from 'path'
16
17
 
17
18
  const CRLF_BUF = Buffer.from('\r\n')
@@ -25,7 +26,7 @@ const CRLF_BUF = Buffer.from('\r\n')
25
26
  */
26
27
  export function appendWriteAudit(specDir, record) {
27
28
  try {
28
- const p = join(specDir, '.runtime', 'write-audit.jsonl')
29
+ const p = runtimeTempPath(join(specDir, '.runtime'), 'write-audit.jsonl')
29
30
  mkdirSync(dirname(p), { recursive: true })
30
31
  appendFileSync(p, JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n', 'utf8')
31
32
  return true
@@ -6,7 +6,7 @@
6
6
  ## 选道(动手前先对号入座)
7
7
  | 需求形态 | 走法 |
8
8
  |---|---|
9
- | 需求已含决策(改什么、成功标准说得清——成功标准须来自**用户原话或既有文档转写**;要靠你推断拟出的标准=需求不清晰) | **轻量变更(默认快道)**:`flow start` → 直接干活 → `flow done`(2 次协议调用;操作细节见 `/sillyspec:flow` skill 与 flow start 输出) |
9
+ | 需求已含决策(改什么、成功标准说得清——成功标准须来自**用户原话或既有文档转写**;要靠你推断拟出的标准=需求不清晰) | **轻量变更(默认快道)**:`flow start` → 补 spec 面(FR 绑定+design 四节)→ 批准(`flow approve` / 用户显式 `--autopilot`)→ 干活 → `flow done`(协议调用仍 2 次,批准由用户运行;操作细节见 `/sillyspec:flow` skill 与 flow start 输出) |
10
10
  | 用户显式要求极速(「极速/别验证/只要 spec 和代码」类原话)且需求已含决策 | **轻量变更极速版**:同上行加 `--turbo`(`flow start --change <名> --input "<...>" --turbo`)——**仅用户显式触发,禁止你自行启用**;收口跳过实测与重仪式门(hunk/dirty/ui 探针/评审),spec 结构校验/tasks 全勾/ledger/patch 冻结/distill/归档照常(跳的是验证不是证据);升厚即失效恢复全量仪式;收口输出显式标注「不含测试客观核验」。预段组合极速:头脑风暴预段走轻量跑判定+单轮合并问询+burst 一次 --done,收编时按用户要求带 --turbo |
11
11
  | 需求不清晰 / 需要方案探索(含「成功标准要你替用户拟」的形态——自己拟标准再自答是把默认共识伪装成共识) | 头脑风暴预段:`run brainstorm --change <名>` → 完成后 `flow start --change <名>` 收编续跑(design 以头脑风暴版为准)。上个轻量变更被 route-hindsight 标记时全新 start 会被同意门拦下要求显式选道:确认已含决策带 `--no-prestage` 声明留痕,否则先走本行预段 |
12
12
  | 大改动:跨模块取舍 / Wave 计划编排 / 多阶段治理 / 设计期人机对抗 | 完整流程五阶段:`run brainstorm → plan → execute → verify → archive`(每阶段一次渲染 + 一次 --done 收口;细节见各阶段 skill 与 CLI 输出) |
@@ -15,7 +15,7 @@
15
15
  `--input` 过门格式(轻量道首次调用即过,勿靠报错学;引号内换行合法,下例可照抄):
16
16
 
17
17
  ```bash
18
- sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景
18
+ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,必须落在 10-30 字硬区间、建议 ~20 字——不合区间 flow start 拒启);随后动机与背景
19
19
 
20
20
  成功标准:
21
21
  - <可验证标准>"
@@ -31,7 +31,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
31
31
  ## 核心规则
32
32
  1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
33
33
  2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md(如有))或现有代码依据,无依据不改。
34
- 3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。测试按层适用:纯逻辑先写测试,行为契约断言用户可见行为,视觉样式与纯配置/文档不写测试(记「不适用:理由」,验收靠规范与人工)。
34
+ 3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档(轻量道即:FR 绑定+design 四节+用户批准先于动码,读代码探究不算动码)。测试按层适用:纯逻辑先写测试,行为契约断言用户可见行为,视觉样式与纯配置/文档不写测试(记「不适用:理由」,验收靠规范与人工)。
35
35
  4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`。以落盘文件与测试结果为准,不信口头"已完成"。
36
36
  5. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
37
37
  6. **git hook 拦截提交时禁止跳过**(如 `.husky/pre-push`),修复问题后再提交。