sillyspec 3.28.5 → 3.28.7
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/attachments/2ab2fd87cfa568ca6acadbb934a651d293a0ad8f3227c66a70b56dcf53a0ea3d.pptx +0 -0
- package/attachments/dc4c2b314ee0c295e1fe5243073eb5c7df7e429074ad7c14424523fbe86e6193.pptx +0 -0
- package/meta.json +11 -0
- package/package.json +1 -1
- package/sa-rename-probe/src/new.js +4 -0
- package/src/config-schema.js +26 -0
- package/src/decision-distill.js +14 -4
- package/src/dispatch/probe.js +8 -1
- package/src/docs-check.js +9 -5
- package/src/friction-tally.js +221 -0
- package/src/index.js +4 -1
- package/src/knowledge-match.js +90 -6
- package/src/progress/change-registry.js +28 -0
- package/src/progress.js +6 -0
- package/src/review-dispatch.js +7 -1
- package/src/run/command.js +17 -1
- package/src/run/complete-handlers.js +105 -2
- package/src/run/complete.js +53 -10
- package/src/run/gate-snapshot.js +73 -2
- package/src/run/gates.js +53 -16
- package/src/run/prompt.js +53 -1
- package/src/run/quick-audit.js +95 -5
- package/src/run/shared.js +29 -1
- package/src/run/stage.js +14 -0
- package/src/run/verify-quality-scan.js +52 -5
- package/src/scope-audit.js +308 -27
- package/src/semantic-guard.js +207 -0
- package/src/sillyhub-mcp/client.js +14 -0
- package/src/stages/brainstorm.js +3 -3
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semantic-guard.js — quick 语义护栏聚合模块(change: 2026-09-11-cross-change-decision-guard,
|
|
3
|
+
* task-03,FR-03/FR-04,D-001@v1):近因他者交付归因 + 断言重写检测 + advisory 渲染 + 总开关。
|
|
4
|
+
*
|
|
5
|
+
* 被 run/prompt.js(task-05,进场注入)与 run/quick-audit.js(task-06,--done 断言 WARNING)
|
|
6
|
+
* 两端消费。纯模块纪律:不 console(渲染返回字符串,输出留给消费端)、不写盘、
|
|
7
|
+
* 不 import run/ 下模块(避免与 task-05/06 循环依赖)。
|
|
8
|
+
*
|
|
9
|
+
* git 交互一律走 git-helper 的 safeGit(数组参数不经 shell,Windows 路径安全);失败静默
|
|
10
|
+
* 降级空结果不抛(acceptance:git 不可用 → 全部降级空结果)。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { safeGit } from './git-helper.js'
|
|
15
|
+
import { matchDecisionsByFiles } from './knowledge-match.js'
|
|
16
|
+
|
|
17
|
+
/** 路径反斜杠归一 POSIX(Windows 调用方传 \ 分隔时不丢匹配;与 matchDecisionsByFiles 双侧归一口径一致) */
|
|
18
|
+
function toPosix(p) {
|
|
19
|
+
return String(p || '').trim().replace(/\\/g, '/')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ───────────────────────── 交付归因(FR-03) ─────────────────────────
|
|
23
|
+
|
|
24
|
+
/** 提交信息里的完整变更名标记:init 生成的 change 目录名形态(2026-09-11-cross-change-decision-guard) */
|
|
25
|
+
const CHANGE_NAME_RE = /\d{4}-\d{2}-\d{2}-[a-z0-9][a-z0-9-]*/g
|
|
26
|
+
/** quicklog 条目 ID 标记:ql-YYYYMMDD-NNN-xxxx(quick 会话提交信息的归因键) */
|
|
27
|
+
const QL_ID_RE = /ql-\d{8}-\d{3}-[a-z0-9]{4}/g
|
|
28
|
+
|
|
29
|
+
/** 单条提交信息 → 两类标记列表(按出现位置排序;两类正则无交集,ql-ID 的日期段无连字符) */
|
|
30
|
+
function extractChangeMarkers(subject) {
|
|
31
|
+
const found = []
|
|
32
|
+
for (const re of [CHANGE_NAME_RE, QL_ID_RE]) {
|
|
33
|
+
for (const m of subject.matchAll(re)) found.push({ token: m[0], at: m.index })
|
|
34
|
+
}
|
|
35
|
+
return found.sort((a, b) => a.at - b.at).map(f => f.token)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 近因他者交付归因:每文件查最近 days 天提交信息(--format=%s,仅 subject),解析变更名 /
|
|
40
|
+
* ql-ID 两类标记,取最新一条非 currentChange 标记 → { [file]: changeName }。
|
|
41
|
+
* git log 输出行为提交 subject,无 porcelain 首行前导空格语义(git-helper.js:88 的
|
|
42
|
+
* trim 坑不影响此处),safeGit 默认 trim 安全。
|
|
43
|
+
* @param {{ cwd?: string, files?: string[], currentChange?: string, days?: number }} opts
|
|
44
|
+
* @returns {{ [file]: string }} 键 = POSIX 归一路径;无标记 / 仅本变更标记 / git 失败 /
|
|
45
|
+
* 第 20 个之后的文件 → 不记
|
|
46
|
+
*/
|
|
47
|
+
export function collectRecentForeignDelivery({ cwd, files, currentChange, days = 7 } = {}) {
|
|
48
|
+
const result = {}
|
|
49
|
+
if (!cwd || !Array.isArray(files)) return result
|
|
50
|
+
const list = files.map(toPosix).filter(Boolean).slice(0, 20)
|
|
51
|
+
for (const file of list) {
|
|
52
|
+
const r = safeGit(cwd, ['log', `--since=${days}.days`, '--format=%s', '--', file])
|
|
53
|
+
if (r.error || !r.value) continue
|
|
54
|
+
// git log 新→旧:逐提交找第一条非 currentChange 标记,命中即止(取最新他者标记)
|
|
55
|
+
for (const subject of r.value.split('\n')) {
|
|
56
|
+
const foreign = extractChangeMarkers(subject).find(m => m !== currentChange)
|
|
57
|
+
if (foreign) {
|
|
58
|
+
result[file] = foreign
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ───────────────────────── 断言重写检测(FR-04) ─────────────────────────
|
|
67
|
+
|
|
68
|
+
/** 删除侧断言 token:expect( / assert / t.equal / toBe / toEqual / strictEqual / should. */
|
|
69
|
+
const ASSERTION_TOKEN_RE = /expect\(|assert|t\.equal|toBe|toEqual|strictEqual|should\./
|
|
70
|
+
|
|
71
|
+
/** 测试文件判定:test/ 前缀 或 *.test.* / *_test.* 命名(路径先 POSIX 归一) */
|
|
72
|
+
function isTestFilePath(p) {
|
|
73
|
+
return p.startsWith('test/') || /\.test\./.test(p) || /_test\./.test(p)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 断言重写检测:测试文件跑 git diff HEAD -U0(HEAD 口径非 index——quick step3 先 git add
|
|
78
|
+
* 暂存的时序下裸 diff 恒空转,坑 Grill X-001),hunk 删除侧(- 行)含断言 token → 命中;
|
|
79
|
+
* 纯新增 hunk(无 - 行)不算;样例行封顶 5/文件。
|
|
80
|
+
* @param {{ cwd?: string, files?: string[] }} opts
|
|
81
|
+
* @returns {{ [file]: string[] }} 键 = POSIX 归一测试文件路径,值 = 删除侧含 token 的原行
|
|
82
|
+
* (去 - 前缀);非测试文件不查;无命中 / git 失败 → 不记
|
|
83
|
+
*/
|
|
84
|
+
export function detectAssertionRewrites({ cwd, files } = {}) {
|
|
85
|
+
const result = {}
|
|
86
|
+
if (!cwd || !Array.isArray(files)) return result
|
|
87
|
+
for (const raw of files) {
|
|
88
|
+
const file = toPosix(raw)
|
|
89
|
+
if (!file || !isTestFilePath(file)) continue
|
|
90
|
+
const r = safeGit(cwd, ['diff', 'HEAD', '-U0', '--', file])
|
|
91
|
+
if (r.error || !r.value) continue
|
|
92
|
+
const samples = []
|
|
93
|
+
let inHunk = false
|
|
94
|
+
let deletedLines = []
|
|
95
|
+
const flushHunk = () => {
|
|
96
|
+
if (!inHunk || deletedLines.length === 0) return // 纯新增 hunk(无 - 行)不算
|
|
97
|
+
for (const line of deletedLines) {
|
|
98
|
+
if (samples.length >= 5) break
|
|
99
|
+
if (ASSERTION_TOKEN_RE.test(line)) samples.push(line)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const line of r.value.split('\n')) {
|
|
103
|
+
if (line.startsWith('@@')) {
|
|
104
|
+
flushHunk()
|
|
105
|
+
inHunk = true
|
|
106
|
+
deletedLines = []
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (!inHunk) continue // 文件头(diff --git / index / --- / +++)不进 hunk 统计
|
|
110
|
+
if (line.startsWith('-')) deletedLines.push(line.slice(1)) // + 行与 "\ No newline" 行跳过
|
|
111
|
+
}
|
|
112
|
+
flushHunk()
|
|
113
|
+
if (samples.length > 0) result[file] = samples
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ───────────────────────── advisory 渲染(FR-03/FR-04 组合) ─────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* advisory 文本段渲染:决策命中(matchDecisionsByFiles,knowledge 目录 = specBase/knowledge,
|
|
122
|
+
* task-02 契约)+ 交付归因(collectRecentForeignDelivery 默认 7 天窗)组合成注入文本,
|
|
123
|
+
* 供 task-05 进场注入 / task-06 --done WARNING 拼接;两类均零命中 → ''(不留空段)。
|
|
124
|
+
* @param {{ specBase?: string, cwd?: string, candidateFiles?: string[], currentChange?: string }} opts
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
export function renderSemanticGuardBlock({ specBase, cwd, candidateFiles, currentChange } = {}) {
|
|
128
|
+
if (!Array.isArray(candidateFiles) || candidateFiles.length === 0) return ''
|
|
129
|
+
const files = [...new Set(candidateFiles.map(toPosix).filter(Boolean))]
|
|
130
|
+
if (files.length === 0) return ''
|
|
131
|
+
const decisions = specBase
|
|
132
|
+
? matchDecisionsByFiles(join(specBase, 'knowledge'), files)
|
|
133
|
+
: {}
|
|
134
|
+
const delivery = cwd
|
|
135
|
+
? collectRecentForeignDelivery({ cwd, files, currentChange })
|
|
136
|
+
: {}
|
|
137
|
+
const decisionFiles = files.filter(f => Array.isArray(decisions[f]) && decisions[f].length > 0)
|
|
138
|
+
const deliveryFiles = files.filter(f => delivery[f])
|
|
139
|
+
if (decisionFiles.length === 0 && deliveryFiles.length === 0) return ''
|
|
140
|
+
const lines = ['【语义护栏】改动文件触及既有决策 / 近期他者交付,动手前请对照:']
|
|
141
|
+
if (decisionFiles.length > 0) {
|
|
142
|
+
lines.push(`■ 决策库命中(${decisionFiles.length} 文件)`)
|
|
143
|
+
for (const f of decisionFiles) {
|
|
144
|
+
for (const hit of decisions[f]) {
|
|
145
|
+
const rejected = hit.status === 'rejected'
|
|
146
|
+
lines.push(` - ${f} → ${hit.id} ${hit.title}[${rejected ? 'rejected,已否决,勿复潮' : hit.status || 'unknown'}]`)
|
|
147
|
+
lines.push(` 理由:${hit.reason || '(未记录)'}`)
|
|
148
|
+
if (hit.file) lines.push(` 详见:knowledge/${hit.file}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (deliveryFiles.length > 0) {
|
|
153
|
+
lines.push('■ 近 7 天他者交付归因(最后交付的非本变更标记)')
|
|
154
|
+
for (const f of deliveryFiles) lines.push(` - ${f} ← ${delivery[f]}`)
|
|
155
|
+
}
|
|
156
|
+
return lines.join('\n')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ───────────────────────── 总开关(D-001@v1) ─────────────────────────
|
|
160
|
+
|
|
161
|
+
/** YAML 布尔宽松解析:仅显式 false(含引号形态)为关,其余值一律 true(fail-open,先例 friction-tally R-05) */
|
|
162
|
+
function parseEnabledValue(raw) {
|
|
163
|
+
const v = String(raw || '').trim().replace(/^['"]|['"]$/g, '')
|
|
164
|
+
return v.toLowerCase() !== 'false'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 读 specBase/local.yaml 的 semantic_guard.enabled 总开关(D-001@v1):默认 true;
|
|
169
|
+
* 缺键 / 文件缺失 / 解析异常 → true(fail-open——配置读取异常不该把护栏误判为关)。
|
|
170
|
+
* 轻量行式解析不引 yaml 依赖(先例 friction-tally readFrictionHintEnabled 同款形态),
|
|
171
|
+
* 兼容嵌套(semantic_guard:\n enabled: false)与 flat(semantic_guard.enabled: false);
|
|
172
|
+
* 行尾先归一(Windows CRLF 残留致行式正则整体失配)。
|
|
173
|
+
* @param {string} specBase - spec 根(local.yaml 所在目录)
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
export function readSemanticGuardEnabled(specBase) {
|
|
177
|
+
try {
|
|
178
|
+
const yamlPath = join(specBase, 'local.yaml')
|
|
179
|
+
if (!existsSync(yamlPath)) return true
|
|
180
|
+
const text = String(readFileSync(yamlPath, 'utf8')).replace(/\r\n?/g, '\n')
|
|
181
|
+
let inSection = false
|
|
182
|
+
let sectionIndent = -1
|
|
183
|
+
for (const line of text.split('\n')) {
|
|
184
|
+
// flat 形态优先:键名带点,不与嵌套段头冲突
|
|
185
|
+
const flat = line.match(/^\s*semantic_guard\.enabled\s*:\s*(.*?)\s*(?:#.*)?$/)
|
|
186
|
+
if (flat) return parseEnabledValue(flat[1])
|
|
187
|
+
const section = line.match(/^(\s*)semantic_guard\s*:\s*(?:#.*)?$/)
|
|
188
|
+
if (section) {
|
|
189
|
+
inSection = true
|
|
190
|
+
sectionIndent = section[1].length
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (!inSection) continue
|
|
194
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue
|
|
195
|
+
// 缩进回落到段头层级(或更浅)= semantic_guard 段结束,之后的 enabled 不属于它
|
|
196
|
+
if (/^\s*/.exec(line)[0].length <= sectionIndent) {
|
|
197
|
+
inSection = false
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
const kv = line.match(/^\s*enabled\s*:\s*(.*?)\s*(?:#.*)?$/)
|
|
201
|
+
if (kv) return parseEnabledValue(kv[1])
|
|
202
|
+
}
|
|
203
|
+
return true
|
|
204
|
+
} catch {
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -282,9 +282,14 @@ export class SillyHubMcpClient {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
if (!res.ok) {
|
|
285
|
+
// 401 状态留痕(2026-09-11 用户实测反馈:token 失效被 probe 报成 daemon-unreachable,
|
|
286
|
+
// 误导排障方向——吊销/过期的 shmcp_ 是「重连再配对」能解的,不是 daemon 的问题)。
|
|
287
|
+
// getLastInitStatus() 供 probeSillyHub 类型化 reason;成功/其他失败置 null(不误判旧状态)。
|
|
288
|
+
this._lastInitHttpStatus = res.status;
|
|
285
289
|
if (!quiet) console.warn(`[sillyhub-mcp] initialize → HTTP ${res.status}`);
|
|
286
290
|
return false;
|
|
287
291
|
}
|
|
292
|
+
this._lastInitHttpStatus = null;
|
|
288
293
|
|
|
289
294
|
// 优先取 header mcp-session-id(FastMCP 实测返回);缺失再查 body _meta.sessionId。
|
|
290
295
|
// ⚠️ 无论 header 是否有 session,都必须读完 body(消费 SSE 流)——FastMCP 在
|
|
@@ -391,6 +396,15 @@ export class SillyHubMcpClient {
|
|
|
391
396
|
return result !== null;
|
|
392
397
|
}
|
|
393
398
|
|
|
399
|
+
/**
|
|
400
|
+
* 最近一次 initialize 握手的 HTTP 状态码(null = 无记录/成功/非 HTTP 失败)。
|
|
401
|
+
* 消费方:probeSillyHub 的 token 失效类型化(401 → reason='mcp-token-invalid',
|
|
402
|
+
* 区别于 daemon-unreachable——2026-09-11 用户实测:吊销 token 被误报 daemon 不可达)。
|
|
403
|
+
*/
|
|
404
|
+
getLastInitStatus() {
|
|
405
|
+
return this._lastInitHttpStatus ?? null;
|
|
406
|
+
}
|
|
407
|
+
|
|
394
408
|
/**
|
|
395
409
|
* 列出 daemon 暴露的 MCP tools(JSON-RPC `tools/list`,task-11 路径A 探测用)。
|
|
396
410
|
*
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -229,7 +229,7 @@ export const definition = {
|
|
|
229
229
|
1. 简单项目:几句话整体描述
|
|
230
230
|
2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
|
|
231
231
|
3. 展示完整设计方案(不要逐段停顿,一次性展示)
|
|
232
|
-
4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth
|
|
232
|
+
4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
|
|
233
233
|
5. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
|
|
234
234
|
6. 暂停等待用户确认或修改意见
|
|
235
235
|
|
|
@@ -263,7 +263,7 @@ export const definition = {
|
|
|
263
263
|
|
|
264
264
|
### 注意
|
|
265
265
|
- 不要一次输出大段文字,按模块/Phase 分段
|
|
266
|
-
- 变更名必须以当天日期开头(YYYY-MM-DD
|
|
266
|
+
- 变更名必须以当天日期开头(YYYY-MM-DD-),后跟英文短横线分隔的简短描述(CLI 强制:不合规 exit 2)`,
|
|
267
267
|
outputHint: '用户确认的设计方案',
|
|
268
268
|
optional: false
|
|
269
269
|
},
|
|
@@ -342,7 +342,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
342
342
|
|
|
343
343
|
### 操作
|
|
344
344
|
1. 确认变更目录存在:\`mkdir -p {SPEC_ROOT}/changes/<change-name>\`(Windows 用 \`mkdir {SPEC_ROOT}/changes\\<变更名>\` 或 PowerShell \`New-Item -ItemType Directory -Force -Path {SPEC_ROOT}/changes/<change-name>\`)
|
|
345
|
-
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth
|
|
345
|
+
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth\`;CLI 强制,不合规 exit 2)
|
|
346
346
|
2. 将确认的设计写入 \`{SPEC_ROOT}/changes/<change-name>/design.md\`——优先跑 \`sillyspec design-init --change <change-name>\` 生成骨架再填散文(决策追踪表已按 decisions.md 当前版本预填,逐行补覆盖点后把「待确认」改「已覆盖」;文件清单表骨架就位);存量手写路径仍合法(design-init 对已存在的 design.md 不覆盖)
|
|
347
347
|
3. 对账整理 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(「对话式探索」「提出方案」步已按增量规则随答落盘,本步做对账而非从零写入):
|
|
348
348
|
- decisions.md 是本次变更的决策台账,不是长期术语表
|