reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
// lib/capability-index.mjs — 能力层闭环:索引生成与完整性检查
//
// 能力层设计(re-analyze/references/capabilities.md):技能声明 capabilities → 路由按能力匹配。
// 本模块是「声明」与「消费」之间的那一环,产出能力 → 技能索引(路由/检索的查询表),
// 并提供两条完整性检查,防止能力层停留在"只写不读"的状态:
// 1) 注册表标签悬空——注册表里有、没有任何技能声明
// 2) 索引过期——声明变更后未重新生成(`node bin/capindex.mjs`)
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { parseFrontmatterDocument } from './frontmatter.mjs';
export const INDEX_REL = 're-analyze/references/capability-index.md';
const REGISTRY_REL = 're-analyze/references/capabilities.md';
const SKILL_PREFIX = 're-';
// 机器生成文件头——含生成命令与用途,避免被当作手写文档维护
const HEADER = `# 能力索引(机器生成,勿手改)
> 生成:\`node bin/capindex.mjs\`|校验:\`npm test\`(过期即失败)
> 用途:路由与检索按能力反查技能——能力层的查询表(声明侧见 capabilities.md)
`;
// 注册表标签 → 一行说明
export function collectRegistryTags(root) {
const p = join(root, REGISTRY_REL);
if (!existsSync(p)) return new Map();
const map = new Map();
for (const line of readFileSync(p, 'utf8').split('\n')) {
const m = line.match(/^\-\s+`([a-z0-9-]+)`\s+—\s+(.*)$/);
if (m) map.set(m[1], m[2].trim());
}
return map;
}
// 各技能声明的能力:tag → [技能名](非法写法在此跳过,由 validate 逐技能报错)
export function collectDeclaredCapabilities(root) {
const byTag = new Map();
for (const dir of readdirSync(root, { withFileTypes: true })) {
if (!dir.isDirectory() || !dir.name.startsWith(SKILL_PREFIX)) continue;
const p = join(root, dir.name, 'SKILL.md');
if (!existsSync(p)) continue;
let value;
try {
value = parseFrontmatterDocument(readFileSync(p, 'utf8')).fm.capabilities;
} catch {
continue;
}
if (value === undefined) continue;
const m = String(value).match(/^\[\s*([a-z0-9-]+(?:\s*,\s*[a-z0-9-]+)*)\s*\]$/);
if (!m) continue;
for (const tag of m[1].split(',').map(s => s.trim())) {
byTag.set(tag, [...(byTag.get(tag) ?? []), dir.name]);
}
}
return byTag;
}
// 渲染索引全文(生成与校验共用同一函数,保证比较的是同一事实)
export function renderCapabilityIndex(root) {
const declared = collectDeclaredCapabilities(root);
const registry = collectRegistryTags(root);
const tags = [...declared.keys()].sort();
const missing = [...registry.keys()].filter(t => !declared.has(t)).sort();
// 覆盖率看板:标签覆盖(注册表自洽)与技能覆盖(渐进标注进度)分开计
const declaredSkills = new Set([...declared.values()].flat());
const totalSkills = readdirSync(root, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name.startsWith(SKILL_PREFIX) && existsSync(join(root, d.name, 'SKILL.md'))).length;
const out = [HEADER];
out.push(`## 已声明能力(标签 ${tags.length}/${registry.size}|技能 ${declaredSkills.size}/${totalSkills})\n`);
out.push('| 能力 | 说明 | 提供技能 |');
out.push('|---|---|---|');
for (const t of tags) {
out.push(`| \`${t}\` | ${registry.get(t) ?? '(不在注册表)'} | ${[...declared.get(t)].sort().join(', ')} |`);
}
out.push(`\n## 尚未被声明(${missing.length})\n`);
out.push(missing.length
? missing.map(t => `- ${t} — ${registry.get(t)}`).join('\n')
: '(无——注册表标签全部有技能声明)');
out.push('');
return out.join('\n');
}
// 索引缺失或与当前声明不一致 → 报错(提示重新生成)
export function checkCapabilityIndex(root) {
const p = join(root, INDEX_REL);
if (!existsSync(p)) return [`capability index missing: ${INDEX_REL}(运行 node bin/capindex.mjs)`];
if (readFileSync(p, 'utf8') !== renderCapabilityIndex(root)) {
return [`capability index out of date: ${INDEX_REL}(运行 node bin/capindex.mjs)`];
}
return [];
}

