sillyspec 3.28.10 → 3.28.12
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-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +3 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/change-risk-profile.js +76 -6
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +23 -0
- package/src/design-facts.js +46 -9
- package/src/diagnostic-codes.js +95 -0
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/doctor-diagnostics.js +141 -1
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +129 -1
- package/src/machine-interface.js +63 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +738 -8
- package/src/stage-review-checklist.js +16 -2
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +8 -3
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +95 -10
- package/src/verify-postcheck.js +346 -28
- package/src/verify-probes.js +1221 -46
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* integration-critical / deployment-critical),产出门控验证需求。
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, statSync } from 'fs'
|
|
9
|
-
import { join } from 'path'
|
|
9
|
+
import { join, isAbsolute } from 'path'
|
|
10
10
|
|
|
11
11
|
// ============ 向后兼容:旧的 INTEGRATION_CRITICAL_PATTERNS ============
|
|
12
12
|
|
|
@@ -301,6 +301,45 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
301
301
|
return { level, triggers, suppressedTriggers, requiredVerification }
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// ============ 回执来源分类打标(X-10 / D-002 / D-006,2026-09-17-pass-cap-semantics task-02) ============
|
|
305
|
+
//
|
|
306
|
+
// EHS 实证(D-002):mvn compile + JUnitCore 纯单测的三条 log 通过了集成回执四条件校验——
|
|
307
|
+
// 单测/编译类回执被当成集成实测。打标口径:只认回执 command 的命令来源声明(quality-scan
|
|
308
|
+
// 记录的命令与 verify_precedents 声明同思想),不解析日志内容猜测(X-10);未定类默认
|
|
309
|
+
// 'build'(fail-closed 侧:build/unit 回执不构成「集成实测已跑」绿判据,宁可触发封顶要求
|
|
310
|
+
// handover——stage-contract 侧 validatePassEligibility(evaluatePassEligibility 纯函数 +
|
|
311
|
+
// resolveFactsExpected 判定)消费本口径对结论=PASS 封顶;verify-probes producer 侧
|
|
312
|
+
// classifyReceiptCommandSource(facts.integrationRan 判定)与本分类同族口径,两处正则族
|
|
313
|
+
// 保持同步演进)。smoke 冒烟命令族计 cross-layer(D-006 退役判据:批次 C commands.smoke
|
|
314
|
+
// 落地后由 smoke 回执一票判定「集成实测已跑」——起服务冒烟即跨层实测,不得默认 build 误拦金路径)。
|
|
315
|
+
//
|
|
316
|
+
// 来源标记直判(2026-09-17-api-coverage-smoke task-02 / Grill B-1 修正,FR-03):条目 source
|
|
317
|
+
// 标记 'cli-noai-smoke'(verify-probes ensureSmokeReceiptSection 注入的机器段行尾注,
|
|
318
|
+
// parseEvidenceSlots additive 解析回填)→ 直判 cross-layer,**优先于命令词正则分类**——
|
|
319
|
+
// 修正点在标记识别而非命令词增补:node scripts/smoke.mjs / bash smoke.sh / python smoke.py
|
|
320
|
+
// 等脚本形态在既有正则下全判 build 会误拦金路径,且脚本形态词表不可枚举(正则族两文件零词
|
|
321
|
+
// 变动)。对齐目标:verify-probes.js classifyReceiptCommandSource(judgeIntegrationRan 消费)
|
|
322
|
+
// 同款 sourceMark 参数与标记族——**任一侧增改标记族必须双侧同步**(G-3 先例:producer 侧
|
|
323
|
+
// 曾漏 smoke 一词致口径分裂)。
|
|
324
|
+
const RECEIPT_SOURCE_CROSS_LAYER_RE = new RegExp([
|
|
325
|
+
'\\bcurl\\b', '\\bwget\\b', '\\bhttpie\\b', '\\bInvoke-WebRequest\\b', '\\bInvoke-RestMethod\\b',
|
|
326
|
+
'\\biwr\\b', '\\birm\\b', 'https?://',
|
|
327
|
+
'spring-boot:run', '\\bjava\\b[^|\\n]*\\s-jar\\b',
|
|
328
|
+
'\\b(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:dev|start|serve|smoke)\\b',
|
|
329
|
+
'\\bdotnet\\s+run\\b', '\\bflask\\s+run\\b', '\\buvicorn\\b', '\\bgunicorn\\b',
|
|
330
|
+
'\\b(?:nc|netcat|telnet|socat)\\b',
|
|
331
|
+
].join('|'), 'i')
|
|
332
|
+
const RECEIPT_SOURCE_UNIT_RE = /\bJUnitCore\b|\bnode\s+--test\b|\bmocha\b|\bjest\b|\bvitest\b|\bpytest\b|\bphpunit\b|\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b/i
|
|
333
|
+
/** CLI 机器段来源标记(与 verify-probes.js SMOKE_RECEIPT_SOURCE_MARK / task-01 记录 source 同值,双侧同步) */
|
|
334
|
+
const CLI_SMOKE_SOURCE_MARK = 'cli-noai-smoke'
|
|
335
|
+
function classifyReceiptSourceTag(command, sourceMark) {
|
|
336
|
+
if (sourceMark === CLI_SMOKE_SOURCE_MARK) return 'cross-layer'
|
|
337
|
+
const cmd = String(command || '')
|
|
338
|
+
if (RECEIPT_SOURCE_CROSS_LAYER_RE.test(cmd)) return 'cross-layer'
|
|
339
|
+
if (RECEIPT_SOURCE_UNIT_RE.test(cmd)) return 'unit'
|
|
340
|
+
return 'build'
|
|
341
|
+
}
|
|
342
|
+
|
|
304
343
|
/**
|
|
305
344
|
* 检查 verify-result.md 是否包含集成验证证据
|
|
306
345
|
*
|
|
@@ -308,6 +347,10 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
|
|
|
308
347
|
* CLI 结构化回执文本(如 verify-services.receipt.json 的服务回收回执)——它随 verifyContent
|
|
309
348
|
* 一起参与 literals 匹配,agent 真实起过服务且 CLI 回收过(有回执)时不再依赖其自然语言
|
|
310
349
|
* 措辞恰好含字面词,表述差异不再误拦。
|
|
350
|
+
*
|
|
351
|
+
* opts.sourceTag(X-10 / D-006,2026-09-17-pass-cap-semantics task-02):可选回执来源声明
|
|
352
|
+
* ('cross-layer' | 'build' | 'unit'),透传给 auditRuntimeReceipt 优先于按 command 就地
|
|
353
|
+
* 分类;非法值忽略。build/unit 回执不作集成实测绿判据(缺省按 command 分类,未定类 build)。
|
|
311
354
|
*/
|
|
312
355
|
export function checkIntegrationEvidence(verifyContent, requiredVerification, opts = {}) {
|
|
313
356
|
const errors = []
|
|
@@ -333,7 +376,7 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification, op
|
|
|
333
376
|
const hasGreen = greenReceipts.length > 0
|
|
334
377
|
if (needsIntegration && !hasGreen) {
|
|
335
378
|
const bad = receiptAudit.filter(a => !a.green)
|
|
336
|
-
errors.push(`integration-critical 变更无绿回执(${bad.length} 条回执校验不过:${bad.map(a => a.reason).join(';') || '无有效回执'})——绿判据:log 存在 × mtime ∈ verify 窗口 × 日志无失败签名 × exit 0
|
|
379
|
+
errors.push(`integration-critical 变更无绿回执(${bad.length} 条回执校验不过:${bad.map(a => a.reason).join(';') || '无有效回执'})——绿判据:log 存在 × mtime ∈ verify 窗口 × 日志无失败签名 × exit 0 × 来源 cross-layer(build/unit 回执不作集成实测判据,D-006)`)
|
|
337
380
|
}
|
|
338
381
|
if (needsLogEvidence && !hasGreen) {
|
|
339
382
|
errors.push('integration-critical 变更的回执未通过一致性校验(Runtime Evidence 等价物)')
|
|
@@ -394,13 +437,33 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification, op
|
|
|
394
437
|
* 单条回执一致性校验(FR-04 绿判据)。噪声剔除:签名行首匹配 + 剔除「0 errors」类良性行
|
|
395
438
|
* (本仓 verify-postcheck.js 测试输出解析先例同款)。fail-soft:校验依赖缺失(opts 无
|
|
396
439
|
* cwd/verifyStartAt)时只核 exitCode 与日志可读性,不假装跑了文件校验。
|
|
440
|
+
*
|
|
441
|
+
* opts.sourceTag(X-10 / D-006,2026-09-17-pass-cap-semantics task-02):可选回执来源声明,
|
|
442
|
+
* 缺省按回执 command 来源就地分类(classifyReceiptSourceTag),未定类默认 'build'(向后
|
|
443
|
+
* 兼容的 fail-closed 缺省);sourceTag ∈ {build, unit} 的回执不作集成实测绿判据(四条件
|
|
444
|
+
* 全过也只到 non-green,reason 点名来源)——verify-postcheck 不在本调用链上,不受影响。
|
|
445
|
+
*
|
|
446
|
+
* r.source(2026-09-17-api-coverage-smoke task-02 / FR-03,Grill B-1):条目级来源标记
|
|
447
|
+
* (parseEvidenceSlots additive 第五字段,CLI 机器段行 'cli-noai-smoke' 尾注回填)透传给
|
|
448
|
+
* classifyReceiptSourceTag 直判 cross-layer——优先级在调用方 opts.sourceTag 声明源之下、
|
|
449
|
+
* 命令词正则分类之上;无标记条目分类路径逐字节不变。
|
|
397
450
|
*/
|
|
398
451
|
function auditRuntimeReceipt(r, opts) {
|
|
399
|
-
const out = { claim: r && r.claim, logPath: r && r.logPath, green: false, logExists: null, mtimeInWindow: null, failSignatures: 0, exitCode: r && typeof r.exitCode === 'number' ? r.exitCode : null, reason: '' }
|
|
452
|
+
const out = { claim: r && r.claim, logPath: r && r.logPath, sourceTag: null, green: false, logExists: null, mtimeInWindow: null, failSignatures: 0, exitCode: r && typeof r.exitCode === 'number' ? r.exitCode : null, reason: '' }
|
|
453
|
+
// 打标依据 = 命令来源声明(不解析日志内容,X-10):opts.sourceTag 调用方声明源优先,
|
|
454
|
+
// 其次条目 source 标记直判(task-02 Grill B-1——'cli-noai-smoke' 直判 cross-layer),
|
|
455
|
+
// 否则按回执 command 分类;非法/缺省走分类,未定类默认 build。
|
|
456
|
+
const declaredSource = opts && typeof opts.sourceTag === 'string'
|
|
457
|
+
&& ['cross-layer', 'build', 'unit'].includes(opts.sourceTag) ? opts.sourceTag : null
|
|
458
|
+
out.sourceTag = declaredSource || classifyReceiptSourceTag(r && r.command, r && r.source)
|
|
400
459
|
if (!r || !r.logPath) { out.reason = '回执缺 logPath'; return out }
|
|
460
|
+
// 绝对路径直读(2026-09-17-api-coverage-smoke task-02):CLI 机器段(source=cli-noai-smoke)
|
|
461
|
+
// 的 logPath 是 quality-scan 记录的绝对路径实录——join(cwd, 绝对路径) 会拼出破损路径
|
|
462
|
+
// 误报「日志不存在」,isAbsolute 时跳过 join(verify-postcheck 测试输出解析同款口径)。
|
|
463
|
+
const resolveLog = (p) => (opts.cwd && !isAbsolute(String(p)) ? join(opts.cwd, p) : p)
|
|
401
464
|
let content = null
|
|
402
465
|
try {
|
|
403
|
-
const abs =
|
|
466
|
+
const abs = resolveLog(r.logPath)
|
|
404
467
|
content = readFileSync(abs, 'utf8')
|
|
405
468
|
out.logExists = true
|
|
406
469
|
} catch {
|
|
@@ -411,7 +474,7 @@ function auditRuntimeReceipt(r, opts) {
|
|
|
411
474
|
if (opts.verifyStartAt) {
|
|
412
475
|
try {
|
|
413
476
|
const startAt = new Date(opts.verifyStartAt).getTime()
|
|
414
|
-
out.mtimeInWindow = statSync(
|
|
477
|
+
out.mtimeInWindow = statSync(resolveLog(r.logPath)).mtimeMs >= startAt - 60_000
|
|
415
478
|
if (!out.mtimeInWindow) { out.reason = `日志 mtime 早于 verifyStartAt(${r.logPath})`; return out }
|
|
416
479
|
} catch { out.mtimeInWindow = null }
|
|
417
480
|
}
|
|
@@ -424,7 +487,14 @@ function auditRuntimeReceipt(r, opts) {
|
|
|
424
487
|
}
|
|
425
488
|
if (out.failSignatures > 0) { out.reason = `日志含 ${out.failSignatures} 行失败签名(行首 error/exception/traceback/fatal,已剔除良性计数行)`; return out }
|
|
426
489
|
if (out.exitCode !== 0) { out.reason = `exitCode=${out.exitCode}(非 0 不可用作在场证据)`; return out }
|
|
490
|
+
// 来源门(D-002/D-006,2026-09-17-pass-cap-semantics task-02):四条件全过但来源 build/unit
|
|
491
|
+
// (编译/单测/构建类命令)→ 不作集成实测绿判据(EHS 实证口径),排在前置硬伤(缺日志/
|
|
492
|
+
// 出窗/签名/exit≠0)之后不抢 reason 优先级。
|
|
493
|
+
if (out.sourceTag !== 'cross-layer') {
|
|
494
|
+
out.reason = `回执来源 ${out.sourceTag}(command:${(r && r.command) || ''})不构成「集成实测已跑」——build/unit 类命令(编译/单测/构建)不作集成绿判据(D-006)。出路:提供跨层实测回执(起服务/HTTP/进程对进程/smoke 冒烟命令),或降级 PASS WITH NOTES 并在「## 移交项(结构化)」承载缺口`
|
|
495
|
+
return out
|
|
496
|
+
}
|
|
427
497
|
out.green = true
|
|
428
|
-
out.reason = '绿回执(log 存在 × mtime 窗口 × 无失败签名 × exit 0)'
|
|
498
|
+
out.reason = '绿回执(log 存在 × mtime 窗口 × 无失败签名 × exit 0 × 来源 cross-layer)'
|
|
429
499
|
return out
|
|
430
500
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commit-guard.js — pre-commit 提交边界守卫(fail-open,只警告不阻断)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要(2026-09-17 实证):多会话共享仓里 git 暂存区是共享状态——他会话 git add 过的
|
|
5
|
+
* 文件会被本会话的裸 git commit 一并带走(当日事故:add 带 pathspec 但 commit 裸跑,并行
|
|
6
|
+
* 会话 5 个 staged 文件被扫入 fb5bc11,事后 reset --soft + pathspec 重提交外科手术拆回)。
|
|
7
|
+
* AGENTS.md 第 18 条已加厚为「add/commit 同清单 pathspec」,本守卫是同一约束的工具化兜底:
|
|
8
|
+
* 纪理会漏,钩子卡点不会漏(与 write-audit / concurrent-detect 同族——确定性的事 CLI 兜)。
|
|
9
|
+
*
|
|
10
|
+
* 信号(只警告,exit 恒 0):
|
|
11
|
+
* S1 quick 声明面对账:staged 含 .sillyspec/quicklog/patches/ql-<id>.json 时,读其
|
|
12
|
+
* rows[].declared 声明面——staged 中超出声明面的文件(quicklog 账本/补丁自身除外)
|
|
13
|
+
* 即警告「疑似扫入他者文件」。
|
|
14
|
+
* S2 跨变更目录:staged 里 .sillyspec/changes/<name>/(含 archive/)命中 ≥2 个不同
|
|
15
|
+
* name → 警告「正常提交只携带一个变更的面」。
|
|
16
|
+
* 命中即 stderr 警告 + appendWriteAudit 留痕(via: 'commit-guard')。
|
|
17
|
+
*
|
|
18
|
+
* fail-open 硬约束(钩子挂在所有会话共享的 .husky/ 上,守卫自身绝不拦任何人的提交):
|
|
19
|
+
* 非 git 仓 / git 失败 / ql json 损坏 / 任何异常 → 静默 exit 0;钩子侧再垫 || true 双保险。
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from 'child_process'
|
|
22
|
+
import { readFileSync, existsSync } from 'fs'
|
|
23
|
+
import { join } from 'path'
|
|
24
|
+
import { pathToFileURL } from 'url'
|
|
25
|
+
import { appendWriteAudit } from './write-audit.js'
|
|
26
|
+
|
|
27
|
+
const QUICKLOG_PREFIX = '.sillyspec/quicklog/'
|
|
28
|
+
const QL_PATCH_JSON_RE = /^\.sillyspec\/quicklog\/patches\/ql-[^/]+\.json$/
|
|
29
|
+
const CHANGES_RE = /^\.sillyspec\/changes\/(?:archive\/)?([^/]+)\//
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* S1+S2 纯函数:staged 面 + ql patch json 映射 → 警告数组(无 git/文件系统依赖,可直测)。
|
|
33
|
+
* @param {string[]} stagedFiles staged 相对路径
|
|
34
|
+
* @param {Record<string, any>} qlPatches { json路径 → 解析结果 }(损坏的已由调用方跳过)
|
|
35
|
+
* @returns {{signal: string, files: string[], message: string}[]}
|
|
36
|
+
*/
|
|
37
|
+
export function analyzeStagedFace(stagedFiles, qlPatches) {
|
|
38
|
+
const staged = Array.isArray(stagedFiles) ? stagedFiles.filter((f) => typeof f === 'string' && f) : []
|
|
39
|
+
const warnings = []
|
|
40
|
+
// S1 quick 声明面对账
|
|
41
|
+
const qlJsons = staged.filter((f) => QL_PATCH_JSON_RE.test(f))
|
|
42
|
+
if (qlJsons.length > 0) {
|
|
43
|
+
const declared = new Set()
|
|
44
|
+
for (const j of qlJsons) {
|
|
45
|
+
const parsed = (qlPatches || {})[j]
|
|
46
|
+
if (!parsed || !Array.isArray(parsed.rows)) continue
|
|
47
|
+
for (const r of parsed.rows) {
|
|
48
|
+
if (r && r.declared && typeof r.path === 'string') declared.add(r.path)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (declared.size > 0) {
|
|
52
|
+
const offending = staged.filter((f) => !f.startsWith(QUICKLOG_PREFIX) && !declared.has(f))
|
|
53
|
+
if (offending.length > 0) {
|
|
54
|
+
warnings.push({
|
|
55
|
+
signal: 'quick_declared_face_exceeded',
|
|
56
|
+
files: offending,
|
|
57
|
+
message: `staged 超出 quick 声明面(${qlJsons.map((f) => f.split('/').pop()).join(', ')}):${offending.join('、')}——疑似扫入他者文件,核对是否该随本提交带走`,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// S2 跨变更目录
|
|
63
|
+
const changeNames = new Set()
|
|
64
|
+
for (const f of staged) {
|
|
65
|
+
const m = f.match(CHANGES_RE)
|
|
66
|
+
if (m) changeNames.add(m[1])
|
|
67
|
+
}
|
|
68
|
+
if (changeNames.size >= 2) {
|
|
69
|
+
warnings.push({
|
|
70
|
+
signal: 'multi_change_dirs_staged',
|
|
71
|
+
files: staged.filter((f) => CHANGES_RE.test(f)),
|
|
72
|
+
message: `staged 同时含 ${changeNames.size} 个变更目录(${[...changeNames].join('、')})——正常提交只携带一个变更的面,疑似夹带并行会话产物`,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
return warnings
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main() {
|
|
79
|
+
const cwd = process.cwd()
|
|
80
|
+
let staged = []
|
|
81
|
+
try {
|
|
82
|
+
staged = execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
83
|
+
cwd,
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
// stderr 必须管道化:execFileSync 默认把子进程 stderr 透传给父进程——非 git 目录下
|
|
86
|
+
// git 落 --no-index 模式报 unknown option 的 usage 长文会漏进守卫 stderr(噪声)。
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
|
+
})
|
|
89
|
+
.split(/\r?\n/)
|
|
90
|
+
.map((s) => s.trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
} catch {
|
|
93
|
+
return 0 // 非 git 仓 / git 异常 → 放行
|
|
94
|
+
}
|
|
95
|
+
if (staged.length === 0) return 0
|
|
96
|
+
const qlPatches = {}
|
|
97
|
+
for (const f of staged) {
|
|
98
|
+
if (!QL_PATCH_JSON_RE.test(f)) continue
|
|
99
|
+
try {
|
|
100
|
+
qlPatches[f] = JSON.parse(readFileSync(join(cwd, f), 'utf8'))
|
|
101
|
+
} catch { /* 单个 json 损坏 → 跳过该信号源,不连坐 */ }
|
|
102
|
+
}
|
|
103
|
+
const warnings = analyzeStagedFace(staged, qlPatches)
|
|
104
|
+
if (warnings.length === 0) return 0
|
|
105
|
+
for (const w of warnings) process.stderr.write(`⚠️ [commit-guard] ${w.message}\n`)
|
|
106
|
+
const specDir = join(cwd, '.sillyspec')
|
|
107
|
+
if (existsSync(specDir)) {
|
|
108
|
+
appendWriteAudit(specDir, {
|
|
109
|
+
via: 'commit-guard',
|
|
110
|
+
files: [...new Set(warnings.flatMap((w) => w.files))].slice(0, 20),
|
|
111
|
+
detail: { signals: warnings.map((w) => ({ signal: w.signal, files: w.files.slice(0, 20) })) },
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 直接执行(node src/commit-guard.js)才跑 main;被 import 时不跑
|
|
118
|
+
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
119
|
+
if (invokedDirectly) process.exit(main())
|
package/src/config-schema.js
CHANGED
|
@@ -70,6 +70,7 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
70
70
|
{ path: 'commands.build', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '构建命令。', example: 'npm run build' },
|
|
71
71
|
{ path: 'commands.test', type: 'string', optional: true, status: 'live', readers: ['extractTestCommand (src/verify-postcheck.js)', 'runQuickTestLintGate (src/run/quick-audit.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '测试命令——verify 阶段 CLI 亲自执行此命令与 verify-result.md 对账,实测失败即阻断;quick --done 触及 src/test 时同款实测(P0-2 门禁)。', example: 'npm test' },
|
|
72
72
|
{ path: 'commands.lint', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'runQuickTestLintGate (src/run/quick-audit.js)', 'validateCommands (src/scan-postcheck.js)'], desc: 'lint 命令——quick --done 触及 src/test 时 CLI 亲自执行(P0-2 门禁,失败阻断完成)。', example: 'npm run lint' },
|
|
73
|
+
{ path: 'commands.smoke', type: 'string', optional: true, status: 'live', readers: ['executeVerifyQualityScan (src/run/verify-quality-scan.js)'], desc: '接口冒烟命令——verify 阶段 CLI 亲跑(脚本自理服务生命周期:后台起服+轮询就绪+finally 杀),300s 超时帽,失败/超时只记失败态不阻断本步、触发 PASS 封顶(D-002);指纹含此键,未变不重跑。', example: 'node scripts/smoke.mjs' },
|
|
73
74
|
{ path: 'commands.install', type: 'string', optional: true, status: 'live', readers: ['extractInstallCommand (src/worktree-deps.js)'], desc: '依赖安装命令(worktree 依赖 provisioning 用)。', example: 'npm install' },
|
|
74
75
|
],
|
|
75
76
|
},
|
|
@@ -152,6 +153,15 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
152
153
|
{ path: 'worktree.supplyFiles', type: 'array', optional: true, status: 'live', readers: ['readSupplyFilesConfig + _supplyGeneratedFiles (src/worktree.js — create step 5.9 供给步)'], desc: '随 worktree create 从主仓复制的生成物清单:精确路径或 glob(* 单层 / ** 多层),相对仓根。展开上限 200 文件超出截断警告;实供清单记 meta.supplyFiles;缺省 [] = 供给步空转零行为变化。', example: 'src/build-id.ts' },
|
|
153
154
|
],
|
|
154
155
|
},
|
|
156
|
+
{
|
|
157
|
+
id: 'gate_snapshot',
|
|
158
|
+
title: '门禁隔离快照 copy 面',
|
|
159
|
+
note: '门禁快照(gate-snapshot)= HEAD worktree + 会话文件 overlay + 环境目录 junction——gitignored 生成物(api-types/generated 类)不进 HEAD 也不在会话集,快照内 lint/test 环境性假败(2026-09-16 驾驭小结④,friction5-hardening R4 / D-002@v1)。按本清单在快照构建期从主仓 junction 链接补齐(失败回退复制);快照内 overlay 已覆盖的同名路径跳过(本变更最新态优先)。⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录。',
|
|
160
|
+
keys: [
|
|
161
|
+
{ path: 'gate_snapshot.copy', type: 'array', optional: true, status: 'live', readers: ['createGateSnapshot (src/run/gate-snapshot.js)'], desc: '门禁隔离快照的生成物/额外路径 copy 面(元素 string,相对仓根):HEAD 快照缺 gitignored 生成物导致 lint/test 环境性假败;声明后快照构建期从主仓 junction 链接(失败回退复制)。⚠️ junction 是活链接,快照内再跑生成命令会写穿到主仓该目录。未配置/空清单 = 全段空转零行为变化。', example: 'gate_snapshot:\n copy:\n - src/generated' },
|
|
162
|
+
{ path: 'gate_snapshot.commands', type: 'array', optional: true, status: 'live', readers: ['parseGateSnapshotCommands + runGateSnapshotCommands (src/run/gate-snapshot.js — createGateSnapshot 环境预检后 copy 面前)'], desc: '门禁隔离快照的命令面(元素 string):快照构建期在环境目录链接后、copy 面前于快照根逐条执行(cwd=快照根、300s/条超时帽、非零退出/超时只 warn 继续——fail-open 不作废快照),产出「本仓应然态」生成物,收口主仓生成物缺失/过期(fresh clone 未跑 postinstall / build-id 类产物)时快照内全量 lint/test 必挂的坑(D-002@v2,2026-09-17-feedback-hardening FR-01)。⚠️ 环境目录(node_modules/venv)是活链接,命令写它们会穿透主仓——只放生成物命令(npm run gen:build-id 类),不放 install/build 全家桶。未配置 = 全段空转零行为。', example: 'npm run gen:build-id' },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
155
165
|
{
|
|
156
166
|
id: 'auto_mode',
|
|
157
167
|
title: '变更规模自动分类',
|
|
@@ -307,6 +317,7 @@ commands:
|
|
|
307
317
|
build: npm run build
|
|
308
318
|
test: npm test
|
|
309
319
|
lint: npm run lint
|
|
320
|
+
# smoke: node scripts/smoke.mjs # verify 冒烟命令(可选;脚本自理服务生命周期,失败只封顶 PASS 不阻断)
|
|
310
321
|
# install: npm install # worktree 依赖安装命令
|
|
311
322
|
|
|
312
323
|
# ── SillyHub MCP 客户端凭据(派发到 worker 用)──
|
|
@@ -379,6 +390,18 @@ worktree-hook:
|
|
|
379
390
|
# supplyFiles:
|
|
380
391
|
# - src/build-id.ts
|
|
381
392
|
|
|
393
|
+
# ── 门禁隔离快照 copy 面(HEAD 快照缺 gitignored 生成物 → 快照内 lint/test 环境性假败;构建期从主仓 junction 补齐)──
|
|
394
|
+
# 相对仓根路径列表;失败回退复制;快照内 overlay 已覆盖的同名路径跳过。
|
|
395
|
+
# ⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录。
|
|
396
|
+
# commands 面(D-002@v2):快照构建期在快照根逐条执行的生成物命令(postinstall/build-id 类,
|
|
397
|
+
# 产出本仓应然态;300s/条超时帽,失败 warn 继续)。⚠️ 命令不得改写 node_modules/venv 等
|
|
398
|
+
# 环境目录(活链接,写穿透主仓)——只放生成物命令。
|
|
399
|
+
# gate_snapshot:
|
|
400
|
+
# copy:
|
|
401
|
+
# - src/generated
|
|
402
|
+
# commands:
|
|
403
|
+
# - npm run gen:build-id
|
|
404
|
+
|
|
382
405
|
# ── 变更规模自动分类(sillyspec run auto 时按需求描述强制 quick/full;正则数组,i 大小写无关)──
|
|
383
406
|
auto_mode:
|
|
384
407
|
force_full_patterns:
|
package/src/design-facts.js
CHANGED
|
@@ -332,7 +332,8 @@ export function generateDesignSkeleton({ changeName, decisionsText, author, now
|
|
|
332
332
|
lines.push('')
|
|
333
333
|
lines.push('| 操作 | 文件路径 | 说明 |')
|
|
334
334
|
lines.push('|---|---|---|')
|
|
335
|
-
lines.push('<!-- 示例行(替换为实际清单后删除本注释):| 新增 | src/xxx/NewFile.java | 说明(含对外字段时交代 producer→consumer 数据流) | -->')
|
|
335
|
+
lines.push('<!-- 示例行(替换为实际清单后删除本注释):| 新增 | NEW:src/xxx/NewFile.java | 说明(含对外字段时交代 producer→consumer 数据流) | -->')
|
|
336
|
+
lines.push('<!-- 路径存在性核验铁律:计划新建的文件路径必须带 NEW: 前缀(如上例),已有文件写仓根相对正斜杠路径——末步 --done 门禁逐一核验存在性,无前缀的新建路径按 design_file_ref_invalid 拦截返工(2026-09-17 用户反馈⑥-①,写作期即提示) -->')
|
|
336
337
|
lines.push('')
|
|
337
338
|
|
|
338
339
|
lines.push('## 接口定义')
|
|
@@ -458,22 +459,58 @@ export function validateDesignFileList({ changeDir, cwd } = {}) {
|
|
|
458
459
|
warnings.push(`design 清单疑似仓变更段头格式异常(该段文件可能被记到上一段仓,覆盖对账会点名):${h}`)
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
// 跨仓注册表读侧口径 = <cwd>/.sillyspec/local.yaml(与 execute MultiRepoContext /
|
|
463
|
+
// register-repo 写侧一致,parseRepoRegistry 自带 CRLF 容差)——有段头/无段头两分支
|
|
464
|
+
// 共用(task-05 / FR-10 上移:无段头分支也要做跨仓注册路径前缀命中判定)
|
|
465
|
+
let registry = null
|
|
466
|
+
const localYamlPath = join(cwd, '.sillyspec', 'local.yaml')
|
|
467
|
+
if (existsSync(localYamlPath)) {
|
|
468
|
+
try { registry = parseRepoRegistry(readFileSync(localYamlPath, 'utf8')) } catch { registry = null }
|
|
469
|
+
}
|
|
470
|
+
|
|
461
471
|
if (!hasSegmentHeader) {
|
|
462
472
|
const entries = parseFileChangeListDetailed(designPath, { keepSillyspecDocs: true })
|
|
463
473
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
464
474
|
return { ok: true, errors, warnings: [...warnings, 'design 清单核验跳过:design.md 无文件变更清单段(small 变更可无清单)'] }
|
|
465
475
|
}
|
|
466
|
-
|
|
476
|
+
// task-05 / FR-10 无段头缺口:清单行含跨仓注册路径(注册值/basename 前缀命中)时不按
|
|
477
|
+
// 主仓根打 design_file_ref_invalid 逼 NEW: 前缀(跨仓既有文件被逼声明计划新建=语义撒谎,
|
|
478
|
+
// 坑 design-file-ref-cross-repo-blind 同族的无段头形态),降 warning 提示补段头——补
|
|
479
|
+
// 「## <repo> 仓变更」段头后该段按仓根核验,真幻觉路径仍会在仓根核验下暴露(只消除
|
|
480
|
+
// 假红、不引入假绿)。纯主仓行(无前缀命中)走原主仓根核验,行为零变化。
|
|
481
|
+
const crossPrefixes = []
|
|
482
|
+
if (registry) {
|
|
483
|
+
for (const [repoKey, rootRaw] of registry) {
|
|
484
|
+
const root = String(rootRaw || '').replace(/\\/g, '/').replace(/\/+$/, '')
|
|
485
|
+
if (!root) continue
|
|
486
|
+
const bn = root.split('/').pop()
|
|
487
|
+
crossPrefixes.push({ repoKey, prefixes: bn && bn !== root ? [root, bn] : [root] })
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const hitCrossRepo = (rawPath) => {
|
|
491
|
+
const n = String(rawPath || '').trim().replace(/\\/g, '/')
|
|
492
|
+
if (!n) return null
|
|
493
|
+
for (const c of crossPrefixes) {
|
|
494
|
+
for (const pre of c.prefixes) {
|
|
495
|
+
if (n === pre || n.startsWith(pre + '/')) return c.repoKey
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return null
|
|
499
|
+
}
|
|
500
|
+
const mainPaths = []
|
|
501
|
+
for (const e of entries) {
|
|
502
|
+
const hit = hitCrossRepo(e.path)
|
|
503
|
+
if (hit) {
|
|
504
|
+
warnings.push(`design 清单无仓变更段头且该行疑似跨仓 ${hit} 仓路径:${e.path}——补「## ${hit} 仓变更」段头后按该仓根核验(暂不按主仓根核验/逼 NEW: 前缀)`)
|
|
505
|
+
} else {
|
|
506
|
+
mainPaths.push(e.path)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
checkAgainstRoot(cwd, mainPaths, '主仓')
|
|
467
510
|
return { ok: errors.length === 0, errors, warnings }
|
|
468
511
|
}
|
|
469
512
|
|
|
470
|
-
//
|
|
471
|
-
// (与 execute MultiRepoContext / register-repo 写侧一致,parseRepoRegistry 自带 CRLF 容差)
|
|
472
|
-
let registry = null
|
|
473
|
-
const localYamlPath = join(cwd, '.sillyspec', 'local.yaml')
|
|
474
|
-
if (existsSync(localYamlPath)) {
|
|
475
|
-
try { registry = parseRepoRegistry(readFileSync(localYamlPath, 'utf8')) } catch { registry = null }
|
|
476
|
-
}
|
|
513
|
+
// 有段头:逐段按其仓根核验。
|
|
477
514
|
for (const [repo, paths] of byRepo) {
|
|
478
515
|
if (repo === 'main') {
|
|
479
516
|
checkAgainstRoot(cwd, paths, '主仓')
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diagnostic-codes.js — machine-interface 族诊断码表单一源
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:信封 errors/warnings 是自由文本中文句子,程序化消费方(SillyHub/未来 MCP
|
|
5
|
+
* 消费者)无法按错误类型稳定分支——提示语改一个字就是一次静默破坏性变更。本表给
|
|
6
|
+
* gate/derive/progress show 三面的自产错误配稳定 snake_case 身份码(2026-09-17
|
|
7
|
+
* OpenSpec 源码对比三轮评审②号项,change 2026-09-17-mi-diagnostic-codes)。
|
|
8
|
+
*
|
|
9
|
+
* 纪律(D-002@v1 硬边界):
|
|
10
|
+
* - 恰 10 码:信封级 4(exit 2 面)+ check 级 6(gate/derive 失败面)。
|
|
11
|
+
* - 只码 machine-interface 族自产错误——stage-contract 校验器散文、doctor/validate/
|
|
12
|
+
* scope-audit 面首期不碰(码标识失败面,不标识每条消息)。
|
|
13
|
+
* - 扩码走变更流程追加;本表是码身份唯一源——契约文档诊断码目录(interface-contract.md)
|
|
14
|
+
* 与运行时发射不得出现表外码,test/diagnostic-codes-parity.test.mjs 双向钉死
|
|
15
|
+
* (OpenSpec 的 agent-contract 无 parity 测试,此为反超点)。
|
|
16
|
+
*
|
|
17
|
+
* 零依赖叶子模块(不 import 仓内任何其他 src 模块,防环)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 码 → { surface: 消费命令面, exit: 该失败面的退出码语义, trigger: 触发条件 }。
|
|
22
|
+
* surface 取值 ∈ gate / derive / progress show(CLI 子命令视角)。
|
|
23
|
+
*/
|
|
24
|
+
export const DIAGNOSTIC_CODES = Object.freeze({
|
|
25
|
+
// ── 信封级(无法核验,exit 2)──
|
|
26
|
+
db_missing: {
|
|
27
|
+
surface: ['gate', 'derive', 'progress show'],
|
|
28
|
+
exit: 2,
|
|
29
|
+
trigger: '进度库不存在(只读契约:不为其建库,fail-closed)',
|
|
30
|
+
},
|
|
31
|
+
change_not_found: {
|
|
32
|
+
surface: ['gate', 'derive'],
|
|
33
|
+
exit: 2,
|
|
34
|
+
trigger: '变更名在进度库无记录',
|
|
35
|
+
},
|
|
36
|
+
unknown_facet: {
|
|
37
|
+
surface: ['derive'],
|
|
38
|
+
exit: 2,
|
|
39
|
+
trigger: 'facet ∉ FACETS 白名单(用法错)',
|
|
40
|
+
},
|
|
41
|
+
internal_error: {
|
|
42
|
+
surface: ['gate', 'derive', 'progress show'],
|
|
43
|
+
exit: 2,
|
|
44
|
+
trigger: '内部异常兜底(stdout 仍为合法 JSON)',
|
|
45
|
+
},
|
|
46
|
+
// ── check 级(事实性阻断,exit 1)──
|
|
47
|
+
artifacts_invalid: {
|
|
48
|
+
surface: ['gate', 'derive'],
|
|
49
|
+
exit: 1,
|
|
50
|
+
trigger: 'runValidators 失败(gate artifacts check 与 derive artifacts facet 同源)',
|
|
51
|
+
},
|
|
52
|
+
design_file_ref_invalid: {
|
|
53
|
+
surface: ['gate'],
|
|
54
|
+
exit: 1,
|
|
55
|
+
trigger: 'design 文件清单行级核验失败(直承 design-facts.js 既有稳定码)',
|
|
56
|
+
},
|
|
57
|
+
transition_blocked: {
|
|
58
|
+
surface: ['gate'],
|
|
59
|
+
exit: 1,
|
|
60
|
+
trigger: 'checkTransition 不允许(含 failed_post_check 门控)',
|
|
61
|
+
},
|
|
62
|
+
execute_evidence_unchanged: {
|
|
63
|
+
surface: ['gate', 'derive'],
|
|
64
|
+
exit: 1,
|
|
65
|
+
trigger: 'base..head 无代码变更(checkbox ≠ implementation)',
|
|
66
|
+
},
|
|
67
|
+
task_reviews_invalid: {
|
|
68
|
+
surface: ['gate', 'derive'],
|
|
69
|
+
exit: 1,
|
|
70
|
+
trigger: 'validateTaskReviews 失败(review.json 缺失/schema/verdict=fail)',
|
|
71
|
+
},
|
|
72
|
+
verify_test_failed: {
|
|
73
|
+
surface: ['gate', 'derive'],
|
|
74
|
+
exit: 1,
|
|
75
|
+
trigger: 'commands.test 实测失败',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** gate check id → 失败码(六 check 全覆盖;表外 id 返回 undefined,调用方不挂键)。 */
|
|
80
|
+
const CHECK_ID_TO_CODE = Object.freeze({
|
|
81
|
+
artifacts: 'artifacts_invalid',
|
|
82
|
+
'design-file-list': 'design_file_ref_invalid',
|
|
83
|
+
transition: 'transition_blocked',
|
|
84
|
+
'execute-evidence': 'execute_evidence_unchanged',
|
|
85
|
+
'task-reviews': 'task_reviews_invalid',
|
|
86
|
+
'verify-test': 'verify_test_failed',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} checkId - gate check id(artifacts/design-file-list/transition/execute-evidence/task-reviews/verify-test)
|
|
91
|
+
* @returns {string|undefined} 对应失败码;表外 id 返回 undefined(信封不挂 code 键)
|
|
92
|
+
*/
|
|
93
|
+
export function checkCode(checkId) {
|
|
94
|
+
return CHECK_ID_TO_CODE[checkId];
|
|
95
|
+
}
|
package/src/docs-check.js
CHANGED
|
@@ -117,12 +117,15 @@ export function createHeadReader(projectRoot, ref = 'HEAD') {
|
|
|
117
117
|
* 纯位置锚 `file.js:123?`(2026-09-11 用户实证 docs gate 误伤:跨文件引用+反话论述——锚 A
|
|
118
118
|
* 文件而行内反引号 token 全是 B 概念,层 2 必失败,被迫删行号绕开):`?` 显式声明本锚只做
|
|
119
119
|
* 定位不做关键词断言,层 1(存在性+行界)照校——论述语境锚的一等语法,替代行号化绕开。
|
|
120
|
-
* 文件段展开循环形(D-006,2026-09-08-docs-fix-capability
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
120
|
+
* 文件段展开循环形(D-006,2026-09-08-docs-fix-capability;圆/方并列 2026-09-17-docs-bracket-reanchor):
|
|
121
|
+
* 普通段可选 + 零或多个「闭合括号段(圆/方同权)+普通段」迭代——Next.js 路由组 (dashboard) 与
|
|
122
|
+
* 动态路由 [id]/[cid] 一等公民(D-001:完整开闭同形括号对并列,[ ] 不进普通段字符类);markdown
|
|
123
|
+
* 链接 [t](foo.js:12) 的 [t] 匹配方括号段后遇 `(` 截止——后续圆括号段要求 `)` 先于 `:` 闭合、
|
|
124
|
+
* 扩展名组要求 `.` 均失败,回溯放弃 [t] 段回落提取 foo.js:12(与圆括号段同族回落,零回归);嵌套
|
|
125
|
+
* ((x)) 与 [[x]]/[[...slug]] 内容含 `(`/`[` 不属括号段字符类 → 段不成立,部分提取残段(与旧正则
|
|
126
|
+
* 逐字节一致)。⚠ 不用原子序列 (?:A+|B+)+——经典嵌套量词 ReDoS(Grill 实证 n=30 长 token 73.8s),
|
|
127
|
+
* 展开循环每次迭代必含完整开闭同形括号对 → 划分唯一 → 线性。 */
|
|
128
|
+
const REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:(?:\([A-Za-z0-9_.\-\/]+\)|\[[A-Za-z0-9_.\-\/]+\])[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go)):(\d+)(?:-(\d+))?(\?)?/g
|
|
126
129
|
|
|
127
130
|
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八)
|
|
128
131
|
* + .sillyspec/changes/ + .sillyspec/knowledge/(P1-1,noai-ir-roadmap §4:最活跃、最易产生
|
|
@@ -167,11 +170,12 @@ export class DocsCheckConfigError extends Error {}
|
|
|
167
170
|
/**
|
|
168
171
|
* 符号锚正则(P1-2,noai-ir-roadmap §4):`file.js::symbol` 形态——校验时解析符号定义行,
|
|
169
172
|
* 天然免疫行号漂移(重锚从「每次代码移动」降为「仅改名时」)。文件段与 REF_RE 同款(含
|
|
170
|
-
* repo://
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
+
* repo:// 跨仓前缀与括号路径组展开;2026-09-17-docs-bracket-reanchor 起圆/方括号段并列
|
|
174
|
+
* 同权——动态路由 app/[lang]/layout.tsx 类路径可符号锚),`::` 后只认 ASCII 标识符(自然语言/
|
|
175
|
+
* 中文括注不触发,QUICKLOG file-notes 的 `path::注` 约定因注解非标识符而天然互斥)。
|
|
176
|
+
* 与 REF_RE 互斥:`::ident` 无数字不匹配行号式;`file.js:12` 无 `::` 不匹配符号式。
|
|
173
177
|
*/
|
|
174
|
-
const SYMBOL_REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:\([A-Za-z0-9_.\-\/]+\)[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go))::([A-Za-z_$][A-Za-z0-9_$]*)/g
|
|
178
|
+
const SYMBOL_REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:(?:\([A-Za-z0-9_.\-\/]+\)|\[[A-Za-z0-9_.\-\/]+\])[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go))::([A-Za-z_$][A-Za-z0-9_$]*)/g
|
|
175
179
|
|
|
176
180
|
/**
|
|
177
181
|
* 符号定义行定位(与 classifyFix 的 defRe 同款形态,单一判定口径):function/const/let/
|
package/src/docs-debt.js
CHANGED
|
@@ -363,8 +363,13 @@ export function renderDecisionTouchFacts(touches) {
|
|
|
363
363
|
* @returns {{ root: string, changedFiles: string[] }} root = 实际 git 锚(computeDocsDebt 的 projectRoot 同值)
|
|
364
364
|
*/
|
|
365
365
|
export function collectExecuteChangedFiles({ specBase, changeName, cwd, projectRoot } = {}) {
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
// 双候选 worktree 根(坑 platform-dual-root-fourth-consumer,2026-09-16 hunt 实证):平台模式
|
|
367
|
+
// specBase=specRoot 时主仓 worktree 在项目侧 <cwd>/.sillyspec——单查 specBase 必落空 → root 退
|
|
368
|
+
// cwd → 收进的是主仓脏集而非 worktree 交付集,docs-debt 事实面错。对齐 contract-matrix 双候选。
|
|
369
|
+
const wtCandidates = [...new Set([specBase, join(cwd || process.cwd(), '.sillyspec')].filter(Boolean))]
|
|
370
|
+
.map(b => join(b, '.runtime', 'worktrees', changeName || ''))
|
|
371
|
+
const wtRoot = changeName ? (wtCandidates.find(p => existsSync(p)) || null) : null
|
|
372
|
+
const root = projectRoot || (wtRoot ? wtRoot : cwd) || process.cwd()
|
|
368
373
|
const changed = new Set()
|
|
369
374
|
const statusRes = safeGit(root, ['status', '--porcelain'], { trim: false })
|
|
370
375
|
if (!statusRes.error && statusRes.value) {
|