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.
- package/.claude/skills/sillyspec-flow/SKILL.md +18 -10
- package/.github/workflows/ci.yml +11 -3
- package/AGENTS.md +3 -3
- package/README.md +2 -2
- package/assets/command-cards/flow.md +9 -5
- package/package.json +1 -1
- package/src/archive-delta.js +4 -3
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/cross-repo-reconcile.js +4 -1
- package/src/db.js +8 -2
- package/src/decision-distill.js +22 -8
- package/src/decision-values.js +34 -0
- package/src/decisions-io.js +5 -2
- package/src/deleted-ledger.js +2 -1
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +17 -10
- package/src/flow-parity.js +93 -5
- package/src/flow-review-pack.js +183 -0
- package/src/flow-review.js +45 -3
- package/src/flow.js +365 -109
- package/src/friction-ledger.js +2 -1
- package/src/friction-tally.js +8 -4
- package/src/handoff.js +2 -1
- package/src/hooks/worktree-guard.js +5 -5
- package/src/index.js +62 -21
- package/src/init.js +7 -30
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- package/src/progress/consistency-doctor.js +5 -2
- package/src/progress/stage-machine.js +6 -2
- package/src/progress.js +19 -8
- package/src/quicklog.js +194 -9
- package/src/review-dispatch.js +4 -3
- package/src/review-tier.js +2 -1
- package/src/route-hindsight.js +4 -3
- package/src/run/bg-sync.js +10 -3
- package/src/run/command.js +14 -12
- package/src/run/complete-handlers.js +40 -37
- package/src/run/complete.js +12 -8
- package/src/run/gate-snapshot-ledger.js +2 -1
- package/src/run/gate-snapshot.js +102 -23
- package/src/run/gates.js +32 -8
- package/src/run/prompt.js +6 -5
- package/src/run/scan-profile.js +2 -1
- package/src/run/shared.js +109 -5
- package/src/run/stage.js +5 -4
- package/src/run/test-ledger.js +2 -1
- package/src/run/verify-quality-scan.js +11 -5
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +13 -7
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +38 -22
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +3 -2
- package/src/stage-templates.js +3 -2
- package/src/stages/brainstorm.js +6 -5
- package/src/sync-noise.js +3 -2
- package/src/sync.js +71 -15
- package/src/task-done.js +2 -1
- package/src/task-review.js +7 -5
- package/src/task-tick.js +45 -1
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +11 -10
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +2 -1
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +3 -3
package/src/verify-probes.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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[],
|
|
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
|
|
1912
|
+
const nameAnchors = []
|
|
1883
1913
|
for (const term of extractAcceptanceTerms(item)) {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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],
|
|
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
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
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[],
|
|
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
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
-
|
|
1934
|
-
if (
|
|
1935
|
-
|
|
1936
|
-
|
|
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
|
|
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}
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
876
|
+
if (isWatcherLeaseLive(lock)) return { status: 'coalesced', logPath: runtimeTempPath(runtimeRoot, WATCHER_LOG_FILENAME) };
|
|
873
877
|
|
|
874
|
-
const logPath =
|
|
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 =
|
|
933
|
-
const timingPath =
|
|
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 = () => {
|
package/src/worktree-apply.js
CHANGED
|
@@ -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 =
|
|
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 结果 */ }
|
package/src/write-audit.js
CHANGED
|
@@ -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` →
|
|
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`),修复问题后再提交。
|