View File

@@ -0,0 +1,68 @@
// lib/frontmatter.mjs — 技能 frontmatter 解析的唯一实现
//
// validate.mjs(结构校验)与 bin/convert.mjs(规则转换)共用本模块。
// 此前两边各写一套行式解析:convert 支持块标量、validate 不支持,
// 导致 104/121 个技能的 description 在 validate 侧被解析成字面量 ">",
//「description 非空」校验形同虚设(2026-09-13 审查记录,tests/frontmatter.test.mjs 回归)。
//
// 支持的 YAML 子集(技能库实际用到的部分):
// - 简单 scalar:key: value
// - 块标量:key: >(折叠为空格)/ key: |(保留换行),吸收后续缩进行
// - 内联 flow list / 内联 JSON:key: [a, b] / key: {...}(按字符串返回,由调用方解析)
// 不支持锚点、嵌套 map、多行 flow 等其余 YAML 特性——新增 frontmatter 写法前先扩展本模块并补测试,
// 否则校验与转换会再次出现解析分歧。
// 顶格且非 `key:` 的行一律报错、不静默丢弃:块标量正文里误写顶层列表项(`- xxx`)会让
// description 被静默截断,且标准 YAML 解析器会判定整块 frontmatter 非法、丢弃整个技能
// (tests/frontmatter.test.mjs 与 tests/fixtures/bad-frontmatter 回归)。
// 换行归一:Windows 上 core.autocrlf=true 的检出会把提交时的 LF 变成 CRLF,
// 而本模块的分界与行正则都按 LF 写。DSH 侧解析器显式容错 CR(`replace(/\r$/, '')`),
// 本模块同样先归一,避免同一份技能在 LF / CRLF 检出一个通过、一个报 missing frontmatter。
// 归一只影响换行,不改动内容——CRLF 与 LF 检出的校验结果因此一致。
function toLf(text) {
return text.replace(/\r\n?/g, '\n');
}
// 拆文档:frontmatter 原文 + 正文(正文前多余空行一并去掉)
export function splitFrontmatter(md) {
const m = toLf(md).match(/^---\n([\s\S]*?)\n---\n*([\s\S]*)$/);
if (!m) throw new Error('missing frontmatter');
return { raw: m[1], body: m[2] };
}
// 行式解析 frontmatter 原文 → 键值对象(值均为字符串;块标量已折叠/展开)
export function parseFrontmatterFields(raw) {
const fm = {};
const lines = toLf(raw).split('\n');
for (let i = 0; i < lines.length; i++) {
const kv = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (!kv) {
// 顶格行既不是 `key:` 也不是块标量的缩进续行——解析器看不见它,内容会被静默丢弃。
// 实际踩过的坑:块标量正文里误写顶层列表项(`- xxx`),description 在此截断,
// 而严格 YAML 解析器(外部消费方如 DSH)会因整块 frontmatter 报错而丢掉整个技能。
if (lines[i].trim() !== '' && !/^\s/.test(lines[i])) {
throw new Error(`unparsable frontmatter line ${i + 1} (not a 'key:' line nor an indented continuation): ${lines[i]}`);
}
continue;
}
const key = kv[1];
const value = kv[2];
if (/^[>|]/.test(value)) {
// YAML 块标量:吸收后续缩进行(`>` 折叠为空格,`|` 保留换行)
const folded = [];
while (lines[i + 1] !== undefined && /^\s+/.test(lines[i + 1])) {
folded.push(lines[++i].replace(/^\s+/, ''));
}
fm[key] = folded.join(value[0] === '>' ? ' ' : '\n').trim();
} else {
fm[key] = value;
}
}
return fm;
}
// 便捷入口:一次拿到 { fm, body }
export function parseFrontmatterDocument(md) {
const { raw, body } = splitFrontmatter(md);
return { fm: parseFrontmatterFields(raw), body };
}

View File

