/** * column-types — the tool declared what its rows contain, and the rows say * otherwise. Filed at the WRITE seam, at the moment the tool answers. * * Pattern: pure function over (one declaration, one finished rowset); the * SHAPE check, one storey below `empty-lookup` — that one asks * whether the answer had any rows, this one asks what is in them. * Role: the decidable fragment of "is this rowset the rowset it claims to * be?". * * ── THE MEASURED FAILURES. Three, and they are one shape ──────────────── * * 1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. LUN 0 * is falsy, so on 2,094 mappings LUN 0 was stored as an EMPTY STRING — * and a host group missing the LUN an initiator probes first became * indistinguishable from one that had it. The column was numeric; the * value was `''`; nothing anywhere disagreed. * 2. A capacity view rendered `round(mib / 1024, 1)`, so an 8 MiB disk came * out as `0.0 GB` — which reads as NO DISK, i.e. a provisioning failure, * during a live desktop-fleet incident. * 3. Earlier in the same application, a whole family of tools returned their * numbers as quoted strings (`"1240"`). Every chart silently went blank, * because nothing downstream could tell a measure from a label. * * All three are "a number became something else, and nothing noticed at the * seam". The library already lets a tool declare what its result IS * (`resultKind`, 9.70.0). It did not let a tool declare what its result * CONTAINS, so there was nothing for a rowset to be wrong against. * * ── THE CEILING, and case 2 is the whole argument for stating it ──────── * This judges TYPE, never MEANING. It can see that a column declared * `number` holds a string. It can NEVER see that the string should have been * `0`, or that `0.0` should have been `0.0078`. Case 2 above passes this * check cleanly — `0.0` is a perfectly good number — and the check says so * out loud rather than letting a green row imply otherwise. The ceiling ships * as {@link COLUMN_TYPE_CEILING} and is quoted verbatim into every finding, * the `EMPTY_LOOKUP_CEILING` law: one owner for the bound, so it cannot drift * out of a message and leave a reader believing the library knows more than * it does. * * ── TWO FINDINGS, because the field bug turned on the difference ──────── * • `column-type-mismatch` — the column is THERE and holds the wrong thing. * Cases 1 and 3. * • `missing-column` — the column the author declared is in NO row of the * result. Nothing to type-check; the promise was broken one level up. * Collapsing them would recreate the exact ambiguity the LUN report died of: * "the value is not what it should be" and "the value is not there" send a * person to two different files, and a checker that says only "something is * off with logical_unit_number" has helped with neither. * * ── DECLARED, NEVER INFERRED ──────────────────────────────────────────── * A tool is this check's subject only because its author wrote * `resultColumns`. Nothing here sniffs a type off the data — sniffing is * precisely what the consumers do today, and precisely what produced the * failures above: one stray `''` demotes a numeric column to text, and the * demotion is silent. * * ── WHAT IS NOT JUDGED ────────────────────────────────────────────────── * See {@link readRowset}. A result is read only when it is an array of plain * objects with at least one row. Everything else — prose, a `null`, a bespoke * `{ rows: [...] }` wrapper, a claim ticket, AND the zero-row result — is * `not-applicable`, filed as a ROW. The zero-row case belongs to the * neighbour (`empty-lookup`) and is deliberately not stolen: an empty result * has no columns to be wrong about, and filing `missing-column` for every * declared column of an empty answer would turn one honest emptiness into a * pile of false accusations. */ import type { Disposition } from '../disposition/types.js'; import type { ContextError } from '../finding/types.js'; import { type ColumnType, type ToolResultColumns } from './types.js'; /** * THE CEILING, as one string with one owner. * * Quoted verbatim into every finding's message, into this folder's README and * into the docs page, so the bound cannot drift out of one of them. */ export declare const COLUMN_TYPE_CEILING: string; /** How the boundary was told to act on what this finds. */ export type ColumnCheckMode = 'warn' | 'enforce'; /** * What the library could read about a finished result: the rows, or nothing. * * Deliberately thin. The check needs the rows and nothing else, and a reading * that carried a verdict would be this file judging in two places. */ export interface RowsetReading { readonly rows: readonly Readonly>[]; } /** * READ a finished result as a rowset, or decline to. * * The one readable shape, and why only this one: an ARRAY OF PLAIN OBJECTS * with at least one row. That is what a rowset is on this wire, it is what * every consumer named in the docs page already expects, and it is the same * `Array.isArray` law the neighbouring check reads by — the two must never * disagree about what a rowset is. * * `undefined` (⇒ `not-applicable`, a ROW) for everything else: * • a non-array — prose, a `null`, a `{ rows: [...] }` wrapper, a ticket; * • an array holding anything that is not a plain object — a list of * strings has no columns, and inventing some is how a checker starts * lying; * • an array of ZERO rows — an empty answer has no columns to be wrong * about, and it is the neighbour's subject, not this one's. */ export declare function readRowset(value: unknown): RowsetReading | undefined; /** One finished call to a tool whose author declared its result's columns. */ export interface ColumnTypesCall { readonly toolName: string; /** The provider's id for this call — what the witness points a reader at. */ readonly toolCallId: string; /** `Tool.resultColumns`, exactly as the author wrote it. */ readonly columns: ToolResultColumns; /** What the library could read of the result — `undefined` = not a rowset. */ readonly reading: RowsetReading | undefined; /** What the boundary will do with a finding — carried so the message can * say what actually happened rather than guess. */ readonly mode: ColumnCheckMode; } /** One declared column the rows disagreed with. */ export interface ColumnViolation { readonly column: string; readonly declared: ColumnType; /** How many rows hold something that is not the declared type. */ readonly rows: number; /** Total rows read, so a reader can see 3-of-4 rather than a bare 3. */ readonly ofRows: number; /** The first offending value, rendered and clipped — what a person recognizes. */ readonly sample: string; /** What that first offending value actually is (`string`, `null`, `missing`, …). */ readonly got: string; } /** One encounter's outcome: the findings, the ledger row, and — under * `enforce` — the sentence that replaces the payload. */ export interface ColumnTypesEncounter { readonly findings: readonly ContextError[]; /** Computed HERE, beside the rules that decide it, so "the library refused * to judge this shape" is provable without a live agent. */ readonly disposition: Disposition; /** * The teaching refusal, present ONLY under `mode: 'enforce'` with at least * one finding. The `applyResultCeiling` idiom, for the same reason: the * model reads a sentence that says what was wrong and how to fix it, never * a stack trace and never a truncated result that reads as complete. */ readonly refusal?: string; } /** * Judge one finished call to a tool that declared its result's columns. * * @param call the call, the declaration, and what the library could read of * the result. The caller has already established the tool declared * `resultColumns` and that the dial is on; nothing here re-decides arming. * @param epoch the run iteration, stamped on every witness. */ export declare function columnTypesOf(call: ColumnTypesCall, epoch: number): ColumnTypesEncounter; //# sourceMappingURL=check.d.ts.map