modelfunction-cli 0.0.0-stage → 0.1.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 +74 -2
- package/fixtures/models.json +39 -0
- package/fixtures/suite.json +150 -0
- package/lib/ablate.js +120 -0
- package/lib/adapter-import.js +24 -0
- package/lib/adapters/mock.js +143 -0
- package/lib/adapters/openai.js +95 -0
- package/lib/cases.js +188 -0
- package/lib/compare.js +220 -0
- package/lib/eval-exec.js +140 -0
- package/lib/exec.js +265 -0
- package/lib/fingerprint.js +52 -0
- package/lib/fsutil.js +7 -0
- package/lib/lint.js +110 -0
- package/lib/mcp-probe.js +152 -0
- package/lib/mcp-server.js +216 -0
- package/lib/report.js +119 -0
- package/lib/runtime/harness.mjs +145 -0
- package/lib/runtime/harness.py +373 -0
- package/lib/sandbox-check.js +166 -0
- package/lib/scan.js +510 -0
- package/lib/score.js +137 -0
- package/lib/suite.js +117 -0
- package/mf.js +636 -0
- package/package.json +38 -4
- package/skills/mf-cases/SKILL.md +58 -0
package/lib/suite.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const EMPTY_SCHEMA = { type: "object", properties: {} };
|
|
5
|
+
|
|
6
|
+
export async function loadSuite(suitePath, { allowEmptyCases = false } = {}) {
|
|
7
|
+
const abs = resolve(suitePath);
|
|
8
|
+
const raw = JSON.parse(await readFile(abs, "utf8"));
|
|
9
|
+
if (!Array.isArray(raw.tools) || raw.tools.length === 0) {
|
|
10
|
+
throw new Error(`suite ${abs}: "tools" must be a non-empty array`);
|
|
11
|
+
}
|
|
12
|
+
if (!Array.isArray(raw.cases) || (raw.cases.length === 0 && !allowEmptyCases)) {
|
|
13
|
+
throw new Error(`suite ${abs}: "cases" must be a non-empty array`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const baseDir = dirname(abs);
|
|
17
|
+
const tools = await Promise.all(raw.tools.map((entry) => normalizeTool(entry, baseDir)));
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const tool of tools) {
|
|
20
|
+
if (seen.has(tool.name)) throw new Error(`suite ${abs}: duplicate tool "${tool.name}"`);
|
|
21
|
+
seen.add(tool.name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cases = raw.cases.map((entry, index) => normalizeCase(entry, index, seen));
|
|
25
|
+
return {
|
|
26
|
+
name: raw.name ?? "unnamed",
|
|
27
|
+
description: raw.description ?? "",
|
|
28
|
+
path: abs,
|
|
29
|
+
tools,
|
|
30
|
+
cases,
|
|
31
|
+
execution:
|
|
32
|
+
raw.execution && typeof raw.execution === "object" && !Array.isArray(raw.execution)
|
|
33
|
+
? { ...raw.execution }
|
|
34
|
+
: null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function normalizeTool(entry, baseDir) {
|
|
39
|
+
if (entry.ref) {
|
|
40
|
+
const specPath = resolve(baseDir, entry.ref);
|
|
41
|
+
const spec = JSON.parse(await readFile(specPath, "utf8"));
|
|
42
|
+
return {
|
|
43
|
+
name: requireString(spec.name, `tool ${specPath}: "name"`),
|
|
44
|
+
description: spec.description ?? "",
|
|
45
|
+
parameters: spec.input ?? spec.parameters ?? EMPTY_SCHEMA,
|
|
46
|
+
source: specPath,
|
|
47
|
+
sideEffects: normalizeSideEffects(spec.sideEffects ?? spec.side_effects)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: requireString(entry.name, "tool name"),
|
|
52
|
+
description: entry.description ?? "",
|
|
53
|
+
parameters: entry.parameters ?? entry.input ?? EMPTY_SCHEMA,
|
|
54
|
+
source: null,
|
|
55
|
+
sideEffects: normalizeSideEffects(entry.sideEffects ?? entry.side_effects)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeCase(entry, index, toolNames) {
|
|
60
|
+
const id = entry.id ?? `case_${index + 1}`;
|
|
61
|
+
if (typeof entry.input !== "string" || entry.input.trim() === "") {
|
|
62
|
+
throw new Error(`case ${id}: "input" must be a non-empty string`);
|
|
63
|
+
}
|
|
64
|
+
const expectedTool = entry.expectedTool ?? null;
|
|
65
|
+
if (expectedTool !== null && !toolNames.has(expectedTool)) {
|
|
66
|
+
throw new Error(`case ${id}: expectedTool "${expectedTool}" is not in the tool list`);
|
|
67
|
+
}
|
|
68
|
+
if (expectedTool === null && entry.expectedArgs && Object.keys(entry.expectedArgs).length > 0) {
|
|
69
|
+
throw new Error(`case ${id}: expectedArgs requires an expectedTool`);
|
|
70
|
+
}
|
|
71
|
+
const toolInput =
|
|
72
|
+
entry.toolInput && typeof entry.toolInput === "object" && !Array.isArray(entry.toolInput)
|
|
73
|
+
? entry.toolInput
|
|
74
|
+
: null;
|
|
75
|
+
if (toolInput && expectedTool === null) {
|
|
76
|
+
throw new Error(`case ${id}: toolInput requires an expectedTool`);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
input: entry.input,
|
|
81
|
+
expectedTool,
|
|
82
|
+
expectedArgs: entry.expectedArgs ?? {},
|
|
83
|
+
note: entry.note ?? "",
|
|
84
|
+
source: typeof entry.source === "string" && entry.source ? entry.source : "manual",
|
|
85
|
+
reviewed: entry.reviewed === undefined ? true : Boolean(entry.reviewed),
|
|
86
|
+
tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [],
|
|
87
|
+
toolInput,
|
|
88
|
+
expect:
|
|
89
|
+
entry.expect && typeof entry.expect === "object" && !Array.isArray(entry.expect)
|
|
90
|
+
? entry.expect
|
|
91
|
+
: null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeSideEffects(value) {
|
|
96
|
+
if (typeof value !== "string") return null;
|
|
97
|
+
const level = value.trim().toLowerCase();
|
|
98
|
+
return ["none", "read", "write", "external"].includes(level) ? level : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function requireString(value, label) {
|
|
102
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
103
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
104
|
+
}
|
|
105
|
+
return value.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function toModelTools(tools) {
|
|
109
|
+
return tools.map((tool) => ({
|
|
110
|
+
type: "function",
|
|
111
|
+
function: {
|
|
112
|
+
name: tool.name,
|
|
113
|
+
description: tool.description,
|
|
114
|
+
parameters: tool.parameters
|
|
115
|
+
}
|
|
116
|
+
}));
|
|
117
|
+
}
|