@@ -0,0 +1,60 @@
// lib/probe-tools.mjs — probe.sh 的工具清单:由登记表生成(单一事实源)
//
// 背景:probe.sh 曾硬编码 19 个工具,而登记表已有 140 项——环境里装了什么
// 是 agent 判断"优先用什么"的依据,硬编码清单会随登记表增长而失同步。
// 做法与能力索引同构:登记表是源,probe.sh 里的清单是生成物,`--check` 校验新鲜度。
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
export const REGISTER_REL = 'docs/audit/tool-register.json';
export const PROBE_REL = '.claude/skills/re-analyze/references/probe.sh';
export const BEGIN = '# >>> GENERATED-TOOLS';
export const END = '# <<< GENERATED-TOOLS';
/** 从登记表取要探测的工具名(忽略名单之外的 cli 项,按名排序) */
export function probeToolNames(repoRoot) {
const reg = JSON.parse(readFileSync(join(repoRoot, REGISTER_REL), 'utf8'));
const ignore = new Set(reg.ignore ?? []);
return reg.tools
.map((t) => t.name)
.filter((n) => !ignore.has(n))
.sort((a, b) => a.localeCompare(b));
}
/** 生成标记块(含换行) */
export function renderBlock(names) {
return [
`${BEGIN}(源:${REGISTER_REL};改清单请改登记表后跑 node bin/probelist.mjs)`,
'# shellcheck disable=SC2034',
`PROBE_TOOLS="${names.join(' ')}"`,
END,
].join('\n');
}
/** 替换 probe.sh 里的标记块;返回是否发生变化 */
export function writeProbeBlock(repoRoot) {
const p = join(repoRoot, PROBE_REL);
const text = readFileSync(p, 'utf8');
const block = renderBlock(probeToolNames(repoRoot));
const re = new RegExp(`^${BEGIN}[\\s\\S]*?^${END}$`, 'm');
if (!re.test(text)) throw new Error(`${PROBE_REL} 里找不到 GENERATED-TOOLS 标记块`);
const next = text.replace(re, block);
if (next === text) return false;
writeFileSync(p, next);
return true;
}
/** 校验 probe.sh 的清单是否与登记表一致 */
export function checkProbeBlock(repoRoot) {
const p = join(repoRoot, PROBE_REL);
if (!existsSync(p)) return [`找不到 ${PROBE_REL}`];
const text = readFileSync(p, 'utf8');
const re = new RegExp(`^${BEGIN}[\\s\\S]*?^${END}$`, 'm');
const m = re.exec(text);
if (!m) return [`${PROBE_REL} 里找不到 GENERATED-TOOLS 标记块`];
const expected = renderBlock(probeToolNames(repoRoot));
if (m[0] !== expected) {
return [`${PROBE_REL} 的工具清单与登记表不一致——跑 node bin/probelist.mjs 重新生成`];
}
return [];
}

View File

