agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
231 lines • 9.99 kB
JavaScript
/**
* semantics/described — `describedResult()`, the declaration door spelled the
* way code is written.
*
* Pattern: one core, one authoring door (`envelope.ts` · `mintSemantics`). This file
* is the camelCase door: it says how its author spells the
* declaration and respells each object to the wire. It judges
* nothing itself — every rule lives in the core, once.
* Role: lib/ layer, pure.
* Emits: N/A (the caller emits `agentfootprint.tools.semantics_declared`).
*
* ## One authoring spelling, one unchanged wire
*
* `semantic()` copied the wire's snake_case names into its declaration
* (`measured_at`, `is_counter`, `filter_note`) beside camelCase ones
* (`notChecked`, `cannotCover`), so an author wrote two spellings in one
* object — and "semantic" already means vector retrieval everywhere else in
* this library. `describedResult()` takes the same fields in camelCase and
* mints the same wire, byte for byte. The wire does not change: the model and
* any foreign process keep reading snake_case.
*
* The authoring door takes ONE spelling and refuses snake_case, quoting the key its
* author wrote and naming the one meant (`provenance.measured_at` → "did you
* mean `measuredAt`?"). A migration that swaps only the function name then
* fails loudly at the line that needs the new spelling, instead of silently
* minting without it. The declaration never accepts both spellings in one object.
*/
import { COVERAGE_DECLARATION_KEYS } from './coverage/items.js';
import { refusal, refuseUnknownKeys } from './coverage/refusal.js';
import { CLARIFY_DECLARATION_KEYS, mintSemantics, provenanceIssues, WIRE_SPELLING, } from './envelope.js';
/**
* Each declared key → its wire key, per object, tied to the types in BOTH
* directions: a declared key with no wire name, or a wire name the wire type
* does not have, fails to compile. The order is the order a refusal lists the
* fields in.
*/
const GRAIN_NAMES = {
interval: 'interval',
aggregation: 'aggregation',
isCounter: 'is_counter',
collapsed: 'collapsed',
};
const PROVENANCE_NAMES = {
measuredAt: 'measured_at',
ageSeconds: 'age_seconds',
source: 'source',
sourceExportDate: 'source_export_date',
};
const RENDER_NAMES = {
default: 'default',
columns: 'columns',
sort: 'sort',
filterNote: 'filter_note',
chartHint: 'chart_hint',
};
const WIRE_NAMES = {
grain: GRAIN_NAMES,
provenance: PROVENANCE_NAMES,
render: RENDER_NAMES,
};
/**
* How this door's author spells the six snake_case wire fields — each value
* checked against the tables above, so a renamed declared key that is not
* renamed here fails to compile instead of naming a field nobody wrote.
*/
const CAMEL_SPELLING = {
is_counter: 'isCounter',
measured_at: 'measuredAt',
age_seconds: 'ageSeconds',
source_export_date: 'sourceExportDate',
filter_note: 'filterNote',
chart_hint: 'chartHint',
};
/**
* The eleven top-level fields, tied to the type in both directions.
*/
const DECLARATION_KEYS = Object.keys({
series: true,
facts: true,
edges: true,
grain: true,
provenance: true,
period: true,
resolved: true,
paging: true,
coverage: true,
clarify: true,
render: true,
});
/**
* `value` with each key renamed to its wire name, in the author's key order.
* A value that is not a plain object is handed through untouched, so the rule
* set names it for what it is — "`provenance` must be an object ({ measuredAt,
* source, … })" — where a spread would have turned `null` into `{}` and a
* string into indexed characters. A plain object carries only known keys
* here: the door refused every other key first.
*/
function toWire(field, value) {
if (typeof value !== 'object' || value === null || Array.isArray(value))
return value;
const names = WIRE_NAMES[field];
const declared = value;
const out = {};
// `Object.hasOwn`, never `names[key]` alone: a key such as `toString` would
// otherwise read a function off Object.prototype and rename to it.
for (const key of Object.keys(declared)) {
out[Object.hasOwn(names, key) ? names[key] : key] = declared[key];
}
return out;
}
const DESCRIBED_DOOR = {
name: 'describedResult',
declarationKeys: DECLARATION_KEYS,
derivedKeys: ['notCovered', 'not_covered'],
objectKeys: [
['grain', Object.keys(GRAIN_NAMES)],
['provenance', Object.keys(PROVENANCE_NAMES)],
['coverage', COVERAGE_DECLARATION_KEYS],
['clarify', CLARIFY_DECLARATION_KEYS],
['render', Object.keys(RENDER_NAMES)],
],
toWire,
spelling: CAMEL_SPELLING,
};
/**
* Mint an author's (camelCase) `provenance` into the wire's spelling by THIS
* door's rules — `absent()` asks it (honesty step 7b), so an absence says its
* source and time in the shape, the spelling and the rule set a described
* result does: `measuredAt` and `source` required, `ageSeconds` a finite
* number ≥ 0, `sourceExportDate` a non-empty string, a snake_case key refused
* naming the camelCase one. THROWS the refusal (`refused: …`) at the line the
* author wrote. A fresh object, keys in the author's order.
*/
export function mintProvenance(declared) {
if (typeof declared === 'object' && declared !== null && !Array.isArray(declared)) {
refuseUnknownKeys(declared, Object.keys(PROVENANCE_NAMES), 'provenance');
}
const wire = toWire('provenance', declared);
const [first] = provenanceIssues(wire, CAMEL_SPELLING);
if (first !== undefined)
throw refusal(`${first.message} (field: ${first.field})`);
return wire;
}
/**
* Read a WIRE provenance (an absence minted anywhere — a Python helper, an
* older process) into the record's camelCase form, by the same rule set —
* never repaired. `{}` when none is declared (`undefined` or `null`).
*/
export function readProvenance(wire) {
if (wire === undefined || wire === null)
return {};
const [problem] = provenanceIssues(wire, WIRE_SPELLING);
if (problem !== undefined)
return { problem };
const p = wire;
return {
provenance: {
measuredAt: p.measured_at,
source: p.source,
...(p.age_seconds !== undefined && { ageSeconds: p.age_seconds }),
...(p.source_export_date !== undefined && { sourceExportDate: p.source_export_date }),
},
};
}
/**
* Return rows, a series or relationships from a system of record — WITH the
* caveats that make them honest — in a shape the framework recognizes, the
* record keeps whole, and a build gate can refuse.
*
* **Where it lives.** It is the tool's RESPONSE: `execute` returns it, and
* nothing is added to the system prompt or the tool's schema. The model reads
* a compact projection ({@link semanticsForModel}: the data, `grain`,
* `provenance`, the `not_covered` lines composed from `coverage`, a non-null
* `clarify` and a static note) — never the `checked` list, the `render` hints
* or the marker. The run record keeps the FULL envelope
* (`agentfootprint.tools.semantics_declared`), even when the result is over
* the tool's `resultCeiling` and refused. A declared `coverage` also flows
* through the channel `coverage()` uses, which is the only part that can reach
* the final answer (`.limitsTravelWithTheAnswer()`).
*
* **Values from the data.** `provenance.measuredAt` is when the WORLD was
* measured, taken from the data — the export's time, the moment of a live
* read, the newest sample of a series, the end of a window — never typed in.
* It is never parsed: the library passes your words through.
*
* **The period** (honesty step 7b). `period: { queried, held, readAt? }` says
* what time the READ covered — the instants it asked for, and what the store
* holds (or `'unknown'`) — as ISO 8601 instants with a zone. The model reads it
* as declared; the results layer compares the instants and files its verdict
* (covered · partly held · not held · unknown). A malformed period is refused
* here.
*
* Refuses (throws, at the call site — the `absent()` law) any declaration
* this vocabulary cannot honor: series without `grain`, series or facts
* without `provenance.measuredAt` and `provenance.source`, a counter-looking
* aggregation with `isCounter` unstated, and every malformed shape. Each
* refusal starts `refused: `, names the field as you spelled it, and — inside
* `execute` — is the call's error result, which the model reads in place of
* the data; the run continues. A snake_case key (`measured_at`,
* `not_checked`) is refused, naming the camelCase one.
*
* Use `absent()` when nothing matched and `coverage()` for
* any other value that has limits; never wrap one helper's result in another.
*
* @example rows from a nightly export — the time comes from the export
* ```ts
* const exportTime = snapshot.exportedAt; // when the WORLD was measured
* return describedResult({
* facts: rows.map((r) => ({ entity: r.vm, datastore: r.datastore, size_tb: r.sizeTb })),
* provenance: { measuredAt: exportTime, source: 'RVTools export' },
* coverage: {
* checked: [`every VM in the RVTools export of ${exportTime}`],
* cannotCover: [{ what: 'hosts that are not VMware', why: 'RVTools sees VMware only' }],
* },
* });
* ```
*
* @example a series — `measuredAt` is the newest sample
* ```ts
* const newest = rows.reduce((a, b) => (a.time > b.time ? a : b)).time;
* return describedResult({
* series: rows.map((r) => ({ t: r.time, entity: r.port, metric: 'avg_iops', value: r.iops })),
* grain: { interval: '30m', aggregation: 'avg', isCounter: false },
* provenance: { measuredAt: newest, source: 'InfluxDB SwitchPortStats' },
* });
* ```
*/
export function describedResult(decl) {
return mintSemantics(decl, DESCRIBED_DOOR);
}
//# sourceMappingURL=described.js.map