modelfunction-cli 0.4.1 → 0.4.2
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/README.md +1 -0
- package/lib/check.js +118 -0
- package/lib/scan.js +4 -1
- package/mf.js +22 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ INTERNAL_LLM_KEY=... mf eval --suite mf.suite.json \
|
|
|
69
69
|
| `mf case add` | 写入用例(默认 `source=agent`、`reviewed=false`) |
|
|
70
70
|
| `mf coverage` | 每工具用例数、参数覆盖、负例比例、疑似混淆对;有 `profiles` 时按组统计 |
|
|
71
71
|
| `mf lint` | 静态检查工具描述与参数 schema,以及 skills 的 name/description(不调用模型);重复规则自动合并成一条;有 `profiles` 时给出「这些组不要合并挂载」 |
|
|
72
|
+
| `mf check` | 安装门禁:一个新能力 × 已装能力(`--manifest` + `--installed`),纯静态、返回 pass/warn、不阻断;也可 `import { check } from "modelfunction-cli"` |
|
|
72
73
|
| `mf compare` | 跨版本比较、干扰归因、混淆矩阵;两份报告都有分组时按组对比 |
|
|
73
74
|
| `mf ablate` | 留一法:去掉某个工具再跑一遍,量化它是否干扰其他工具;`--profile` 限定在某个挂载组内 |
|
|
74
75
|
| `mf cluster` | 聚类工具,建议挂载分组;参数形状拆散的同一族(如 `sessions_*`)列为「同族候选」;`--out` 把分组写成套件的 `profiles` |
|
package/lib/check.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Install-gate entry point: judge one new capability against what is already
|
|
2
|
+
// installed. Static only — no model call, no network, no file system, no state.
|
|
3
|
+
// Quality findings are advisory: the verdict is pass/warn, never "block".
|
|
4
|
+
import { lintSuite } from "./lint.js";
|
|
5
|
+
|
|
6
|
+
export function check({ manifest, installed = [] } = {}) {
|
|
7
|
+
const target = normalizeEntry(manifest);
|
|
8
|
+
if (!target) throw new Error('check: manifest 需要非空 "name"');
|
|
9
|
+
const pool = [];
|
|
10
|
+
if (Array.isArray(installed)) pool.push(...installed);
|
|
11
|
+
else if (installed && typeof installed === "object") {
|
|
12
|
+
pool.push(...(installed.tools ?? []), ...(installed.skills ?? []));
|
|
13
|
+
}
|
|
14
|
+
const entries = pool.map(normalizeEntry).filter(Boolean);
|
|
15
|
+
|
|
16
|
+
const tools = [];
|
|
17
|
+
const skills = [];
|
|
18
|
+
for (const entry of entries) (isToolLike(entry) ? tools : skills).push(entry);
|
|
19
|
+
(isToolLike(target) ? tools : skills).push(target);
|
|
20
|
+
|
|
21
|
+
const report = lintSuite({ tools, skills });
|
|
22
|
+
const findings = report.findings
|
|
23
|
+
.filter((finding) => touches(finding, target.name))
|
|
24
|
+
.map((finding) => shape(finding, target.name));
|
|
25
|
+
const summary = { error: 0, warn: 0, info: 0 };
|
|
26
|
+
for (const finding of findings) summary[finding.severity] += 1;
|
|
27
|
+
const family = familyOf(report.findings.filter((finding) => finding.pair), target.name);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
verdict: summary.error + summary.warn > 0 ? "warn" : "pass",
|
|
31
|
+
summary,
|
|
32
|
+
findings,
|
|
33
|
+
...(family.length > 1 ? { family } : {})
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeEntry(entry) {
|
|
38
|
+
if (!entry || typeof entry !== "object") return null;
|
|
39
|
+
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
|
40
|
+
if (!name) return null;
|
|
41
|
+
const parameters = entry.parameters ?? entry.inputSchema ?? entry.input ?? null;
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
description: typeof entry.description === "string" ? entry.description : "",
|
|
45
|
+
parameters: parameters && typeof parameters === "object" ? parameters : null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A capability with a parameter schema is a tool; name + description alone is
|
|
50
|
+
// a prompt-injected skill (SKILL.md form).
|
|
51
|
+
function isToolLike(entry) {
|
|
52
|
+
return entry.parameters !== null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function touches(finding, name) {
|
|
56
|
+
if (finding.kind === "skill") {
|
|
57
|
+
if (finding.skill === name) return true;
|
|
58
|
+
return Array.isArray(finding.pair) && finding.pair.includes(name);
|
|
59
|
+
}
|
|
60
|
+
if (finding.tool === name) return true;
|
|
61
|
+
return Array.isArray(finding.pair) && finding.pair.includes(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shape(finding, name) {
|
|
65
|
+
const other = (finding.pair ?? []).find((item) => item !== name);
|
|
66
|
+
return {
|
|
67
|
+
severity: finding.severity,
|
|
68
|
+
code: finding.code,
|
|
69
|
+
subject: name,
|
|
70
|
+
...(finding.param ? { param: finding.param } : {}),
|
|
71
|
+
...(other ? { with: other } : {}),
|
|
72
|
+
message: finding.message,
|
|
73
|
+
...(finding.suggestion ? { suggestion: finding.suggestion } : {})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The connected family around the manifest — what an installer shows as
|
|
78
|
+
// "these installed capabilities compete with the new one".
|
|
79
|
+
function familyOf(pairs, name) {
|
|
80
|
+
const edges = new Map();
|
|
81
|
+
const link = (a, b) => {
|
|
82
|
+
if (!edges.has(a)) edges.set(a, new Set());
|
|
83
|
+
edges.get(a).add(b);
|
|
84
|
+
};
|
|
85
|
+
for (const finding of pairs) {
|
|
86
|
+
const [a, b] = finding.pair;
|
|
87
|
+
link(a, b);
|
|
88
|
+
link(b, a);
|
|
89
|
+
}
|
|
90
|
+
if (!edges.has(name)) return [name];
|
|
91
|
+
const seen = new Set([name]);
|
|
92
|
+
const queue = [name];
|
|
93
|
+
while (queue.length > 0) {
|
|
94
|
+
const current = queue.shift();
|
|
95
|
+
for (const next of edges.get(current) ?? []) {
|
|
96
|
+
if (seen.has(next)) continue;
|
|
97
|
+
seen.add(next);
|
|
98
|
+
queue.push(next);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...seen].sort();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatCheck(result) {
|
|
105
|
+
const lines = [
|
|
106
|
+
`verdict: ${result.verdict}(error ${result.summary.error} / warn ${result.summary.warn} / info ${result.summary.info})`
|
|
107
|
+
];
|
|
108
|
+
if (result.findings.length === 0) lines.push("没有发现问题");
|
|
109
|
+
for (const finding of result.findings) {
|
|
110
|
+
const param = finding.param ? `.${finding.param}` : "";
|
|
111
|
+
const withWhat = finding.with ? ` × ${finding.with}` : "";
|
|
112
|
+
lines.push(`[${finding.severity}] ${finding.code} ${finding.subject}${param}${withWhat}`);
|
|
113
|
+
lines.push(` ${finding.message}`);
|
|
114
|
+
if (finding.suggestion) lines.push(` ${finding.suggestion}`);
|
|
115
|
+
}
|
|
116
|
+
if (result.family) lines.push("", `族:${result.family.join(", ")}`);
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
package/lib/scan.js
CHANGED
|
@@ -50,6 +50,9 @@ const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".py"]);
|
|
|
50
50
|
// Test fixtures and mocks use the same APIs as real tool definitions, so any
|
|
51
51
|
// match there is noise for a tool inventory.
|
|
52
52
|
const TEST_PATH = /(^|[\\/])(__tests__|tests?)([\\/])|\.(test|spec)\.[cm]?[jt]sx?$|(^|[\\/])test_[^\\/]+\.py$|_test\.py$/i;
|
|
53
|
+
// Dev harnesses shape objects like tools (`{ name: "ping", inputSchema }`) but
|
|
54
|
+
// are not part of the agent's tool surface.
|
|
55
|
+
const DEV_PATH = /(^|[\\/])(scripts?|bench|benchmarks?|dev|examples?|fixtures?|mocks?)([\\/])|[-_.](runner|scan|bench|fixture|mock)\.[cm]?[jt]sx?$/i;
|
|
53
56
|
|
|
54
57
|
const CODE_PATTERNS = [
|
|
55
58
|
{ label: "registerTool", confidence: "medium", re: /registerTool\s*\(\s*["'`]([\w.-]+)["'`]/ },
|
|
@@ -238,7 +241,7 @@ export async function scanProject(root = ".") {
|
|
|
238
241
|
});
|
|
239
242
|
}
|
|
240
243
|
}
|
|
241
|
-
if (!name.endsWith(".py") && !TEST_PATH.test(rel)) {
|
|
244
|
+
if (!name.endsWith(".py") && !TEST_PATH.test(rel) && !DEV_PATH.test(rel)) {
|
|
242
245
|
for (const match of text.matchAll(/(?:export\s+)?const\s+([A-Z][A-Z0-9_]{2,})\s*=\s*(['"])([^'"\n]{1,200})\2/g)) {
|
|
243
246
|
if (!constIndex.has(match[1])) constIndex.set(match[1], match[3]);
|
|
244
247
|
}
|
package/mf.js
CHANGED
|
@@ -10,6 +10,7 @@ import { probeStdioServer } from "./lib/mcp-probe.js";
|
|
|
10
10
|
import { loadAdapterTools } from "./lib/adapter-import.js";
|
|
11
11
|
import { addCases, computeCoverage, formatCoverage } from "./lib/cases.js";
|
|
12
12
|
import { lintSuite, formatLint } from "./lib/lint.js";
|
|
13
|
+
import { check, formatCheck } from "./lib/check.js";
|
|
13
14
|
import { suiteFingerprint, hashText, hashOf } from "./lib/fingerprint.js";
|
|
14
15
|
import { runExecutionChecks } from "./lib/eval-exec.js";
|
|
15
16
|
import { checkSandbox, formatSandboxReport } from "./lib/sandbox-check.js";
|
|
@@ -51,6 +52,7 @@ Usage:
|
|
|
51
52
|
mf case add --suite <file> (--input <file> | stdin) [--source agent] [--reviewed] [--dry-run]
|
|
52
53
|
mf coverage [--suite <file>] [--json]
|
|
53
54
|
mf lint [--suite <file>] [--json] # 静态检查 tools 与 skills,不调用模型
|
|
55
|
+
mf check --manifest <file> [--installed <file>] [--json] # 安装门禁:新能力 × 已装能力
|
|
54
56
|
mf compare <before.json> <after.json> [--json]
|
|
55
57
|
mf ablate --tool <name> [--suite <file>] [--profile <group>] [--models <ids>] [--json]
|
|
56
58
|
mf cluster [--suite <file>] [--threshold <0.5>] [--max-size <10>] [--out <file>] [--json] # 建议挂载分组 + 同族候选
|
|
@@ -425,6 +427,19 @@ export async function runSandboxCheck(options, io = console) {
|
|
|
425
427
|
return 0;
|
|
426
428
|
}
|
|
427
429
|
|
|
430
|
+
// Advisory install gate: static only, never blocks. Exit code stays 0 for a
|
|
431
|
+
// warn verdict so a platform can log it without failing the install.
|
|
432
|
+
export async function runCheck(options, io = console) {
|
|
433
|
+
if (!options.manifest) throw new Error("--manifest <file> 必填(新能力的 [name, description, parameters?])");
|
|
434
|
+
const manifest = JSON.parse(await readFile(resolve(options.manifest), "utf8"));
|
|
435
|
+
const installed = options.installed
|
|
436
|
+
? JSON.parse(await readFile(resolve(options.installed), "utf8"))
|
|
437
|
+
: [];
|
|
438
|
+
const result = check({ manifest, installed });
|
|
439
|
+
io.log(options.json ? JSON.stringify(result, null, 2) : formatCheck(result));
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
428
443
|
export async function runLint(options, io = console) {
|
|
429
444
|
const suite = await loadSuite(resolve(options.suite ?? DEFAULT_SUITE), {
|
|
430
445
|
allowEmptyCases: true,
|
|
@@ -669,6 +684,8 @@ function parseArgs(argv) {
|
|
|
669
684
|
reviewed: false,
|
|
670
685
|
dryRun: false,
|
|
671
686
|
tool: null,
|
|
687
|
+
manifest: null,
|
|
688
|
+
installed: null,
|
|
672
689
|
threshold: 0.5,
|
|
673
690
|
maxSize: 10,
|
|
674
691
|
baseline: null,
|
|
@@ -728,6 +745,8 @@ function parseArgs(argv) {
|
|
|
728
745
|
case "--reviewed": options.reviewed = true; break;
|
|
729
746
|
case "--dry-run": options.dryRun = true; break;
|
|
730
747
|
case "--tool": options.tool = value(); break;
|
|
748
|
+
case "--manifest": options.manifest = resolve(value()); break;
|
|
749
|
+
case "--installed": options.installed = resolve(value()); break;
|
|
731
750
|
case "--threshold": {
|
|
732
751
|
const raw = Number(value());
|
|
733
752
|
if (!Number.isFinite(raw) || raw <= 0 || raw > 1) throw new Error("--threshold expects a ratio between 0 and 1");
|
|
@@ -821,7 +840,7 @@ if (isMain) {
|
|
|
821
840
|
}
|
|
822
841
|
} else if (argv.length === 0 || argv[0] === "help" || argv[0] === "-h" || argv[0] === "--help") {
|
|
823
842
|
process.stdout.write(USAGE);
|
|
824
|
-
} else if (!["eval", "scan", "case", "coverage", "lint", "ablate", "cluster", "exec"].includes(argv[0])) {
|
|
843
|
+
} else if (!["eval", "scan", "case", "coverage", "lint", "check", "ablate", "cluster", "exec"].includes(argv[0])) {
|
|
825
844
|
process.stderr.write(`unknown command "${argv[0]}"\n\n${USAGE}`);
|
|
826
845
|
process.exit(1);
|
|
827
846
|
} else if (argv[0] === "case" && argv[1] !== "add") {
|
|
@@ -844,6 +863,8 @@ if (isMain) {
|
|
|
844
863
|
? await runCoverage(options)
|
|
845
864
|
: argv[0] === "lint"
|
|
846
865
|
? await runLint(options)
|
|
866
|
+
: argv[0] === "check"
|
|
867
|
+
? await runCheck(options)
|
|
847
868
|
: argv[0] === "ablate"
|
|
848
869
|
? await runAblate(options)
|
|
849
870
|
: argv[0] === "cluster"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelfunction-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "mf — function-calling accuracy scorecard for AI agents",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"pack:check": "npm pack --dry-run",
|
|
32
32
|
"prepublishOnly": "npm test"
|
|
33
33
|
},
|
|
34
|
+
"exports": {
|
|
35
|
+
".": "./lib/check.js"
|
|
36
|
+
},
|
|
34
37
|
"engines": {
|
|
35
38
|
"node": ">=20"
|
|
36
39
|
},
|