@@ -0,0 +1,100 @@
// lib/review-state.mjs — 审查状态:每技能一份内容 hash + 复核记录
//
// 解决的问题:审查波是全量式——每波把整批技能从头审一遍,已确认的部分被反复重审。
// 本模块把审查成本从 O(技能总数) 降到 O(变更数):先看哪些技能的内容在复核之后又变了。
//
// 状态语义:
// last_reviewed = null 存量技能,尚未按本机制复核
// 当前 hash ≠ 记录 hash 内容变了 → 需要复核(下一轮审查波的工作集)
// 当前 hash = 记录 hash 已复核且未再变
import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { createHash } from 'node:crypto';
export const STATE_REL = 'docs/audit/review-state.json';
/** 技能内容 hash:目录内全部文件按相对路径排序后逐个喂入 */
export function hashSkill(dir) {
const h = createHash('sha256');
const files = [];
const walk = (d) => {
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const p = join(d, e.name);
if (e.isDirectory()) walk(p);
else if (e.name.endsWith('.md') || e.name.endsWith('.sh')) files.push(p);
}
};
walk(dir);
for (const f of files) {
h.update(relative(dir, f).split('\\').join('/'));
h.update('\0');
h.update(readFileSync(f));
h.update('\0');
}
return h.digest('hex').slice(0, 16);
}
export function skillDirs(skillsDir) {
return readdirSync(skillsDir, { withFileTypes: true })
.filter((e) => e.isDirectory() && e.name.startsWith('re-'))
.map((e) => e.name)
.sort();
}
export function loadState(repoRoot) {
const p = join(repoRoot, STATE_REL);
if (!existsSync(p)) return null;
return JSON.parse(readFileSync(p, 'utf8'));
}
export function saveState(repoRoot, state) {
writeFileSync(join(repoRoot, STATE_REL), JSON.stringify(state, null, 2) + '\n');
}
/**
* 计算状态:
* changed 内容在复核之后又变了 → 下一轮审查波的工作集
* unreviewed 从未按本机制复核
* ok 已复核且未变
*/
export function computeStatus(repoRoot, skillsDir) {
const state = loadState(repoRoot) ?? { skills: {} };
const recorded = state.skills ?? {};
const changed = [], unreviewed = [], ok = [], unknown = [];
for (const name of skillDirs(skillsDir)) {
const hash = hashSkill(join(skillsDir, name));
const rec = recorded[name];
// 无记录、或记录里 last_reviewed 为空 → 从未按本机制复核(此时 hash 是否变化无意义)
if (!rec || !rec.last_reviewed) unreviewed.push({ name, hash });
else if (rec.hash !== hash) changed.push({ name, hash, was: rec.hash, last_reviewed: rec.last_reviewed });
else ok.push({ name, hash, last_reviewed: rec.last_reviewed });
}
for (const name of Object.keys(recorded)) {
if (!skillDirs(skillsDir).includes(name)) unknown.push(name); // 已删除的技能仍留在状态里
}
return { changed, unreviewed, ok, unknown };
}
/** 回填:记录"这些技能已复核"(写入当前 hash 与日期) */
export function markReviewed(repoRoot, skillsDir, names, { date } = {}) {
const status = computeStatus(repoRoot, skillsDir);
const state = loadState(repoRoot) ?? {
'$comment': '审查状态:每技能内容 hash + 最近复核。changed = 内容在复核后又变了,即下一轮审查波的工作集。',
skills: {},
};
state.skills ??= {};
const known = new Set(skillDirs(skillsDir));
for (const n of names) {
if (!known.has(n)) throw new Error(`未知技能:${n}`);
const entry = [...status.changed, ...status.ok, ...status.unreviewed].find((s) => s.name === n);
state.skills[n] = { hash: entry.hash, last_reviewed: date, note: state.skills[n]?.note ?? '' };
}
// 清理已删除技能
for (const n of Object.keys(state.skills)) if (!known.has(n)) delete state.skills[n];
saveState(repoRoot, state);
return names.length;
}
export function todayISO() {
return new Date().toISOString().slice(0, 10);
}

View File

