/** * Do the proxy's two recorders agree about the traffic they both saw? * * WHAT THIS IS, AND WHAT IT IS NOT. The savings this package publishes come * from the accounting ledger, which the proxy writes from its own arithmetic on * the request path. Nothing checked that arithmetic against a second record of * the same traffic, so a byte counted at the wrong point or a price attributed * to the wrong model would have been published as a measurement. The capture * directory is that second record: it holds each request body AS IT ARRIVED, * written by a different code path at a different moment. * * This is therefore a CROSS-RECORDER audit, not an external one. Both recorders * live inside the same proxy, so a fault they share -- a body already altered * before either saw it -- is invisible here and needs a capture taken off the * wire. What it does catch is the family of defects that has actually happened * in this repository: a figure attributed to the wrong record, a count taken * after a re-serialization rather than before it, and a corpus that is not the * corpus the numbers were computed over. * * MULTISETS, NOT PAIRS, AND THAT IS DELIBERATE. The two files have no shared * request id, and the ledger is appended when a response completes while the * capture is written when a request arrives, so concurrent requests appear in * different orders. Pairing them by position would invent a correspondence and * then report agreement about it. Comparing the multiset of byte lengths and * the multiset of models per path needs no correspondence at all: either the * same values occur the same number of times or they do not, whatever order * they arrived in, and a difference is a fact rather than an inference. * * ONLY THE OVERLAP IS COMPARED, AND THE REST IS COUNTED. Capture and accounting * are enabled independently, so one file routinely covers a period the other * does not -- and even when both are on for a whole session, the first request * reaches the capture before it reaches the ledger. Records outside the * intersection of the two extents are EXCLUDED and reported as excluded, * because a boundary request is legitimately in one recorder and not the other. * An exclusion that is never counted is the difference between "they agree" and * "the parts I chose to look at agree". * * ONE KNOWN FALSE POSITIVE, RECORDED RATHER THAN HIDDEN. The ledger counts the * request as raw bytes; the capture stores it as a JSON string, decoded as * UTF-8. A body that is not valid UTF-8 therefore reaches the two recorders as * two different lengths, and this audit reports that as a disagreement. It is * the right answer for the wrong reason -- the bodies at issue are JSON from an * HTTP client, so a malformed one is itself worth looking at -- but an operator * chasing a finding needs to know the decoder is in the path. * * READS BODIES, EMITS NONE. A capture directory is conversation content in * plaintext -- see `proxy/capture.ts` for why it is opt-in and announced. This * module parses those bodies and keeps NOTHING from them but the byte length * and the `model` field, which the ledger already records. No message, no * system prompt, no tool body, and no hash of any of them reaches a finding. */ /** The file `proxy/capture.ts` appends to inside a capture directory. */ export declare const CAPTURE_FILE = "requests.jsonl"; /** Everything this audit keeps from one captured request. */ export interface CapturedRequest { /** Epoch milliseconds, as the capture writer recorded them. */ readonly at: number; readonly path: string; /** UTF-8 length of the body as it arrived -- never the body itself. */ readonly bytes: number; /** The model the body named, or null when it named none we could read. */ readonly model: string | null; } /** What a capture directory yielded, with what it could not. */ export interface CaptureRead { readonly requests: readonly CapturedRequest[]; /** Lines that were not parseable JSON objects of the expected shape. */ readonly skipped: number; /** True when the directory held no capture file at all. */ readonly missing: boolean; } /** * Reads a capture directory, keeping only the projection above. * * STREAMED, AND THE BODY IS DROPPED AS SOON AS IT IS MEASURED. A capture file * is the largest thing this package writes -- it is whole request bodies -- so * slurping it to count bytes would need more memory than the proxy that wrote * it. Each line is parsed, measured and discarded. * * A LINE THAT DOES NOT PARSE IS SKIPPED AND COUNTED, the same rule the ledger * reader follows: a capture can be truncated by a full disk mid-write, and * refusing to audit nine thousand good records because of one torn line would * be the wrong trade. The count is returned so a reader is told. */ export declare function readCapture(dir: string): Promise; /** * One capture line as the projection, or null when it is not one. * * THE MODEL IS READ, THE REST IS NOT. `model` is the field the ledger prices * the request by, so it is the one value in the body this audit has a question * about. A body that is not JSON, or names no model, yields a null model and is * still counted -- a request whose model could not be read is a fact about the * corpus, and dropping it would shrink the corpus silently. */ export declare function projectCaptureLine(line: string): CapturedRequest | null; /** Everything this audit keeps from one ledger record. */ export interface LedgerRequest { /** * Epoch milliseconds of the instant the request ARRIVED, which is the instant * the capture also stamps -- not the `ts` on the record. See * `readLedgerProjection` for why the two differ and how the gap is undone. */ readonly at: number; readonly path: string; /** The record's own count of the body before transformation. */ readonly bytes: number; readonly model: string | null; } export interface LedgerProjection { readonly requests: readonly LedgerRequest[]; readonly skipped: number; readonly missing: boolean; /** * Records with no `timing`, whose arrival could not be recovered and which * therefore sit on the completion clock instead. * * COUNTED, BECAUSE IT CHANGES WHAT THE WINDOW MEANS. `timing` is optional so * that an old ledger stays readable, and an old ledger is exactly the one * whose records are placed a round trip late. That only matters at the edges * of the compared window, but an operator cannot judge whether it mattered * here unless the number is in front of them. */ readonly unaligned: number; } /** * The whole ledger, projected to the four fields this audit compares. * * ONE VALIDITY RULE, NOT TWO. `looksLikeRecord` is the ledger reader's own test * for "is this line a record", imported rather than restated, so a line this * audit silently ignores is exactly a line `token-optimizer-inspect` would also * refuse to render. A second copy of that rule would eventually disagree, and * the audit would then be measuring a different corpus than the command it is * auditing. * * PROJECTED WHILE STREAMING, because `readLedger` keeps whole records and this * needs every record rather than the last few: a session's ledger is tens of * megabytes, and the audit has a question about four fields of it. */ export declare function readLedgerProjection(path: string): Promise; /** One value the two recorders do not report the same number of times. */ export interface Disagreement { readonly path: string; /** The byte length, or the model id, the two sides disagree about. */ readonly value: string; readonly inCapture: number; readonly inLedger: number; } /** How much of one side's records the comparison actually used. */ export interface SideSummary { readonly total: number; readonly skipped: number; readonly missing: boolean; /** Records inside the overlap, which are the ones compared. */ readonly compared: number; /** Records outside it, excluded and reported rather than silently dropped. */ readonly excluded: number; /** * Records whose arrival could not be recovered -- always zero for the * capture, which stamps arrival directly. See `LedgerProjection.unaligned`. */ readonly unaligned: number; } export interface AuditReport { readonly capture: SideSummary; readonly ledger: SideSummary; /** The intersection of the two extents, ISO, or null when there is none. */ readonly from: string | null; readonly to: string | null; readonly bytes: readonly Disagreement[]; readonly models: readonly Disagreement[]; /** * True only when something was compared AND nothing disagreed. * * A COMPARISON OF NOTHING IS NOT AGREEMENT. An empty overlap, a capture that * was never on, a ledger from a different machine -- each produces zero * disagreements, and reporting that as agreement is how an instrument comes * to certify a result it never examined. */ readonly agreed: boolean; } /** * Both recorders' agreement about the traffic they both saw. * * ONE QUESTION, ASKED TWICE. The byte length is the figure every savings number * is built on, and the model id is what each of those figures is attributed to. * They are compared separately because they fail separately: a re-serialization * moves the bytes and leaves the attribution intact, and a mis-read request * moves the attribution and leaves the bytes intact. */ export declare function auditRecorders(captureDir: string, ledgerPath: string): Promise; /** * The value standing in for a body with no `model` field. * * A LITERAL NOBODY CAN SEND. A model id is a string from the request body, so * any readable placeholder could also arrive as a real value and collide with * it; a null byte cannot appear in one. */ export declare const NO_MODEL = "\0none"; /** * The report as lines. * * LEADS WITH WHAT WAS COMPARED, NOT WITH THE VERDICT. "They agree" over four of * nine thousand requests is the shape of answer this instrument exists to stop * giving, so the extent comes first and the verdict last. */ export declare function renderAudit(report: AuditReport): readonly string[]; //# sourceMappingURL=audit.d.ts.map