agentic-flow-kit 0.2.0
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/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/flow-kit.mjs +4 -0
- package/modules/gates/dotnet-ca/README.md +24 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +128 -0
- package/modules/gates/dotnet-ca/local-pre-commit.line +1 -0
- package/modules/hosts/omp/agents/implementer.md +12 -0
- package/modules/hosts/omp/agents/independent-reviewer.md +11 -0
- package/modules/hosts/omp/agents/ui-verifier.md +12 -0
- package/modules/hosts/opencode/agents/implementer.md +14 -0
- package/modules/hosts/opencode/agents/independent-reviewer.md +10 -0
- package/modules/hosts/opencode/agents/ui-verifier.md +10 -0
- package/modules/hosts/opencode/commands/build.md +13 -0
- package/modules/hosts/opencode/commands/deploy.md +12 -0
- package/modules/hosts/opencode/commands/design.md +12 -0
- package/modules/hosts/opencode/commands/maintain.md +12 -0
- package/modules/hosts/opencode/commands/new-task.md +12 -0
- package/modules/hosts/opencode/commands/plan.md +12 -0
- package/modules/hosts/opencode/commands/review.md +12 -0
- package/modules/hosts/opencode/commands/test.md +14 -0
- package/modules/hosts/trae/agents/implementer.md +9 -0
- package/modules/hosts/trae/agents/independent-reviewer.md +8 -0
- package/modules/hosts/trae/agents/ui-verifier.md +12 -0
- package/modules/hosts/trae/commands/wf-build.md +14 -0
- package/modules/hosts/trae/commands/wf-deploy.md +13 -0
- package/modules/hosts/trae/commands/wf-design.md +13 -0
- package/modules/hosts/trae/commands/wf-maintain.md +13 -0
- package/modules/hosts/trae/commands/wf-new-task.md +13 -0
- package/modules/hosts/trae/commands/wf-plan.md +13 -0
- package/modules/hosts/trae/commands/wf-review.md +13 -0
- package/modules/hosts/trae/commands/wf-test.md +14 -0
- package/modules/hosts/trae/hooks/post-edit-check.cjs +87 -0
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +151 -0
- package/modules/hosts/trae/hooks/resolve-shell.cjs +21 -0
- package/modules/hosts/trae/hooks.json +29 -0
- package/modules/hosts/trae/rules/beautify-input-chinese.md +6 -0
- package/modules/hosts/trae/rules/git-commit-message.md +10 -0
- package/modules/hosts/trae/rules/project-workflow.md +14 -0
- package/modules/hosts/zcode/agents/implementer.md +11 -0
- package/modules/hosts/zcode/agents/independent-reviewer.md +10 -0
- package/modules/hosts/zcode/agents/ui-verifier.md +10 -0
- package/package.json +34 -0
- package/src/add-gate.mjs +83 -0
- package/src/add-host.mjs +63 -0
- package/src/cli.mjs +81 -0
- package/src/doctor.mjs +150 -0
- package/src/init.mjs +190 -0
- package/src/profiles.mjs +101 -0
- package/src/render.mjs +68 -0
- package/src/run-tests.mjs +35 -0
- package/src/sync.mjs +141 -0
- package/src/sync.test.mjs +207 -0
- package/templates/.gitattributes +6 -0
- package/templates/AGENTS.md +52 -0
- package/templates/_agents/board/index.html +382 -0
- package/templates/_agents/board/marked.min.js +6 -0
- package/templates/_agents/cache/kb-index.json +1 -0
- package/templates/_agents/commands/build.md +74 -0
- package/templates/_agents/commands/deploy.md +65 -0
- package/templates/_agents/commands/design.md +54 -0
- package/templates/_agents/commands/maintain.md +91 -0
- package/templates/_agents/commands/new-task.md +73 -0
- package/templates/_agents/commands/plan.md +46 -0
- package/templates/_agents/commands/review.md +59 -0
- package/templates/_agents/commands/test.md +80 -0
- package/templates/_agents/hooks/check-pairing-incremental.sh +29 -0
- package/templates/_agents/hooks/check-wiki-ledger.sh +18 -0
- package/templates/_agents/hooks/commit-check.cjs +140 -0
- package/templates/_agents/hooks/local-pre-commit +6 -0
- package/templates/_agents/notes/runtime-env.md +18 -0
- package/templates/_agents/roles/implementer.md +46 -0
- package/templates/_agents/roles/independent-reviewer.md +43 -0
- package/templates/_agents/roles/ui-verifier.md +42 -0
- package/templates/_agents/rule-budgets.txt +8 -0
- package/templates/_agents/scripts/agg-delegations.cjs +175 -0
- package/templates/_agents/scripts/check-loop.sh +803 -0
- package/templates/_agents/scripts/check-loop.test.sh +1042 -0
- package/templates/_agents/scripts/ensure-board.ps1 +48 -0
- package/templates/_agents/scripts/gen-wiki-board.mjs +197 -0
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +136 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +190 -0
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +113 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +172 -0
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +101 -0
- package/templates/_agents/scripts/kb-cache-evict.mjs +50 -0
- package/templates/_agents/scripts/kb-cache-evict.test.mjs +104 -0
- package/templates/_agents/scripts/kb-search.mjs +307 -0
- package/templates/_agents/scripts/kb-search.test.mjs +153 -0
- package/templates/_agents/scripts/rule-budget.sh +68 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +192 -0
- package/templates/_agents/scripts/wiki-search.mjs +19 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +244 -0
- package/templates/_agents/skills/verify-ui/SKILL.md +24 -0
- package/templates/_agents/skills/wiki/SKILL.md +104 -0
- package/templates/_agents/workflow-modules.txt +11 -0
- package/templates/_githooks/commit-msg +15 -0
- package/templates/_githooks/post-commit +10 -0
- package/templates/_githooks/pre-commit +58 -0
- package/templates/_githooks/pre-merge-commit +24 -0
- package/templates/_githooks/pre-push +32 -0
- package/templates/wiki/INDEX.md +23 -0
- package/templates/wiki/drafts-archive/README.md +3 -0
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +56 -0
- package/templates/workflow/README.md +52 -0
- package/templates/workflow/delegations.md +28 -0
- package/templates/workflow/incidents/_TEMPLATE.md +42 -0
- package/templates/workflow/intents/_TEMPLATE.md +40 -0
- package/templates/workflow/papercuts.md +7 -0
- package/templates/workflow/plans/_TEMPLATE.md +38 -0
- package/templates/workflow/regression-checklist.md +22 -0
- package/templates/workflow/specs/_TEMPLATE.md +41 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 跨语料检索入口(2026-09-21,intent 2026-09-21-workflow-doc-retrieval)
|
|
3
|
+
// 用法:node .agents/scripts/kb-search.mjs [选项] <词1> [词2 ...](多词 AND,全部命中才算)
|
|
4
|
+
//
|
|
5
|
+
// 语料与口径
|
|
6
|
+
// workflow(流程留痕):只搜 frontmatter + H1 + 指定节(背景 / 目标 / 根因 / 复盘三件套 / 验收标准 / 影响面),
|
|
7
|
+
// 默认全状态可命中、活跃(draft/approved/open)排序 ×1.3 优先;输出带「节名 + 行号」便于节级读取。
|
|
8
|
+
// wiki(领域知识):沿用 2026-09-21 降噪口径——默认跳过 .json/.csv、*.visual-check.* 与 >180KB bulk 文件。
|
|
9
|
+
// 两侧排序均为 命中数/√文件KB(防大文件靠体量霸榜);`-n` 为每语料展示上限。
|
|
10
|
+
// 结构缓存(2026-09-23 intent kb-search-structure-cache):可检索子集预提取进 .agents/cache/kb-index.json,
|
|
11
|
+
// 按 [mtimeMs,size] 逐文件失效,口径指纹变化整份作废;匹配/排序/输出路径不变(三路输出与冷扫逐字节一致);
|
|
12
|
+
// 缓存任何读写异常静默回退全扫。`--include-generated` 走纯扫(bulk 文件不入缓存)。
|
|
13
|
+
// 测试:node .agents/scripts/kb-search.test.mjs(fixture 回归,须全绿)
|
|
14
|
+
// 模块词表:.agents/workflow-modules.txt(--module 取值校验,单源)
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const MODULES_FILE = path.join(SCRIPT_DIR, '..', 'workflow-modules.txt');
|
|
22
|
+
|
|
23
|
+
const USAGE = `用法: node .agents/scripts/kb-search.mjs [选项] <词1> [词2 ...](多词 AND)
|
|
24
|
+
|
|
25
|
+
--scope <workflow|wiki|all> 检索语料,默认 all(指定 -t 时默认转 wiki)
|
|
26
|
+
--type <intents,specs,plans,incidents> workflow:限定文档类型(逗号分隔)
|
|
27
|
+
--module <词表token> workflow:按 frontmatter 模块过滤(词表见 .agents/workflow-modules.txt)
|
|
28
|
+
--status <all|active|terminal> workflow:状态过滤,默认 all(active = draft/approved/open)
|
|
29
|
+
-t <主题> wiki:限定主题目录(如 数据维护)
|
|
30
|
+
-n <N> 每语料展示文件数上限,默认 10;0 = 不限
|
|
31
|
+
--include-generated wiki:纳入默认跳过的 json/csv、*.visual-check.* 与 >180KB bulk 文件(走纯扫,不吃缓存)
|
|
32
|
+
--no-cache 禁用结构缓存,全量扫(与缓存热路径输出逐字节一致)
|
|
33
|
+
--rebuild 忽略现有缓存重建后回写
|
|
34
|
+
--help 打印本说明
|
|
35
|
+
|
|
36
|
+
退出码:0 正常(含无命中);1 参数错误(无词 / 未知选项 / 非法枚举值)`;
|
|
37
|
+
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
let scope = '';
|
|
40
|
+
let scopeExplicit = false;
|
|
41
|
+
let typeFilter = null;
|
|
42
|
+
let moduleFilter = '';
|
|
43
|
+
let statusFilter = 'all';
|
|
44
|
+
let topicFilter = '';
|
|
45
|
+
let limit = 10;
|
|
46
|
+
let limitRaw = '10';
|
|
47
|
+
let includeGenerated = false;
|
|
48
|
+
let noCache = false;
|
|
49
|
+
let rebuild = false;
|
|
50
|
+
const words = [];
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const a = args[i];
|
|
53
|
+
if (a === '--scope') { scope = args[++i] || ''; scopeExplicit = true; }
|
|
54
|
+
else if (a === '--type') typeFilter = (args[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
55
|
+
else if (a === '--module') moduleFilter = args[++i] || '';
|
|
56
|
+
else if (a === '--status') statusFilter = args[++i] || '';
|
|
57
|
+
else if (a === '-t') topicFilter = args[++i] || '';
|
|
58
|
+
else if (a === '-n') { limitRaw = args[++i] ?? ''; limit = Number(limitRaw); }
|
|
59
|
+
else if (a === '--include-generated') includeGenerated = true;
|
|
60
|
+
else if (a === '--no-cache') noCache = true;
|
|
61
|
+
else if (a === '--rebuild') rebuild = true;
|
|
62
|
+
else if (a === '--help' || a === '-h') { console.log(USAGE); process.exit(0); }
|
|
63
|
+
else words.push(a);
|
|
64
|
+
}
|
|
65
|
+
if (!words.length) { console.error(USAGE); process.exit(1); }
|
|
66
|
+
if (!Number.isFinite(limit) || limit < 0) { console.error(`-n 须为非负整数(现 ${limitRaw})\n\n${USAGE}`); process.exit(1); }
|
|
67
|
+
if (!scope) scope = topicFilter ? 'wiki' : 'all';
|
|
68
|
+
if (!['workflow', 'wiki', 'all'].includes(scope)) { console.error(`--scope 须为 workflow|wiki|all(现 ${scope})\n\n${USAGE}`); process.exit(1); }
|
|
69
|
+
if (!['all', 'active', 'terminal'].includes(statusFilter)) { console.error(`--status 须为 all|active|terminal(现 ${statusFilter})\n\n${USAGE}`); process.exit(1); }
|
|
70
|
+
const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
|
|
71
|
+
if (typeFilter && typeFilter.some((t) => !DOC_TYPES.includes(t))) {
|
|
72
|
+
console.error(`--type 取值须为 ${DOC_TYPES.join('|')}(现 ${typeFilter.join(',')})\n\n${USAGE}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const MODULES = fs.readFileSync(MODULES_FILE, 'utf8').split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
|
|
76
|
+
if (moduleFilter && !MODULES.includes(moduleFilter)) {
|
|
77
|
+
console.error(`--module 未知模块:${moduleFilter}(词表见 .agents/workflow-modules.txt:${MODULES.join(' / ')})\n\n${USAGE}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ACTIVE_STATUS = ['draft', 'approved', 'open']; // 与 workflow/INDEX.md 活跃口径一致(fixed 归档案,默认也可命中)
|
|
82
|
+
const WF_SECTIONS = ['背景', '目标', '根因', '复盘三件套', '验收标准', '影响面'];
|
|
83
|
+
const WIKI_EXCLUDE = new Set(['INDEX.md', '知识沉淀总览.html']);
|
|
84
|
+
const WIKI_MAX_KB = 180;
|
|
85
|
+
|
|
86
|
+
// ---- frontmatter(受限子集:每行 `键: 值`)+ H1 标题(去「INTENT — 」前缀)----
|
|
87
|
+
function parseDoc(text) {
|
|
88
|
+
const meta = {};
|
|
89
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
90
|
+
if (m) {
|
|
91
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
92
|
+
const kv = line.match(/^(\S+):\s*(.*)$/);
|
|
93
|
+
if (kv) meta[kv[1].trim()] = kv[2].trim();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const h1 = text.match(/^# (.+)$/m);
|
|
97
|
+
let title = h1 ? h1[1] : '';
|
|
98
|
+
const dash = title.match(/\S+\s*[—–-]+\s*(.+)$/);
|
|
99
|
+
if (dash) title = dash[1];
|
|
100
|
+
return { meta, title };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- workflow 侧可检索行:frontmatter + H1 + 指定节(节名随 `## ` 标题滚动);词过滤在调用方,缓存存词无关子集 ----
|
|
104
|
+
function workflowLines(text) {
|
|
105
|
+
const lines = text.split(/\r?\n/);
|
|
106
|
+
const out = [];
|
|
107
|
+
let i = 0;
|
|
108
|
+
if (/^---\s*$/.test(lines[0] || '')) {
|
|
109
|
+
out.push({ ln: 1, section: '(frontmatter)', line: lines[0] });
|
|
110
|
+
for (i = 1; i < lines.length; i++) {
|
|
111
|
+
out.push({ ln: i + 1, section: '(frontmatter)', line: lines[i] });
|
|
112
|
+
if (/^---\s*$/.test(lines[i])) { i++; break; }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let section = '';
|
|
116
|
+
for (; i < lines.length; i++) {
|
|
117
|
+
const line = lines[i];
|
|
118
|
+
const h2 = line.match(/^## (.+?)\s*$/);
|
|
119
|
+
if (h2) section = h2[1];
|
|
120
|
+
if (/^# /.test(line) && !/^## /.test(line)) { out.push({ ln: i + 1, section: '(标题)', line }); section = ''; continue; }
|
|
121
|
+
if (WF_SECTIONS.some((s) => section.startsWith(s))) out.push({ ln: i + 1, section, line });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function matchWords(lines, words) {
|
|
127
|
+
const byWord = words.map((w) => lines.filter((x) => x.line.includes(w)));
|
|
128
|
+
if (byWord.some((h) => h.length === 0)) return null; // AND:任一词未命中即跳过
|
|
129
|
+
const total = byWord.reduce((n, h) => n + h.length, 0);
|
|
130
|
+
const shown = [...new Map(lines.map((x) => [x.ln, x])).values()].sort((a, b) => a.ln - b.ln).slice(0, 3);
|
|
131
|
+
return { total, shown };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- 结构缓存:可检索子集预提取,[mtimeMs,size] 两元组逐文件失效,指纹变即整份作废 ----
|
|
135
|
+
// 键 = <检索根 cwd>::<相对路径>(多语料根共存一文件,fixture 临时目录天然隔离);读写任何异常静默回退全扫。
|
|
136
|
+
const CACHE_DIR = path.join(SCRIPT_DIR, '..', 'cache');
|
|
137
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'kb-index.json');
|
|
138
|
+
const ROOT_NS = `${path.resolve('.')}::`;
|
|
139
|
+
const CFG_FP = createHash('sha256').update(JSON.stringify({
|
|
140
|
+
v: 1, WF_SECTIONS, ACTIVE_STATUS, WIKI_MAX_KB, excl: [...WIKI_EXCLUDE],
|
|
141
|
+
exts: ['md', 'sql', 'html', 'txt', 'gen:csv|json'],
|
|
142
|
+
fns: [parseDoc.toString(), workflowLines.toString(), matchWords.toString()],
|
|
143
|
+
})).digest('hex').slice(0, 16);
|
|
144
|
+
const cacheUsable = !noCache && !includeGenerated;
|
|
145
|
+
let cache = null;
|
|
146
|
+
const touched = new Set();
|
|
147
|
+
if (cacheUsable) {
|
|
148
|
+
try {
|
|
149
|
+
const j = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
150
|
+
cache = j && j.fp === CFG_FP && j.files && typeof j.files === 'object' ? j.files : {};
|
|
151
|
+
} catch { cache = {}; }
|
|
152
|
+
if (rebuild) cache = {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const serializeLines = (lines, kind) => lines.map((x) => (kind === 'wf' ? [x.ln, x.section, x.line] : [x.ln, x.line]));
|
|
156
|
+
const reviveLines = (lines, kind) => lines.map((a) => (kind === 'wf' ? { ln: a[0], section: a[1], line: a[2] } : { ln: a[0], line: a[1] }));
|
|
157
|
+
|
|
158
|
+
// 取一个源文件的缓存条目;miss 时 build()(读全文提取可检索子集)并登记回写。返回 {st, meta?, title?, lines}
|
|
159
|
+
function cachedEntry(relPath, kind, build) {
|
|
160
|
+
const abs = path.resolve(relPath);
|
|
161
|
+
if (!cache) {
|
|
162
|
+
const e = build();
|
|
163
|
+
e.st = [fs.statSync(abs).mtimeMs, fs.statSync(abs).size];
|
|
164
|
+
return e;
|
|
165
|
+
}
|
|
166
|
+
const key = ROOT_NS + relPath.split(path.sep).join('/');
|
|
167
|
+
const st = fs.statSync(abs);
|
|
168
|
+
const sig = [st.mtimeMs, st.size];
|
|
169
|
+
const e = cache[key];
|
|
170
|
+
if (e && e.kind === kind && Array.isArray(e.st) && e.st[0] === sig[0] && e.st[1] === sig[1]) {
|
|
171
|
+
touched.add(key);
|
|
172
|
+
return { st: e.st, meta: e.meta, title: e.title, lines: reviveLines(e.lines, kind) };
|
|
173
|
+
}
|
|
174
|
+
const built = build();
|
|
175
|
+
cache[key] = {
|
|
176
|
+
st: sig, kind,
|
|
177
|
+
...(kind === 'wf' ? { meta: built.meta, title: built.title } : {}),
|
|
178
|
+
lines: serializeLines(built.lines, kind),
|
|
179
|
+
};
|
|
180
|
+
touched.add(key);
|
|
181
|
+
return { st: sig, meta: built.meta, title: built.title, lines: built.lines };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function saveCache() {
|
|
185
|
+
if (!cache) return;
|
|
186
|
+
try {
|
|
187
|
+
for (const k of Object.keys(cache)) {
|
|
188
|
+
if (!k.startsWith(ROOT_NS) || touched.has(k)) continue;
|
|
189
|
+
const rel = k.slice(ROOT_NS.length).replace(/\//g, path.sep);
|
|
190
|
+
try {
|
|
191
|
+
const st = fs.statSync(rel);
|
|
192
|
+
const e = cache[k];
|
|
193
|
+
if (e.st[0] !== st.mtimeMs || e.st[1] !== st.size) delete cache[k];
|
|
194
|
+
} catch { delete cache[k]; }
|
|
195
|
+
}
|
|
196
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
197
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ fp: CFG_FP, files: cache }));
|
|
198
|
+
} catch { /* 只读目录 / 并发写等异常:放弃回写,下次重建 */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- workflow 检索 ----
|
|
202
|
+
function searchWorkflow(words) {
|
|
203
|
+
const results = [];
|
|
204
|
+
for (const type of (typeFilter || DOC_TYPES)) {
|
|
205
|
+
const dir = path.join('workflow', type);
|
|
206
|
+
if (!fs.existsSync(dir)) continue;
|
|
207
|
+
for (const f of fs.readdirSync(dir)) {
|
|
208
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
209
|
+
const p = path.join(dir, f);
|
|
210
|
+
const e = cachedEntry(p, 'wf', () => {
|
|
211
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
212
|
+
const { meta, title } = parseDoc(text);
|
|
213
|
+
return { meta, title, lines: workflowLines(text) };
|
|
214
|
+
});
|
|
215
|
+
const meta = e.meta || {};
|
|
216
|
+
const status = meta['状态'] || '';
|
|
217
|
+
const module = meta['模块'] || '—';
|
|
218
|
+
if (moduleFilter && module !== moduleFilter) continue;
|
|
219
|
+
if (statusFilter === 'active' && !ACTIVE_STATUS.includes(status)) continue;
|
|
220
|
+
if (statusFilter === 'terminal' && ACTIVE_STATUS.includes(status)) continue;
|
|
221
|
+
const hits = matchWords(e.lines.filter((x) => words.some((w) => x.line.includes(w))), words);
|
|
222
|
+
if (!hits) continue;
|
|
223
|
+
const kb = e.st[1] / 1024;
|
|
224
|
+
const active = ACTIVE_STATUS.includes(status);
|
|
225
|
+
results.push({
|
|
226
|
+
file: `${type}/${f}`, title: e.title || '', status, level: meta['级别'] || '—', module,
|
|
227
|
+
total: hits.total, shown: hits.shown, kb,
|
|
228
|
+
score: (hits.total / Math.sqrt(Math.max(kb, 0.1))) * (active ? 1.3 : 1),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return results.sort((a, b) => b.score - a.score);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- wiki 检索(口径同 2026-09-21 降噪版 wiki-search)----
|
|
236
|
+
const WIKI_EXTS = includeGenerated ? /\.(md|sql|html|csv|json|txt)$/i : /\.(md|sql|html|txt)$/i;
|
|
237
|
+
const walkWiki = (dir, rel) => {
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const e of fs.readdirSync(dir)) {
|
|
240
|
+
const p = path.join(dir, e);
|
|
241
|
+
if (fs.statSync(p).isDirectory()) { out.push(...walkWiki(p, `${rel}/${e}`)); continue; }
|
|
242
|
+
if (!WIKI_EXTS.test(e) || WIKI_EXCLUDE.has(e)) continue;
|
|
243
|
+
out.push({ fsPath: p, key: `${rel}/${e}` });
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function searchWiki(words) {
|
|
249
|
+
const results = [];
|
|
250
|
+
let skipped = 0;
|
|
251
|
+
if (!fs.existsSync('wiki')) return { results, skipped };
|
|
252
|
+
for (const d of fs.readdirSync('wiki')) {
|
|
253
|
+
const dir = path.join('wiki', d);
|
|
254
|
+
if (!fs.statSync(dir).isDirectory() || d === 'drafts-archive') continue;
|
|
255
|
+
if (topicFilter && d !== topicFilter) continue;
|
|
256
|
+
for (const { fsPath: p, key } of walkWiki(dir, d)) {
|
|
257
|
+
const size = fs.statSync(p).size;
|
|
258
|
+
if (!includeGenerated && (/\.visual-check\./i.test(path.basename(p)) || size > WIKI_MAX_KB * 1024)) { skipped++; continue; }
|
|
259
|
+
const e = cachedEntry(p, 'wiki', () => {
|
|
260
|
+
let text;
|
|
261
|
+
try { text = fs.readFileSync(p, 'utf8'); } catch { return { lines: [] }; }
|
|
262
|
+
return { lines: text.split('\n').map((line, i) => ({ ln: i + 1, line })) };
|
|
263
|
+
});
|
|
264
|
+
if (!e.lines.length) continue;
|
|
265
|
+
const hits = matchWords(e.lines.filter((x) => words.some((w) => x.line.includes(w))), words);
|
|
266
|
+
if (!hits) continue;
|
|
267
|
+
const kb = size / 1024;
|
|
268
|
+
results.push({ file: key, total: hits.total, shown: hits.shown, kb, score: hits.total / Math.sqrt(Math.max(kb, 0.1)) });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { results: results.sort((a, b) => b.score - a.score), skipped };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---- 输出 ----
|
|
275
|
+
const top = (arr) => (limit ? arr.slice(0, limit) : arr);
|
|
276
|
+
let anyHits = 0;
|
|
277
|
+
|
|
278
|
+
if (scope === 'workflow' || scope === 'all') {
|
|
279
|
+
const res = searchWorkflow(words);
|
|
280
|
+
anyHits += res.length;
|
|
281
|
+
const shown = top(res);
|
|
282
|
+
console.log(`── workflow(${res.length} 个文件命中${res.length > shown.length ? `,展示前 ${shown.length}` : ''})──`);
|
|
283
|
+
for (const r of shown) {
|
|
284
|
+
console.log(`📄 workflow/${r.file} — ${r.status || '未填状态'} · ${r.level} · ${r.module} · ${r.total} 处命中 / ${r.kb < 10 ? r.kb.toFixed(1) : Math.round(r.kb)}KB`);
|
|
285
|
+
for (const h of r.shown) console.log(` [${h.section}] L${h.ln}: ${h.line.trim().slice(0, 130)}`);
|
|
286
|
+
}
|
|
287
|
+
if (!res.length) console.log('(无命中)');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (scope === 'wiki' || scope === 'all') {
|
|
291
|
+
const { results: res, skipped } = searchWiki(words);
|
|
292
|
+
anyHits += res.length;
|
|
293
|
+
const shown = top(res);
|
|
294
|
+
console.log(`── wiki(${res.length} 个文件命中${res.length > shown.length ? `,展示前 ${shown.length}` : ''})──`);
|
|
295
|
+
for (const r of shown) {
|
|
296
|
+
console.log(`📄 wiki/${r.file} — ${r.total} 处命中 / ${r.kb < 10 ? r.kb.toFixed(1) : Math.round(r.kb)}KB`);
|
|
297
|
+
for (const h of r.shown) console.log(` L${h.ln}: ${h.line.trim().slice(0, 130)}`);
|
|
298
|
+
}
|
|
299
|
+
if (!res.length) console.log('(无命中)');
|
|
300
|
+
if (skipped) console.log(`(wiki 跳过 ${skipped} 个产物/大文件——--include-generated 纳入)`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log(`\n${anyHits ? `合计 ${anyHits} 个文件命中(${words.join(' AND ')})` : `无命中(${words.join(' AND ')})`}` +
|
|
304
|
+
`;排序:命中数/√文件KB${scope === 'workflow' ? '' : '(workflow 侧活跃 ×1.3)'}` +
|
|
305
|
+
(scope === 'all' ? ';`--scope` 限定语料、`--status all` 显式全量' : ''));
|
|
306
|
+
|
|
307
|
+
saveCache();
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// kb-search.mjs 的 fixture 驱动测试
|
|
3
|
+
// 现场构造临时 workflow 语料(换 cwd 运行被测脚本,脚本按相对路径 'workflow/' 读盘)。
|
|
4
|
+
// 断言:①AND 语义与「节限定」(指定节外的内容不参与命中);②活跃优先排序(同命中近同体积时 active 在前);
|
|
5
|
+
// ③过滤与上限(--type / --module / --status / -n);④失败路径(非法模块、无词 → exit 1);
|
|
6
|
+
// ⑤结构缓存三路逐字节等值(cold/warm/--no-cache);⑥增量失效(改 1 文件仅该文件重解析);
|
|
7
|
+
// ⑦口径指纹变化整份作废;⑧缓存损坏静默回退全扫。
|
|
8
|
+
// 用法:node .agents/scripts/kb-search.test.mjs(在仓库任意目录执行均可)
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const KB = path.join(SCRIPT_DIR, 'kb-search.mjs');
|
|
17
|
+
const CACHE_FILE = path.join(SCRIPT_DIR, '..', 'cache', 'kb-index.json');
|
|
18
|
+
const USAGE = '用法:node .agents/scripts/kb-search.test.mjs';
|
|
19
|
+
|
|
20
|
+
let pass = 0;
|
|
21
|
+
let fail = 0;
|
|
22
|
+
const check = (desc, cond, detail = '') => {
|
|
23
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
24
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const doc = (fm, title, sections) =>
|
|
28
|
+
`---\n${fm}\n---\n# ${title}\n\n${sections.map(([h, body]) => `## ${h}\n${body}\n`).join('\n')}\n`;
|
|
29
|
+
|
|
30
|
+
// mkfix:甲(closed,incidents)/ 乙(approved,intents)同命中近同体积(验活跃优先);
|
|
31
|
+
// 丙(approved,plans)只含单词(验 AND)、其「非目标」节含标记词(验节限定)
|
|
32
|
+
const mkfix = () => {
|
|
33
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-search-test-'));
|
|
34
|
+
for (const d of ['intents', 'plans', 'incidents']) fs.mkdirSync(path.join(root, 'workflow', d), { recursive: true });
|
|
35
|
+
const w = path.join(root, 'workflow');
|
|
36
|
+
const hit = (tag) => [
|
|
37
|
+
['目标', `- 统一口径:编码模板与分类层级,冲突处理见 A/B${tag}`],
|
|
38
|
+
['非目标', '- MARKER_ONLY_OUTSIDE_SECTIONS(本节不参与检索,词:编码模板)'],
|
|
39
|
+
];
|
|
40
|
+
fs.writeFileSync(path.join(w, 'incidents', '2026-09-01-aaa.md'), doc('状态: closed\n级别: L2\n发现: 2026-09-01\n模块: material', 'INCIDENT — 甲事故', hit('甲')), 'utf8');
|
|
41
|
+
fs.writeFileSync(path.join(w, 'intents', '2026-09-21-bbb.md'), doc('状态: approved\n级别: L1\n日期: 2026-09-21\n模块: pipeline', 'INTENT — 乙需求', hit('乙')), 'utf8');
|
|
42
|
+
fs.writeFileSync(path.join(w, 'plans', '2026-09-20-ccc.md'), doc('状态: approved\n级别: L1\n模块: purchase', 'PLAN — 丙计划', [['目标', '- 只讲编码模板,不提另一个词'], ['非目标', '- MARKER_ONLY_OUTSIDE_SECTIONS(本节不参与检索)']]), 'utf8');
|
|
43
|
+
// wiki 侧最小语料(验结构缓存对 wiki 行数组的存取路径)
|
|
44
|
+
fs.mkdirSync(path.join(root, 'wiki', '测试主题'), { recursive: true });
|
|
45
|
+
fs.writeFileSync(path.join(root, 'wiki', '测试主题', '说明.md'), '# 说明\n\n编码模板的领域知识条目。\n', 'utf8');
|
|
46
|
+
return root;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const run = (root, args) => spawnSync(process.execPath, [KB, ...args], { cwd: root, encoding: 'utf8' });
|
|
50
|
+
const files = (out) => (out.match(/^📄 (workflow\/\S+)/gm) || []).map((l) => l.replace('📄 ', ''));
|
|
51
|
+
|
|
52
|
+
// ---- 场景 1:主路径——AND 语义 / 节限定 / 输出字段 / 活跃优先 ----
|
|
53
|
+
{
|
|
54
|
+
const root = mkfix();
|
|
55
|
+
const r = run(root, ['--scope', 'workflow', '编码模板', '冲突']);
|
|
56
|
+
check('场景 1:检索 exit 0', r.status === 0, `exit=${r.status}\n${r.stderr}`);
|
|
57
|
+
check('场景 1:AND——两词齐全才命中(甲/乙,丙被排除)', files(r.stdout).length === 2 && !r.stdout.includes('2026-09-20-ccc.md'), r.stdout);
|
|
58
|
+
check('场景 1:活跃优先——approved 的乙排在 closed 的甲之前', files(r.stdout)[0]?.includes('2026-09-21-bbb.md'), files(r.stdout).join('\n'));
|
|
59
|
+
check('场景 1:输出行含 状态·级别·模块', /📄 workflow\/intents\/2026-09-21-bbb\.md — approved · L1 · pipeline ·/.test(r.stdout), r.stdout);
|
|
60
|
+
check('场景 1:命中带节名与行号', /\[目标\] L\d+: /.test(r.stdout), r.stdout);
|
|
61
|
+
const outside = run(root, ['--scope', 'workflow', 'MARKER_ONLY_OUTSIDE_SECTIONS']);
|
|
62
|
+
check('场景 1:节外内容不参与检索(仅标记词 → 无命中)', files(outside.stdout).length === 0 && /(无命中)/.test(outside.stdout), outside.stdout);
|
|
63
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- 场景 2:过滤与上限(--type / --module / --status / -n)----
|
|
67
|
+
{
|
|
68
|
+
const root = mkfix();
|
|
69
|
+
const t = run(root, ['--scope', 'workflow', '--type', 'plans', '编码模板']);
|
|
70
|
+
check('场景 2:--type plans 仅出 plans', files(t.stdout).length === 1 && files(t.stdout)[0].includes('plans/'), t.stdout);
|
|
71
|
+
const m = run(root, ['--scope', 'workflow', '--module', 'pipeline', '编码模板']);
|
|
72
|
+
check('场景 2:--module pipeline 仅出该模块', files(m.stdout).length === 1 && files(m.stdout)[0].includes('2026-09-21-bbb.md'), m.stdout);
|
|
73
|
+
const a = run(root, ['--scope', 'workflow', '--status', 'active', '编码模板']);
|
|
74
|
+
check('场景 2:--status active 排除终态', !a.stdout.includes('2026-09-01-aaa.md') && a.stdout.includes('2026-09-21-bbb.md'), a.stdout);
|
|
75
|
+
const term = run(root, ['--scope', 'workflow', '--status', 'terminal', '编码模板']);
|
|
76
|
+
check('场景 2:--status terminal 仅终态', term.stdout.includes('2026-09-01-aaa.md') && !term.stdout.includes('2026-09-21-bbb.md'), term.stdout);
|
|
77
|
+
const n1 = run(root, ['--scope', 'workflow', '-n', '1', '编码模板']);
|
|
78
|
+
check('场景 2:-n 1 只展示 1 个文件', files(n1.stdout).length === 1, n1.stdout);
|
|
79
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- 场景 3:失败路径(非法模块 / 无词 / 无命中)----
|
|
83
|
+
{
|
|
84
|
+
const root = mkfix();
|
|
85
|
+
const bad = run(root, ['--module', '不存在的模块', '编码模板']);
|
|
86
|
+
check('场景 3:非法 --module exit 1 且提示词表', bad.status === 1 && /未知模块/.test(bad.stderr), `exit=${bad.status}\n${bad.stderr}`);
|
|
87
|
+
const empty = run(root, []);
|
|
88
|
+
check('场景 3:无词 exit 1', empty.status === 1, `exit=${empty.status}`);
|
|
89
|
+
const none = run(root, ['--scope', 'workflow', 'zzz-绝不存在-zzz']);
|
|
90
|
+
check('场景 3:无命中 exit 0 且提示', none.status === 0 && /(无命中)/.test(none.stdout), none.stdout);
|
|
91
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- 场景 4:结构缓存三路逐字节等值(cold 首建 / warm 命中 / --no-cache),workflow+wiki 双侧 ----
|
|
95
|
+
{
|
|
96
|
+
const root = mkfix();
|
|
97
|
+
const cold = run(root, ['编码模板']); // 该临时根 ns 无条目 → cold
|
|
98
|
+
const warm = run(root, ['编码模板']); // 全命中缓存
|
|
99
|
+
const nocache = run(root, ['--no-cache', '编码模板']);
|
|
100
|
+
check('场景 4:cold/warm exit 0 且 warm 确有缓存回写', cold.status === 0 && warm.status === 0 && fs.existsSync(CACHE_FILE), `cold=${cold.status} warm=${warm.status}`);
|
|
101
|
+
check('场景 4:cold ≡ warm(逐字节)', cold.stdout === warm.stdout, `cold:\n${cold.stdout}\nwarm:\n${warm.stdout}`);
|
|
102
|
+
check('场景 4:cold ≡ --no-cache(逐字节)', cold.stdout === nocache.stdout, `cold:\n${cold.stdout}\nnocache:\n${nocache.stdout}`);
|
|
103
|
+
check('场景 4:双侧均有命中(workflow 3 + wiki 1)', files(cold.stdout).length === 3 && /wiki\/测试主题/.test(cold.stdout), cold.stdout);
|
|
104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- 场景 5:增量失效——warm 后改 1 个源文件,仅该文件重解析且输出与纯扫一致 ----
|
|
108
|
+
{
|
|
109
|
+
const root = mkfix();
|
|
110
|
+
const base = run(root, ['编码模板']);
|
|
111
|
+
const planP = path.join(root, 'workflow', 'plans', '2026-09-20-ccc.md');
|
|
112
|
+
fs.appendFileSync(planP, '\n## 影响面\n- 新标记NEWWORD:编码模板修订\n', 'utf8');
|
|
113
|
+
const after = run(root, ['编码模板', 'NEWWORD']);
|
|
114
|
+
const scan = run(root, ['--no-cache', '编码模板', 'NEWWORD']);
|
|
115
|
+
check('场景 5:改动文件被重解析(NEWWORD 命中该篇)', after.status === 0 && after.stdout.includes('2026-09-20-ccc.md'), after.stdout);
|
|
116
|
+
check('场景 5:增量结果与纯扫逐字节一致', after.stdout === scan.stdout, `after:\n${after.stdout}\nscan:\n${scan.stdout}`);
|
|
117
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- 场景 6:口径指纹变化 → 整份作废重建,无旧口径残留 ----
|
|
121
|
+
{
|
|
122
|
+
const root = mkfix();
|
|
123
|
+
run(root, ['编码模板']); // 建缓存
|
|
124
|
+
const j = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
125
|
+
const fpBefore = j.fp;
|
|
126
|
+
j.fp = 'ffffffffffffffff';
|
|
127
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(j), 'utf8');
|
|
128
|
+
const after = run(root, ['编码模板']);
|
|
129
|
+
const scan = run(root, ['--no-cache', '编码模板']);
|
|
130
|
+
check('场景 6:指纹不符 → 输出仍与纯扫逐字节一致', after.status === 0 && after.stdout === scan.stdout, `after:\n${after.stdout}\nscan:\n${scan.stdout}`);
|
|
131
|
+
const j2 = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
132
|
+
check('场景 6:回写恢复正确指纹', j2.fp === fpBefore && j2.fp !== 'ffffffffffffffff', `fp=${j2.fp}`);
|
|
133
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- 场景 7:缓存损坏 → 静默回退全扫(不报错、输出与 --no-cache 一致) ----
|
|
137
|
+
{
|
|
138
|
+
const root = mkfix();
|
|
139
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
140
|
+
fs.writeFileSync(CACHE_FILE, 'not-json{{{', 'utf8');
|
|
141
|
+
const r = run(root, ['编码模板']);
|
|
142
|
+
const scan = run(root, ['--no-cache', '编码模板']);
|
|
143
|
+
check('场景 7:损坏缓存 exit 0(stderr 无泄漏)', r.status === 0 && !r.stderr, `exit=${r.status}\n${r.stderr}`);
|
|
144
|
+
check('场景 7:损坏后输出与纯扫逐字节一致', r.stdout === scan.stdout, `r:\n${r.stdout}\nscan:\n${scan.stdout}`);
|
|
145
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
149
|
+
if (fail) {
|
|
150
|
+
console.log(`\n${USAGE}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
process.exit(0);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# 常驻面体积预算检查(单源:本脚本 + .agents/rule-budgets.txt;两个触发点共用)
|
|
3
|
+
# 用法:sh .agents/scripts/rule-budget.sh [--all|--staged]
|
|
4
|
+
# --all 查「提交后树」全量(check-loop.sh 检查 13 调用,warning 侧)
|
|
5
|
+
# --staged 仅查本次暂存触及的条目 + 触及目录的合计(.githooks/pre-commit 调用,超限即硬拦)
|
|
6
|
+
# 退出码:0 通过;1 有超限(违例逐条打到 stdout,调用方决定警告还是阻断)
|
|
7
|
+
# 「提交后树」= 索引(index)内容:staged 版优先于 HEAD;无 git(fixture 沙箱)时回退工作区文件。
|
|
8
|
+
# 预算表按字节读,显式去 \r——本仓库 core.autocrlf=true,表文件检出即 CRLF,行尾残留会让数值比较失配。
|
|
9
|
+
# 字节口径 = 索引 / HEAD 内容(git cat-file,LF 归一)。**禁用 wc -c 核对**——工作区文件是 CRLF,量出来系统性偏大
|
|
10
|
+
# (2026-09-21 实测 .agents/commands/ 工作区 39963B vs 索引 39451B,差 ~500B,一度误判「已超限」)。
|
|
11
|
+
cd "$(git rev-parse --show-toplevel 2>/dev/null)" || true # 非 git 环境(fixture)留在原地
|
|
12
|
+
MODE=${1:---all}
|
|
13
|
+
BUDGETS=.agents/rule-budgets.txt
|
|
14
|
+
[ -f "$BUDGETS" ] || exit 0
|
|
15
|
+
|
|
16
|
+
# 取「提交后」字节数:索引 → HEAD → 工作区
|
|
17
|
+
fsize() { # <path>
|
|
18
|
+
if git cat-file -e ":$1" 2>/dev/null; then git cat-file -s ":$1"
|
|
19
|
+
elif git cat-file -e "HEAD:$1" 2>/dev/null; then git cat-file -s "HEAD:$1"
|
|
20
|
+
else wc -c < "$1" 2>/dev/null | tr -d '[:space:]'
|
|
21
|
+
fi
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
staged_list=""
|
|
25
|
+
if [ "$MODE" = "--staged" ]; then
|
|
26
|
+
staged_list=$(git -c core.quotepath=off diff --cached --name-only 2>/dev/null)
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
viol=0
|
|
30
|
+
while IFS= read -r line; do
|
|
31
|
+
line=$(printf '%s' "$line" | tr -d '\r')
|
|
32
|
+
pat=${line%% *}
|
|
33
|
+
lim=${line##* }
|
|
34
|
+
case "$pat" in ''|'#'*) continue ;; esac
|
|
35
|
+
case "$lim" in ''|*[!0-9]*) continue ;; esac
|
|
36
|
+
case "$pat" in
|
|
37
|
+
*/) # 目录合计:--staged 下仅当暂存触及该目录内文件才查
|
|
38
|
+
if [ "$MODE" = "--staged" ]; then
|
|
39
|
+
printf '%s\n' "$staged_list" | grep -q "^${pat}" || continue
|
|
40
|
+
fi
|
|
41
|
+
total=0
|
|
42
|
+
for f in "$pat"*; do
|
|
43
|
+
[ -f "$f" ] || continue
|
|
44
|
+
total=$((total + $(fsize "$f")))
|
|
45
|
+
done
|
|
46
|
+
if [ "$total" -gt "$lim" ]; then
|
|
47
|
+
echo "常驻面超限:${pat} 合计 ${total}B > 上限 ${lim}B"
|
|
48
|
+
viol=1
|
|
49
|
+
fi
|
|
50
|
+
;;
|
|
51
|
+
*) # 逐文件(glob 刻意不加引号以展开;无命中时 [ -f ] 兜住)
|
|
52
|
+
for f in $pat; do
|
|
53
|
+
[ -f "$f" ] || continue
|
|
54
|
+
if [ "$MODE" = "--staged" ]; then
|
|
55
|
+
printf '%s\n' "$staged_list" | grep -qxF "$f" || continue
|
|
56
|
+
fi
|
|
57
|
+
b=$(fsize "$f")
|
|
58
|
+
if [ "$b" -gt "$lim" ]; then
|
|
59
|
+
echo "常驻面超限:$f = ${b}B > 上限 ${lim}B"
|
|
60
|
+
viol=1
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
done < "$BUDGETS"
|
|
66
|
+
|
|
67
|
+
[ "$viol" -eq 0 ] || exit 1
|
|
68
|
+
exit 0
|