agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
209 lines (205 loc) • 8.94 kB
JavaScript
;
/**
* check:semantics CLI core (9.53.0 — the build gate).
*
* Pattern: humble shell (the tool-lint `cli.ts` precedent) —
* `bin/agentfootprint-check-semantics.mjs` is a 2-line wrapper;
* ALL behavior (arg parsing, catalog coercion, report, exit code)
* lives here so it is unit-testable without spawning a process.
* Role: `src/semantics/`. Reads ONE JSON file of tools + sample
* results, prints a report, returns the process exit code:
* 0 — report.ok (and, under --strict, zero warnings)
* 1 — findings failed the gate
* 2 — usage / input error (bad flags, unreadable file,
* unrecognized JSON shape, unknown resultClass)
*
* Consumers wire it beside their other gates (the `check:tools` convention):
*
* "check:semantics": "node scripts/dump-semantics-catalog.mjs && agentfootprint-check-semantics semantics-catalog.json"
*
* The catalog is sample results — what your MOCK tools return — because the
* gate judges result shapes, and a build must never run tools that touch
* live systems. A mock catalog is something this ecosystem already has
* everywhere the check matters.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runCheckSemanticsCli = exports.coerceSemanticsCatalog = void 0;
const check_js_1 = require("./check.js");
const format_js_1 = require("./format.js");
const overclaim_js_1 = require("./overclaim.js");
const types_js_1 = require("./types.js");
const USAGE = `usage: agentfootprint-check-semantics <semantics-catalog.json> [options]
<semantics-catalog.json> JSON file of tools + sample results. Accepted shapes:
[{ name, resultClass?, results: [...] }]
[{ name, resultClass?, result: ... }] (single sample)
{ tools: [...] } (either row shape)
resultClass ∈ { ${types_js_1.RESULT_CLASSES.join(', ')} } — the class
declared on defineTool({ resultClass }); omit for tools
with no class rules (envelope rules still apply).
refusals: [...] (optional) — what the tool throws or
returns when it refuses a call, or { text, argument,
value } naming what it looked up; judged by the
overclaim rules.
--strict warnings also fail the gate; the overclaim rules
report as errors
--rules=<set,...> opt-in rule sets: ${overclaim_js_1.SEMANTICS_RULE_SETS.join(', ')} — errors, judged
over every sample
--json print the full report as JSON instead of text
exit codes: 0 ok · 1 findings failed the gate · 2 usage/input error`;
/**
* Normalize the accepted JSON shapes to the checker's catalog. Throws (with
* a shape description) on unrecognized input — the CLI maps that to exit
* code 2. `resultClass` values are passed through untouched; the checker is
* the one owner of the closed-set refusal.
*/
function coerceSemanticsCatalog(json) {
const list = Array.isArray(json)
? json
: json !== null &&
typeof json === 'object' &&
Array.isArray(json.tools)
? json.tools
: undefined;
if (list === undefined) {
throw new Error('expected a JSON array of { name, resultClass?, results } rows or { tools: [...] }');
}
return list.map((raw, index) => {
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
throw new Error(`tools[${index}] is not an object`);
}
const entry = raw;
const name = entry.name;
if (typeof name !== 'string' || name.length === 0) {
throw new Error(`tools[${index}] has no string 'name'`);
}
const results = Array.isArray(entry.results)
? entry.results
: 'result' in entry
? [entry.result]
: undefined;
if (results === undefined) {
throw new Error(`tools[${index}] ('${name}') has neither 'results' (an array of sample results) nor ` +
`'result' (one sample) — the gate judges sample results; a mock tool's return is ` +
`the natural sample.`);
}
return {
name,
...(entry.resultClass !== undefined
? { resultClass: entry.resultClass }
: {}),
results,
...(entry.projections !== undefined
? { projections: entry.projections }
: {}),
...(entry.refusals !== undefined
? { refusals: entry.refusals }
: {}),
};
});
}
exports.coerceSemanticsCatalog = coerceSemanticsCatalog;
/**
* Run the gate CLI. Returns the exit code (never calls `process.exit` — the
* bin wrapper assigns it to `process.exitCode`).
*/
async function runCheckSemanticsCli(argv, io = {
// eslint-disable-next-line no-console
stdout: (line) => console.log(line),
// eslint-disable-next-line no-console
stderr: (line) => console.error(line),
}) {
let file;
let strict = false;
let json = false;
let rules = [];
for (const arg of argv) {
if (arg === '--strict')
strict = true;
else if (arg === '--json')
json = true;
else if (arg.startsWith('--rules=')) {
const asked = arg
.slice('--rules='.length)
.split(',')
.filter((r) => r !== '');
const known = overclaim_js_1.SEMANTICS_RULE_SETS;
const unknown = asked.find((r) => !known.includes(r));
if (asked.length === 0 || unknown !== undefined) {
io.stderr(`unknown rule set '${unknown ?? ''}' — the opt-in rule sets are: ` +
`${overclaim_js_1.SEMANTICS_RULE_SETS.join(', ')}\n\n${USAGE}`);
return 2;
}
rules = asked;
}
else if (arg === '--help' || arg === '-h') {
io.stderr(USAGE);
return 2;
}
else if (arg.startsWith('-')) {
io.stderr(`unknown flag '${arg}'\n\n${USAGE}`);
return 2;
}
else if (file === undefined)
file = arg;
else {
io.stderr(`unexpected extra argument '${arg}'\n\n${USAGE}`);
return 2;
}
}
if (file === undefined) {
io.stderr(USAGE);
return 2;
}
let report;
try {
// Lazy node:fs import (browser-compat — the tool-lint precedent):
// `agentfootprint/observe` re-exports this module, and a top-level
// node:fs import detonates a browser bundle at module-eval. The CLI
// path is the only consumer that touches the filesystem.
const { readFile } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
const catalog = coerceSemanticsCatalog(JSON.parse(await readFile(file, 'utf8')));
const options = {
...(strict && { strict: true }),
...(rules.length > 0 && { rules }),
};
report = (0, check_js_1.checkSemantics)(catalog, options);
}
catch (error) {
io.stderr(`agentfootprint-check-semantics: ${file}: ${error.message}`);
return 2;
}
if (json)
io.stdout(JSON.stringify(report, null, 2));
else
io.stdout((0, format_js_1.formatSemanticsReport)(report));
if (!report.ok)
return 1;
if (strict && report.findings.length > 0)
return 1;
return 0;
}
exports.runCheckSemanticsCli = runCheckSemanticsCli;
//# sourceMappingURL=cli.js.map