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 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.1",
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
  },