sillyspec 3.28.13 → 3.29.1
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/package.json +1 -1
- package/src/blast-surface.js +168 -0
- package/src/ceremony-tier.js +359 -0
- package/src/change-risk-profile.js +72 -199
- package/src/config-schema.js +22 -1
- package/src/decisions-io.js +32 -0
- package/src/design-facts.js +1 -0
- package/src/doctor-diagnostics.js +119 -1
- package/src/index.js +92 -5
- package/src/knowledge-stats.js +25 -0
- package/src/modules.js +28 -0
- package/src/prefill.js +491 -0
- package/src/probe7-anchor-check.js +8 -6
- package/src/quick-gate-profile.js +8 -4
- package/src/quick-session-owner.js +63 -0
- package/src/redlines.js +195 -0
- package/src/review-dispatch.js +473 -1
- package/src/review-material-pack.js +265 -0
- package/src/review-tier.js +185 -41
- package/src/run/archive-distill.js +6 -0
- package/src/run/command.js +158 -87
- package/src/run/complete-handlers.js +82 -1
- package/src/run/complete.js +90 -3
- package/src/run/gate-snapshot.js +30 -1
- package/src/run/gates.js +301 -19
- package/src/run/prompt.js +423 -21
- package/src/run/quick-audit.js +12 -2
- package/src/run/shared.js +36 -0
- package/src/run/verify-quality-scan.js +15 -7
- package/src/scope-audit.js +8 -1
- package/src/span-risk-surface.js +127 -0
- package/src/stage-contract-spec.js +2 -2
- package/src/stage-contract.js +102 -81
- package/src/stage-review.js +26 -2
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +35 -19
- package/src/stages/execute.js +11 -3
- package/src/stages/plan.js +45 -24
- package/src/stages/scan.js +1 -1
- package/src/stages/verify.js +7 -7
- package/src/taskcard.js +3 -0
- package/src/verify-postcheck.js +375 -20
- package/src/verify-probes.js +194 -10
- package/src/worktree-apply.js +1 -1
- package/src/worktree.js +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +1 -1
package/src/redlines.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redlines.js —— 红线机检评估器(2026-09-20-redline-machine-check)
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要(2026-09-19 双实现对撞实验核心教训):语义级设计红线(「已结束会话
|
|
5
|
+
* 不得假运行」「用量归属不得后写覆盖」)活在历史 design/knowledge 散文里,无机器
|
|
6
|
+
* 可查形态——三层独立评审 + 352 例测试拦不住两处红线失分,靠事后人工逐文件深读
|
|
7
|
+
* 才发现。本模块把红线转成机器可查断言:消费者仓 `.sillyspec/redlines.yaml` 条目
|
|
8
|
+
* = forbid/require 正则 × scope 文件 glob × severity × origin 文档锚。
|
|
9
|
+
*
|
|
10
|
+
* 定位与边界(decisions.md D-001~D-004):
|
|
11
|
+
* - v1 = 模式级断言(人写正则人负责;statement/origin 保留散文溯源);不做
|
|
12
|
+
* AST/语义级,不做散文自动提炼。
|
|
13
|
+
* - 机制与内容分离:清单消费者仓自持;本仓缺清单 = 不适用(零打扰)。
|
|
14
|
+
* - fail-open 全链:坏 yaml→不适用+注记;单条求值异常→跳过+warn;任何红线面
|
|
15
|
+
* 故障不炸 verify 主链(探针 11 接线侧 try/catch fail-soft,同探针 8/9/10)。
|
|
16
|
+
* - advisory:探针渲染 ❌/⚠️ 进 verify-result 骨架供 agent 裁定;不进 PASS
|
|
17
|
+
* 封顶(D-003:攒误报率数据后另案升硬门)。
|
|
18
|
+
*
|
|
19
|
+
* 纯函数纪律:root 注入,不读 env/时钟;fs 只经 resolveScopeFiles 的 walk。
|
|
20
|
+
* 零新增外部依赖(js-yaml 已有)。
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
23
|
+
import { join, relative } from 'path';
|
|
24
|
+
import jsYaml from 'js-yaml';
|
|
25
|
+
|
|
26
|
+
/** scope walk 排除目录(依赖/版本库/工具自身产物——含 .sillyspec:清单/进度文件不得喂饱 require 造成假阴性,S2 审查 D1)。 */
|
|
27
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.sillyspec', '.runtime', 'dist', 'build', '.next', '__pycache__']);
|
|
28
|
+
|
|
29
|
+
/** 单文件扫描上限:2MB(大产物/锁文件跳过,warn 留痕)。 */
|
|
30
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
/** glob → 正则:`**` 跨层、`*` 单段(不跨 /);段全等不认长名(src-guide 不蹭 src)。 */
|
|
33
|
+
function globToRegExp(glob) {
|
|
34
|
+
const esc = String(glob).replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
35
|
+
return new RegExp(
|
|
36
|
+
'^' + esc.replace(/\*\*/g, '⟦⟦DBL⟧⟧').replace(/\*/g, '⟦SGL⟧')
|
|
37
|
+
.replace(/⟦⟦DBL⟧⟧/g, '(?:.*)').replace(/⟦SGL⟧/g, '[^/]*') + '$',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 递归收集 root 下全部文件(排除 SKIP_DIRS;返回仓根相对 POSIX 路径)。 */
|
|
42
|
+
function walkFiles(root) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const visit = (dir) => {
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
const p = join(dir, e.name);
|
|
53
|
+
if (e.isDirectory()) {
|
|
54
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
55
|
+
visit(p);
|
|
56
|
+
} else if (e.isFile()) {
|
|
57
|
+
out.push(relative(root, p).replace(/\\/g, '/'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
visit(root);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 解析 redlines.yaml 文本为条目数组(纯函数)。
|
|
67
|
+
* 无效条目四判据(跳过 + warnings 留痕):缺 id / 缺 scope / forbid 与 require
|
|
68
|
+
* 全空 / 任一正则编译失败。severity 缺省 'error'。
|
|
69
|
+
* @param {string} yamlText
|
|
70
|
+
* @returns {{ entries: Array<object>, warnings: string[] }}
|
|
71
|
+
*/
|
|
72
|
+
export function parseRedlines(yamlText) {
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const doc = jsYaml.load(yamlText);
|
|
75
|
+
const rawList = doc && Array.isArray(doc.redlines) ? doc.redlines : [];
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const raw of rawList) {
|
|
78
|
+
if (!raw || typeof raw !== 'object') {
|
|
79
|
+
warnings.push('跳过非对象条目');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const id = typeof raw.id === 'string' && raw.id.trim() !== '' ? raw.id.trim() : null;
|
|
83
|
+
if (id === null) { warnings.push('跳过缺 id 条目'); continue; }
|
|
84
|
+
const scope = Array.isArray(raw.scope) ? raw.scope.filter((s) => typeof s === 'string' && s !== '') : [];
|
|
85
|
+
if (scope.length === 0) { warnings.push(`条目 ${id} 缺 scope,跳过`); continue; }
|
|
86
|
+
const forbid = Array.isArray(raw.forbid) ? raw.forbid.filter((s) => typeof s === 'string' && s !== '') : [];
|
|
87
|
+
const require_ = Array.isArray(raw.require) ? raw.require.filter((s) => typeof s === 'string' && s !== '') : [];
|
|
88
|
+
if (forbid.length === 0 && require_.length === 0) {
|
|
89
|
+
warnings.push(`条目 ${id} 的 forbid 与 require 全空,跳过`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const severity = raw.severity === 'warning' ? 'warning' : 'error';
|
|
93
|
+
const entry = {
|
|
94
|
+
id,
|
|
95
|
+
statement: typeof raw.statement === 'string' ? raw.statement : '',
|
|
96
|
+
scope,
|
|
97
|
+
forbid,
|
|
98
|
+
require: require_,
|
|
99
|
+
severity,
|
|
100
|
+
origin: typeof raw.origin === 'string' ? raw.origin : null,
|
|
101
|
+
_res: { forbid: [], require: [] },
|
|
102
|
+
};
|
|
103
|
+
let badRe = false;
|
|
104
|
+
for (const re of forbid) {
|
|
105
|
+
try { entry._res.forbid.push(new RegExp(re, 'm')); } catch (e) { warnings.push(`条目 ${id} forbid 正则编译失败(${re}),跳过该条`); badRe = true; }
|
|
106
|
+
}
|
|
107
|
+
for (const re of require_) {
|
|
108
|
+
try { entry._res.require.push(new RegExp(re, 'm')); } catch (e) { warnings.push(`条目 ${id} require 正则编译失败(${re}),跳过该条`); badRe = true; }
|
|
109
|
+
}
|
|
110
|
+
if (badRe) continue;
|
|
111
|
+
entries.push(entry);
|
|
112
|
+
}
|
|
113
|
+
return { entries, warnings };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* scope glob 列表 → 命中文件集(root 下仓根相对 POSIX 路径,排序去重)。
|
|
118
|
+
* @param {string[]} scopeGlobs
|
|
119
|
+
* @param {string} root
|
|
120
|
+
* @returns {string[]}
|
|
121
|
+
*/
|
|
122
|
+
export function resolveScopeFiles(scopeGlobs, root) {
|
|
123
|
+
const all = walkFiles(root);
|
|
124
|
+
const res = (Array.isArray(scopeGlobs) ? scopeGlobs : [])
|
|
125
|
+
.filter((g) => typeof g === 'string' && g !== '')
|
|
126
|
+
.map((g) => globToRegExp(g));
|
|
127
|
+
return [...new Set(all.filter((f) => res.some((r) => r.test(f))))].sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 逐条评估红线:forbid 命中收集 file:line + require 缺失 + severity 透传。
|
|
132
|
+
* 单条求值异常跳过 + warnings(fail-open,D-004)。
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {Array<object>} opts.entries - parseRedlines 产物
|
|
135
|
+
* @param {string} opts.root - 求值根(worktree 或仓根,绝对路径)
|
|
136
|
+
* @returns {{ applicable: boolean, entryCount: number, findings: Array<object>, warnings: string[] }}
|
|
137
|
+
*/
|
|
138
|
+
export function evaluateRedlines({ entries, root }) {
|
|
139
|
+
const warnings = [];
|
|
140
|
+
const findings = [];
|
|
141
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
142
|
+
for (const entry of list) {
|
|
143
|
+
try {
|
|
144
|
+
const files = resolveScopeFiles(entry.scope, root);
|
|
145
|
+
const contents = new Map();
|
|
146
|
+
const readIfBig = (rel) => {
|
|
147
|
+
if (!contents.has(rel)) {
|
|
148
|
+
const abs = join(root, rel);
|
|
149
|
+
let text = '';
|
|
150
|
+
try {
|
|
151
|
+
if (statSync(abs).size > MAX_FILE_BYTES) {
|
|
152
|
+
warnings.push(`条目 ${entry.id}:${rel} 超 2MB 跳过`);
|
|
153
|
+
} else {
|
|
154
|
+
text = readFileSync(abs, 'utf8');
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
warnings.push(`条目 ${entry.id}:读 ${rel} 失败(${e && e.code ? e.code : 'error'}),跳过该文件`);
|
|
158
|
+
}
|
|
159
|
+
contents.set(rel, text);
|
|
160
|
+
}
|
|
161
|
+
return contents.get(rel);
|
|
162
|
+
};
|
|
163
|
+
for (const rel of files) {
|
|
164
|
+
const text = readIfBig(rel);
|
|
165
|
+
if (!text) continue;
|
|
166
|
+
const lines = text.split('\n');
|
|
167
|
+
for (const re of entry._res ? entry._res.forbid : []) {
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
const m = lines[i] && re.exec(lines[i]);
|
|
170
|
+
if (m) {
|
|
171
|
+
findings.push({
|
|
172
|
+
id: entry.id, severity: entry.severity, kind: 'forbid',
|
|
173
|
+
file: rel, line: i + 1, snippet: String(m[0]).slice(0, 160),
|
|
174
|
+
statement: entry.statement, origin: entry.origin,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const re of entry._res ? entry._res.require : []) {
|
|
181
|
+
const satisfied = [...contents.values()].some((text) => re.test(text));
|
|
182
|
+
if (!satisfied) {
|
|
183
|
+
findings.push({
|
|
184
|
+
id: entry.id, severity: 'warning', kind: 'require-missing',
|
|
185
|
+
statement: entry.statement, origin: entry.origin,
|
|
186
|
+
detail: `require 模式在 scope 全集(${files.length} 文件)无命中`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
warnings.push(`条目 ${entry.id} 求值异常跳过(${e && e.message ? e.message : e})`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { applicable: true, entryCount: list.length, findings, warnings };
|
|
195
|
+
}
|