@@ -0,0 +1,165 @@
// lib/skill-examples.mjs — 技能里的示例代码块:抽取与语法检查
//
// 定位:validate.mjs 只校验结构,不执行示例。本模块补上"示例至少语法正确"这一层——
// 它抓不到"命令过期",但能抓到"照抄会立刻报错"(如把 0x4011xx 写进 python)。
//
// 依赖说明:python / shell 语法检查需要对应解释器;缺失时**跳过而非失败**,
// 因此本检查是 opt-in(npm run examples),不进 npm test 的默认路径。
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join, relative } from 'node:path';
import { execFileSync } from 'node:child_process';
const FENCE = /^```([A-Za-z0-9_+.-]*)\s*$/;
const PY_TAGS = new Set(['python', 'py', 'python3']);
const SH_TAGS = new Set(['sh', 'bash', 'console', 'shell', 'zsh']);
/** 抽取全部技能里的代码块 */
export function collectExamples(skillsDir) {
const out = [];
for (const skill of readdirSync(skillsDir, { withFileTypes: true })) {
if (!skill.isDirectory() || !skill.name.startsWith('re-')) continue;
walk(join(skillsDir, skill.name), skill.name, skillsDir, out);
}
return out;
}
function walk(dir, skill, skillsDir, out) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) { walk(p, skill, skillsDir, out); continue; }
if (!e.name.endsWith('.md')) continue;
const lines = readFileSync(p, 'utf8').split('\n');
let inBlock = false, tag = '', start = 0, buf = [];
for (let i = 0; i < lines.length; i++) {
const m = FENCE.exec(lines[i].trim());
if (m) {
if (!inBlock) { inBlock = true; tag = m[1].toLowerCase(); start = i + 2; buf = []; }
else {
out.push({ skill, file: relative(skillsDir, p), line: start, tag, body: buf.join('\n') });
inBlock = false;
}
continue;
}
if (inBlock) buf.push(lines[i]);
}
}
}
/** 去公共缩进(技能里的块常缩进在列表项下) */
export function dedent(text) {
const lines = text.split('\n');
const indents = lines.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)[0].length);
const min = indents.length ? Math.min(...indents) : 0;
return lines.map((l) => l.slice(min)).join('\n');
}
// 调试器/解释器交互提示符
const DBG_PROMPT = /^\s*\((gdb|rr|lldb|jdb|pdb|gef|pwndbg|python3?)\)/;
// 交互式工具的启动行——配合 `> ` 行可判定整块是会话记录
const INTERACTIVE = /^(jdb|gdb|lldb|rr|jshell|frida|node|python3?)\b/;
/** 拆出 heredoc:返回 { shell, heredocs: [{lang, body}] },heredoc 体不属于 shell 语法 */
export function splitHeredocs(text) {
const lines = dedent(text).split('\n');
const shell = [];
const heredocs = [];
let pending = null; // { delim, lang, buf }
for (const raw of lines) {
const line = raw.trim();
if (pending) {
if (line === pending.delim) { heredocs.push({ lang: pending.lang, body: pending.buf.join('\n') }); pending = null; }
else pending.buf.push(raw);
continue;
}
const hd = /<<-?\s*'?"?([A-Za-z_][A-Za-z0-9_]*)'?"?/.exec(line);
if (hd) {
const delim = hd[1];
// 语言线索:喂给解释器的 heredoc 才按对应语言检查
const lang = /\bpython3?\b/.test(line) ? 'python'
: /\bnode\b/.test(line) ? 'js'
: null;
pending = { delim, lang, buf: [] };
shell.push(`: <<'${delim}'`); // 用空操作占位,保持行数对齐
continue;
}
shell.push(raw);
}
if (pending) heredocs.push({ lang: pending.lang, body: pending.buf.join('\n') });
return { shell: shell.join('\n'), heredocs };
}
/**
* shell 规范化:去缩进 → 拆 heredoc → 会话记录处理 → 占位符中和
* - **会话记录块**(有调试器提示符,或有交互式工具启动行 + `> ` 命令):丢弃提示符行
* - 占位符中和:`<pid>`、`<BundleID 或 App 名>` 会被 shell 当成重定向
*/
export function normalizeShell(body) {
const { shell } = splitHeredocs(body);
const raw = shell.split('\n').map((l) => l.trim());
const firstCmd = raw.find((l) => l && !l.startsWith('#'));
const isTranscript = raw.some((l) => DBG_PROMPT.test(l))
|| (raw.some((l) => /^>\s/.test(l)) && INTERACTIVE.test(firstCmd ?? ''));
return raw
.filter((l) => (isTranscript ? !DBG_PROMPT.test(l) && !/^>\s/.test(l) : true))
.map((l) => l.replace(DBG_PROMPT, '').replace(/^[$>]\s+/, ''))
.join('\n')
.replace(/<[^<>]*>/g, 'PLACEHOLDER');
}
function has(bin) {
try { execFileSync('sh', ['-c', `command -v ${bin}`], { stdio: 'ignore' }); return true; }
catch { return false; }
}
export function available() {
return { python: has('python3'), shell: has('bash') };
}
/** 逐块语法检查;返回 {checked, errors:[{file,line,tag,msg}], skipped} */
export function checkExamples(skillsDir, { interpreters = available() } = {}) {
const blocks = collectExamples(skillsDir);
const errors = [];
let checked = 0;
const skipped = [];
for (const b of blocks) {
const isPy = PY_TAGS.has(b.tag);
const isSh = SH_TAGS.has(b.tag);
if (!isPy && !isSh) continue;
if (isPy) {
if (!interpreters.python) { skipped.push('python(未找到 python3)'); continue; }
checked++;
try {
execFileSync('python3', ['-c', 'import sys; compile(sys.stdin.read(), "<block>", "exec")'],
{ input: dedent(b.body), stdio: ['pipe', 'ignore', 'pipe'] });
} catch (e) {
const msg = (e.stderr?.toString() ?? String(e)).trim().split('\n').pop();
errors.push({ ...pick(b), msg });
}
} else {
if (!interpreters.shell) { skipped.push('shell(未找到 bash)'); continue; }
checked++;
try {
execFileSync('bash', ['-n'], { input: normalizeShell(b.body), stdio: ['pipe', 'ignore', 'pipe'] });
} catch (e) {
const msg = (e.stderr?.toString() ?? String(e)).trim().split('\n').pop();
errors.push({ ...pick(b), msg });
}
// sh 块里嵌的 python heredoc 单独按 python 检查(`python3 - <<'PY'` 在技能里很常见)
for (const hd of splitHeredocs(b.body).heredocs) {
if (hd.lang !== 'python') continue;
if (!interpreters.python) { skipped.push('heredoc python(未找到 python3)'); continue; }
checked++;
try {
execFileSync('python3', ['-c', 'import sys; compile(sys.stdin.read(), "<heredoc>", "exec")'],
{ input: hd.body, stdio: ['pipe', 'ignore', 'pipe'] });
} catch (e) {
const msg = (e.stderr?.toString() ?? String(e)).trim().split('\n').pop();
errors.push({ ...pick(b), msg: `heredoc: ${msg}` });
}
}
}
}
return { checked, errors, skipped: [...new Set(skipped)], total: blocks.length };
}
const pick = (b) => ({ skill: b.skill, file: b.file, line: b.line, tag: b.tag });

View File

@@ -0,0 +1,216 @@
// lib/tool-register.mjs — 第三方命令/API 清单:抽取、比对、时效
//
// 用途:领域知识比工具操作层稳定——技能里的**命令/API 会随上游版本漂移**,
// 而 validate.mjs 只校验结构,不执行示例、也不检查命令是否还存在。
// 本模块提供「登记表 + 候选发现 + 时效报告」的最小骨架(与增量审查机制共用状态模型)。
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
export const REGISTER_REL = 'docs/audit/tool-register.json';
// shell 关键字 / 控制结构 / heredoc 定界符——不是第三方工具
const SHELL_WORDS = new Set([
'for', 'if', 'while', 'until', 'do', 'done', 'then', 'else', 'elif', 'fi', 'case', 'esac',
'function', 'export', 'set', 'unset', 'source', 'local', 'return', 'exit', 'break', 'continue',
'cd', 'echo', 'printf', 'true', 'false', 'test', 'read', 'eval', 'exec', 'shift', 'trap', 'wait',
'PY', 'EOF', 'SH', 'BASH', 'END', 'JSON', 'PYTHON',
]);
// 常见 shell 内建/基础工具:登记表不收录(噪音大、无生命周期风险)
export const BASE_UTILS = new Set([
'ls', 'cat', 'cp', 'mv', 'rm', 'mkdir', 'rmdir', 'touch', 'ln', 'chmod', 'chown', 'pwd', 'tee',
'head', 'tail', 'wc', 'sort', 'uniq', 'cut', 'tr', 'sed', 'awk', 'grep', 'egrep', 'find', 'xargs',
'which', 'whereis', 'env', 'date', 'sleep', 'kill', 'ps', 'top', 'df', 'du', 'mount', 'umount',
'sha256sum', 'md5sum', 'sha1sum', 'base64', 'xxd', 'od', 'stat', 'file', 'strings', 'diff', 'patch',
'dd', 'hexdump', 'gzip', 'bzip2', 'xz', 'zstd', 'jq', // jq 属基础数据处理,不作为分析工具登记
'tar', 'gzip', 'gunzip', 'zcat', 'unzip', 'zip', 'curl', 'wget', 'ssh', 'scp', 'rsync', 'nc', 'netcat',
'sudo', 'su', 'git', 'make', 'cmake', 'gcc', 'clang', 'python', 'python3', 'pip', 'pip3', 'node', 'npm',
'apt', 'apt-get', 'dnf', 'yum', 'pacman', 'brew', 'choco', 'pipx', 'podman',
]);
// 只抽显式标注为 shell 的代码块——未标注块里混着 python / C / 伪代码,
// 会把 print、import、struct 字段名等当成"命令",噪音量级大一个数量级。
const SHELL_TAGS = new Set(['sh', 'bash', 'console', 'shell', 'zsh']);
/** 抽取代码块中出现的「命令首 token」 */
export function extractCommands(markdown) {
const out = [];
const fence = /^```([A-Za-z]*)\s*$/;
let inBlock = false;
let tag = '';
let skipUntil = null; // heredoc 定界符
for (const raw of markdown.split('\n')) {
const trimmed = raw.trim();
const m = fence.exec(trimmed);
if (m) {
if (!inBlock) { inBlock = true; tag = m[1].toLowerCase(); skipUntil = null; }
else { inBlock = false; tag = ''; skipUntil = null; }
continue;
}
if (!inBlock) continue;
if (!SHELL_TAGS.has(tag)) continue;
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
if (skipUntil) { if (trimmed === skipUntil) skipUntil = null; continue; }
const hd = /<<-?\s*'?"?([A-Za-z_][A-Za-z0-9_]*)'?"?/.exec(trimmed);
if (hd) { skipUntil = hd[1]; continue; }
// 去提示符 / 环境变量赋值前缀 / sudo,取首 token
const stripped = trimmed
.replace(/^(\$|>|#)\s*/, '')
.replace(/^(\w+=\S*\s+)+/, '')
.replace(/^(sudo|doas)\s+/, '');
const tok = /^([A-Za-z][A-Za-z0-9_.+-]*)/.exec(stripped);
if (!tok) continue;
const cmd = tok[1];
if (SHELL_WORDS.has(cmd) || BASE_UTILS.has(cmd) || cmd.length < 2) continue;
out.push(cmd);
}
return out;
}
/** 扫描技能目录,返回 cmd → 技能名集合 */
export function collectFromSkills(skillsDir) {
const map = new Map();
const walk = (dir, owner) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) walk(p, owner ?? e.name);
else if (e.name.endsWith('.md')) {
const skill = owner ?? e.name.replace(/\.md$/, '');
for (const cmd of extractCommands(readFileSync(p, 'utf8'))) {
if (!map.has(cmd)) map.set(cmd, new Set());
map.get(cmd).add(skill);
}
}
}
};
for (const e of readdirSync(skillsDir, { withFileTypes: true })) {
if (e.isDirectory() && e.name.startsWith('re-')) walk(join(skillsDir, e.name), e.name);
}
return map;
}
export function loadRegister(repoRoot) {
const p = join(repoRoot, REGISTER_REL);
if (!existsSync(p)) return null;
return JSON.parse(readFileSync(p, 'utf8'));
}
export function todayISO() {
return new Date().toISOString().slice(0, 10);
}
export function daysSince(iso) {
const t = Date.parse(`${iso}T00:00:00Z`);
if (Number.isNaN(t)) return Infinity;
return Math.floor((Date.now() - t) / 86400000);
}
/** 一致性检查:返回 {errors, warnings, stale, candidates} */
export function checkRegister(repoRoot, skillsDir, { staleDays = 180 } = {}) {
const errors = [];
const warnings = [];
const register = loadRegister(repoRoot);
if (!register) {
return { errors: [`找不到登记表 ${REGISTER_REL}`], warnings: [], stale: [], candidates: [], register: null };
}
if (!Array.isArray(register.tools)) {
return { errors: [`${REGISTER_REL} 缺少 tools 数组`], warnings: [], stale: [], candidates: [], register };
}
const seen = new Set();
for (const t of register.tools) {
if (!t.name) errors.push('登记项缺少 name');
if (seen.has(t.name)) errors.push(`登记项重复:${t.name}`);
seen.add(t.name);
// last_verified 为 null = 已登记但尚未核验当前 CLI/API 形态(允许,且会被计入待核验)
if (t.last_verified !== null && !/^\d{4}-\d{2}-\d{2}$/.test(t.last_verified ?? '')) {
errors.push(`登记项 ${t.name} 的 last_verified 须为 YYYY-MM-DD 或 null`);
}
if (t.last_verified && !t.source) warnings.push(`登记项 ${t.name} 已标核验日期但未记录来源(source)`);
}
const used = collectFromSkills(skillsDir);
// 死条目:登记了却已无技能引用
for (const t of register.tools) {
if (!used.has(t.name)) errors.push(`死条目:登记为在用,但技能里已找不到 ${t.name}`);
}
// 候选:技能里出现但未登记(且不在忽略名单)
const ignore = new Set(register.ignore ?? []);
const candidates = [...used.keys()].filter((c) => !seen.has(c) && !ignore.has(c)).sort();
// 时效:已核验的超期 → stale;从未核验的 → pending(待核验积压,与 stale 分开统计)
const stale = [];
const pending = [];
for (const t of register.tools) {
if (!t.last_verified) { pending.push(t.name); continue; }
const days = daysSince(t.last_verified);
if (days >= staleDays) stale.push({ name: t.name, last_verified: t.last_verified, days });
}
stale.sort((a, b) => b.days - a.days);
pending.sort();
return { errors, warnings, stale, pending, candidates, register, used };
}
// 风险判据:技能对某工具的**断言越具体,越容易随上游版本漂移**
// (只点名工具名 → 低风险;写死子命令/插件路径/版本 → 高风险)
const RISK_FLAG = /(^|\s)--?[A-Za-z][\w-]*/; // 命令行开关
const RISK_PATH = /\b[a-z_]+(\.[a-z_]+){2,}\b/; // 点分子命令/插件路径 windows.registry.hashdump
const RISK_VER = /\bv?\d+\.\d+(\.\d+)?\b/; // 版本号
/**
* 从技能正文推导每个工具的风险分层(不落盘,随内容实时派生)
* 返回 name → { level: 'high'|'low', score, lines: [示例行] }
*/
export function deriveRisk(skillsDir, names) {
const want = new Set(names);
const hits = new Map(); // name → [{file, line}]
for (const skill of readdirSync(skillsDir, { withFileTypes: true })) {
if (!skill.isDirectory() || !skill.name.startsWith('re-')) continue;
walkLines(join(skillsDir, skill.name), (file, line, text) => {
for (const n of want) {
if (new RegExp(`(^|[\\s\`(\\[/])${n.replace(/[.+]/g, '\\$&')}([\\s\`)\\]/:]|$)`).test(text)) {
if (!hits.has(n)) hits.set(n, []);
hits.get(n).push({ file, line, text: text.trim().slice(0, 120) });
}
}
});
}
const out = {};
for (const n of want) {
const hs = hits.get(n) ?? [];
// 断言类型(而不是出现次数)决定风险:写死了层级路径或版本号的,最容易随上游漂移
const hasFlag = hs.some((h) => RISK_FLAG.test(h.text));
const hasPath = hs.some((h) => RISK_PATH.test(h.text));
const hasVer = hs.some((h) => RISK_VER.test(h.text));
const specificity = (hasFlag ? 1 : 0) + (hasPath ? 2 : 0) + (hasVer ? 2 : 0);
out[n] = {
level: specificity >= 2 ? 'high' : 'low',
specificity,
spread: hs.length, // 出现面:错了会影响多少条示例(严重度)
sample: hs.slice(0, 3),
};
}
return out;
}
function walkLines(dir, cb) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) { walkLines(p, cb); continue; }
if (!e.name.endsWith('.md')) continue;
const lines = readFileSync(p, 'utf8').split('\n');
for (let i = 0; i < lines.length; i++) cb(p, i + 1, lines[i]);
}
}
/** 本机探测:仅报告,不参与 CI 判定(CI 机器未必装这些工具) */
export function smokeProbe(names) {
return names.map((n) => {
try {
execFileSync('sh', ['-c', `command -v ${n}`], { stdio: 'ignore' });
return { name: n, present: true };
} catch {
return { name: n, present: false };
}
});
}