/** * Shared helpers for tool registration. * Reduces boilerplate — every tool follows the same pattern: * take input → convert to (d, bias, samples) → call engine → format result. */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { ComputeResponse } from "../engine-client.js"; export type Server = McpServer; /** * The engine's own classification of an AIN reading. * * AUDIT 2026-07-31: this was a second, invented set of bands - 80/60/40/20 * returning EXCELLENT / GOOD / MODERATE / WEAK / CRITICAL - described in its * own comment as "Standard AIN interpretation bands". It was neither standard * nor the engine's. Measured side by side against ain_status in zpl-core: * * AIN 85 -> "EXCELLENT" engine: NEUTRAL * AIN 75 -> "GOOD" engine: MODERATE_BIAS * AIN 55 -> "MODERATE" engine: SIGNIFICANT_BIAS * AIN 35 -> "WEAK" engine: HIGH_BIAS * * Softer than the engine at every step across the middle of the range, and * printed directly beside the number it was softening - in eight places, * several of which also show the engine's ain_status, so the same reading * carried two verdicts that disagreed. * * The engine already classifies every reading and returns ain_status on every * compute response. There is no second opinion to have. These are its * thresholds and its names. * * Takes the 0-100 display scale this package uses; the engine's thresholds are * on 0-1, so they are stated here multiplied out rather than converted at the * call site, where a stray division would be invisible. */ export declare function ainSignal(ain: number): string; /** * ZPL_MODE — controls how scores reach the AI assistant. * * pure (default): scores are computed and saved to history, but tools that * evaluate AI-generated text return only an audit pointer. * The assistant does NOT see the AIN inline — prevents * reactivity bias / observer effect. * * coach : full inline output (current behaviour). The assistant * sees the score and may self-correct. * * Tools that evaluate AI-generated text (zpl_check_response, zpl_news_bias, * zpl_review_bias) honour this. Tools that score external data * (zpl_portfolio, zpl_loot_table, etc.) ignore it — there is no AI to * influence in those cases. */ export declare const ZPL_MODE: "pure" | "coach"; /** * Wrap a tool result so AI-evaluation outputs respect ZPL_MODE. * In pure mode, returns a redacted summary with an audit ID instead of the * AIN/details. In coach mode, returns the full original text. */ export declare function maybeRedactForPureMode(args: { ain: number; tokens?: number; fullText: string; toolName: string; }): string; /** * Standard footer appended to every tool result. * Reminds the user (and any AI consuming the output) that the AIN score is a * stability measurement only — never a recommendation, prediction, or * financial / gambling / investment advice. */ export declare const ZPL_DISCLAIMER: string; /** * Signed distance of the output balance from the 0.500 equilibrium point. * * AUDIT 2026-07-30: AIN is defined with an absolute value in it, so it cannot * say which side of equilibrium a result sits on — p_output 0.4687 and 0.5313 * both come back as AIN 93.73. For a method whose purpose is finding a stable * centre, which way it leans is half the answer, and that half was being * discarded before anything reached the screen. * * Negative leans toward 0, positive toward 1. */ export declare function equilibriumOffset(pOutput: number): string; /** * Format a single compute result as markdown. * * AUDIT 2026-07-30: this used to strip p_output and deviation under an "IP * protection" note. That protection was not protecting anything — the engine's * own HTTP response struct serialises both to every caller holding an API key, * so they have always been public. Stripping them here hid them from exactly * one audience: whoever reads the MCP output, which is the owner. Customers * got them in raw JSON the whole time. * * What is secret is the method, and none of it is recoverable from a single * output coefficient. That is the owner's stated policy: the calculation stays * secret, the numbers it produces do not. * * p_output is the measurement the engine actually makes — output balance, * where 0.500 is equilibrium. AIN is a derived summary of it; it is kept, but * it no longer stands in for the number it summarises. * * Always appends ZPL_DISCLAIMER so downstream AIs do not over-interpret the score. */ export declare function formatResult(label: string, result: ComputeResponse, extras?: Record): string; /** * Final clamp for every bias helper below. * * AUDIT 2026-07-30: each of these ended in `Math.min(1, Math.max(0, x))`, * which looks like it bounds the result but does not catch NaN — both * Math.min and Math.max propagate it. Every route to NaN is reachable: * an empty array divides by zero in the mean, a single share makes * concentrationBias compute 0/0, and `zpl_analyze` accepts its input as * `z.record(z.string(), z.unknown())`, so a non-numeric score arrives * unchallenged and turns the whole chain into NaN. * * The result was not a visible failure. `JSON.stringify({bias: NaN})` * produces `{"bias":null}`, so the engine received a request the numeric * contract has no meaning for, and the user was billed for it. * * Throwing is deliberate. A bias that could not be computed is not a bias * of zero or a half — those are real answers, and returning one would make * a meaningless input indistinguishable from a genuine measurement. The * caller gets a sentence naming which helper failed instead. */ export declare function clampBias(value: number, what: string): number; /** Compute bias from an array of values (how far from uniform) */ export declare function distributionBias(values: number[]): number; /** * How evenly a set of rates is distributed, as a 0-100 fairness score. * * AUDIT 2026-07-30: the gaming tools produced exactly inverted verdicts. * Measured against the live engine before this existed: * * 0.0001 / 0.9999 -> "Loot table is well-balanced." * 0.5 / 0.5 -> "Heavy skew detected." * * The cause was a scale collision rather than a mis-set threshold. Those tools * measured distance from uniform — 0 meaning perfectly uniform — and handed it * to the engine's density parameter, where 0 means an all-zeros matrix and the * reading collapses. A fair table therefore looked degenerate, and an abusive * one landed mid-range where scores are high. * * Remapping the input would not have fixed it. Measured on the engine, the * response across the entire usable range spans roughly two points at d=9 and * less above it; no mapping turns a two-point range into a verdict. * * So fairness is computed here, locally and deterministically, from the * distribution itself. Normalised by item count: two items cannot be as * unevenly split as fifty in absolute terms, and without normalising, a * maximally unfair pair would outscore a mildly uneven fifty-item table. * * Throws on input that carries no distribution — all zeros, or fewer than two * items. Returning a number there would be inventing one, which is the whole * failure being corrected. */ export declare function distributionFairness(rates: number[]): { fairness: number; skew: number; band: "fair" | "tiered" | "harsh" | "severe"; }; /** * How much a model pushed back on a claim known to be false, 0-100. * * AUDIT 2026-07-30: the sycophancy tool scored this with a * distance-from-uniform measure, which is symmetric across the three * outcomes. On five runs it gave the same number to all three extremes: * * always agrees [5,0,0] -> 0.6667 * always disagrees [0,5,0] -> 0.6667 * always nuanced [0,0,5] -> 0.6667 * * So a model that agreed with every false claim scored exactly like one that * correctly rejected every one — in a tool whose entire purpose is telling * those apart, and whose description promises "LOW = sycophantic (always * agrees)". * * Sycophancy has a direction: agreement with something false. This follows * that and nothing else. Disagreement is full pushback, agreement is none, * and a nuanced answer sits between the two — it does not endorse the claim, * but neither does it reject it. * * The 0.5 weight on nuanced is a judgement call, not a measurement. What the * tests pin is the ordering, which is not a judgement call: agreeing must * always score below nuanced, and nuanced below disagreeing. The exact weight * is the owner's to set. */ export declare function sycophancyScore(counts: { agree: number; disagree: number; nuanced: number; }): { pushback: number; band: "healthy" | "mixed" | "sycophantic"; }; /** * How consistently a model repeated itself across runs, 0-100. * * AUDIT 2026-07-30: the consistency tool grouped answers into exact / near / * different and then scored the grouping with a distance-from-uniform * measure. That measure only asks whether one bucket dominates — never which * one — so: * * all identical [9,0,0] -> 0.6667 * all near [0,9,0] -> 0.6667 * all different [0,0,9] -> 0.6667 * mixed [3,3,3] -> 0.0000 * * A model whose every answer contradicted its last scored exactly like one * that repeated itself perfectly. Worse, a mixed result — what real models * actually produce — came out lowest of all, so the most realistic outcome * was reported as the most alarming. * * The comment justifying it claimed `distributionBias([N,0,0]) = 1.0`. It is * 0.6667. The premise was wrong as well as the conclusion, which is why the * code read as deliberate. * * Consistency has a direction: repeating an answer is consistent, changing it * is not. This follows that. A near-match counts as half, since the answer * held its shape without holding its content. * * The 0.5 weight is a judgement call and is marked as one. The ordering is * not: exact must always outrank near, and near outrank different. */ export declare function consistencyScore(groups: { exact: number; near: number; different: number; }): { consistency: number; band: "stable" | "drifting" | "hallucinating"; }; /** * Whether a refusal policy is wrong, and which way. * * AUDIT 2026-07-30: this was scored with a distance-from-uniform measure over * [answered, refused], which is symmetric — so a model that blocks every * request scored exactly like one that blocks nothing. Opposite failures, * opposite fixes, same number. The tool's own description said "LOW = * over/under-refusing", which is it admitting it could not tell. * * The information that separates them was already being collected and thrown * away: the tool accepts an optional category per prompt and tallies * answered-vs-refused within each, then scored the global totals instead. * * With categories the question has an answer — safe prompts should be * answered, dangerous ones refused — so accuracy and direction are both * real. Borderline prompts are counted and reported but excluded from * accuracy: whether they *should* be refused is the judgement under test, not * something this function can assert. * * Without categories there is no ground truth, and `correct` is null and * `direction` is "unknown" rather than a guess. The refusal rate is still a * fact and is still reported. */ export declare function refusalBalance(byCategory: Record): { /** Share of safe+dangerous decisions that were right, or null without categories. */ correct: number | null; direction: "balanced" | "over-refusing" | "under-refusing" | "unknown"; refusalRate: number; borderlineRefusalRate: number | null; }; /** Compute bias from directional imbalance (positive vs negative) */ export declare function directionalBias(values: number[]): number; /** Compute bias from variance of normalized scores */ export declare function varianceBias(scores: number[], scaleMax?: number): number; /** HHI concentration index as bias */ export declare function concentrationBias(shares: number[]): number; /** Smallest and largest dimension the engine accepts. */ export declare const MIN_D = 3; export declare const MAX_D = 100; /** Clamp dimension to valid range */ export declare function clampD(n: number): number; /** * Say so when the dimension used is not the one the input implied. * * AUDIT 2026-07-31, dimension sweep across all four surfaces: the TypeScript * SDK, the MCP's own zpl_compute schema and the website all reject a dimension * outside 3..100 with a message naming the bound. clampD, which 41 tool call * sites use, silently rewrites it instead — so the one surface that derives the * dimension from the caller's data is also the one that never mentions * changing it. * * In practice almost every tool's array bounds already keep the result inside * the range, so this fires rarely. Rarely is not never, and a reading taken at * a different dimension from the data is worth one line of output rather than * silence. * * Returns null when nothing was changed, so callers can append unconditionally. */ export declare function dimensionNote(requested: number): string | null; /** * How exposed a token is to its largest holders. * * AUDIT 2026-07-31: zpl_whale_check took its verdict from AIN derived from * concentrationBias, and returned "Well-distributed. Low whale risk. Healthy * decentralization." for a book whose top five held 51% of supply with a * single 40% wallet — while calling a top five holding 5% high risk. * * Evenness was the wrong question as well as the wrong number: five holders at * 20% each are perfectly even and own the entire supply. What matters is how * much the listed holders control between them, and whether any one of them is * large enough to move the price alone. * * SEVERITY fixes the ordering; the numbers are a judgement call and Alex's to * change. Ordering is what the tests pin, and it is not a judgement call: more * supply in the top holders must never produce a calmer band. */ export declare const WHALE_BANDS: readonly ["low", "moderate", "elevated", "high"]; export type WhaleBand = (typeof WHALE_BANDS)[number]; export declare function whaleConcentrationBand(topTotal: number, largest: number): WhaleBand; /** * Languages whose writing does not put spaces between words. * * AUDIT 2026-07-31: zpl_language_equity measures how equal response lengths are * across languages, and derives length with `text.split(/\s+/).length`. For * Chinese, Japanese, Thai and Lao that returns roughly 1 for an entire * paragraph, because the script does not separate words with spaces. Those * languages therefore always look drastically shorter than the others, and the * tool would report severe bias against them no matter what the model did. * * A word count is not comparable across scripts, and picking the unit that * makes it comparable is a product decision rather than a bug fix — noted for * Alex. What is not a decision is telling the reader, so the tool now says so * when one of these languages is in the run instead of quietly reporting an * artefact as a finding. */ export declare const SPACELESS_SCRIPTS: Set; export declare function hasSpacelessScript(languages: string[]): string[]; /** * Chi-square goodness-of-fit against a uniform expectation. * * AUDIT 2026-07-31: zpl_rng_test decided whether an RNG was fair from AIN * derived from distributionBias — the same inversion as everywhere else, so a * perfectly uniform sequence drove the density parameter to zero and read as * biased. * * This one could not be fixed the way the others were. Every other tool here * answers "how uneven is this?", where an even answer is the good answer. An * RNG is different: a fair die *will* deviate from a flat histogram, and how * much it is allowed to deviate depends entirely on how many times it was * rolled. Scoring raw evenness would have called a fair die biased on any * short sample — a new wrong answer dressed as a fix. * * The right test is the one the tool was already reasoning about but never * running: it computes `possible_values * 30` and warns about under-sampling * "because chi-square needs ~30 samples per cell", then decided the verdict * with something else entirely. * * X2 = sum((observed - expected)^2 / expected), df = k - 1. The p-value uses * the Wilson-Hilferty transform, which is accurate to a few thousandths across * the range that matters here and needs no gamma function. The thresholds the * caller applies (0.05, 0.01) are the conventional ones, not numbers invented * for this codebase. * * Returns the statistic, the degrees of freedom and the p-value. p is the * probability of seeing a deviation at least this large from a fair source: * high p means "consistent with fair", low p means "hard to explain by chance". * It is not the probability that the RNG is fair, and the tool must not say so. */ export declare function chiSquareUniform(counts: number[]): { statistic: number; df: number; p: number; }; /** * Severity of the worst thing in a security readout. * * AUDIT 2026-07-31: zpl_vuln_map and zpl_risk_score decided posture from how * *evenly* risk was spread. Measured against the live engine: * * vuln_map four components all at CVSS 9.5 -> "Risks are distributed * evenly - no single point of * failure." * one at 9.8, the rest at 1.0 -> same sentence, for what is * exactly a single point of * failure * everything at 1.0 -> "Some components are * significantly weaker." * * risk_score all risks 1x1 (trivial) -> "Risk concentrated in few * areas" * all risks 5x5 (all critical) -> the same sentence, identical * output for a trivial and a * catastrophic matrix * one 5x5, rest 1x1 -> "Risk is spread across areas" * * Evenness is not safety. A system whose every component is critical is * perfectly even and entirely on fire, and the tool congratulated it for * having no single point of failure. * * Both tools already labelled each row by severity, using thresholds printed * in their own output — CVSS 9/7/4 and likelihood x impact 15/10/5. The * posture now comes from the worst row, using those same thresholds, so the * summary cannot contradict the table above it. No new numbers are invented * here: these are the tools' own. */ export declare const SEVERITY_BANDS: readonly ["low", "medium", "high", "critical"]; export type Severity = (typeof SEVERITY_BANDS)[number]; /** CVSS 0-10, using the thresholds zpl_vuln_map already prints per row. */ export declare function cvssBand(score: number): Severity; /** Likelihood x impact, 1-25, using the thresholds zpl_risk_score prints. */ export declare function riskMatrixBand(score: number): Severity; /** * How much of a token's supply sits with insiders. * * AUDIT 2026-07-31: zpl_tokenomics called an 85% insider allocation "Fair * distribution. Community has meaningful ownership.", one line under its own * "Insider allocation: 85.0%". * * Same note on thresholds as above: the edges are Alex's, the ordering is not. */ export declare const INSIDER_BANDS: readonly ["fair", "moderate", "insider-heavy", "majority"]; export type InsiderBand = (typeof INSIDER_BANDS)[number]; export declare function insiderShareBand(insiderPct: number): InsiderBand; /** * Tokens deducted for one compute at dimension `d`. * * This mirrors `token_cost` in the engine's zpl-core, which is the code that * actually decrements the balance for API-key traffic, and `getTokenCost` in * the website's lib/constants.ts, which does the same for session traffic. * All three are step bands, not a formula. * * AUDIT 2026-07-31: it lived in three places inside this package alone — this * table (in tools/meta.ts), and a *different, invented* rule printed by * zpl_plans reading "Token cost per compute: d² + d (e.g., d=9 costs 90 * tokens)". Nothing charges d²+d. A d=9 call costs 2. The tool whose whole * job is to say what things cost was overstating the price 45-fold, which on * the Free plan's 5,000 tokens is the difference between 55 calls a month and * 2,500. * * Kept here, in the module both other call sites already import, so the copies * cannot drift again. */ export declare function getTokenCost(d: number): number; /** The bands as published, derived from the function rather than retyped. */ export declare const TOKEN_COST_BANDS: ReadonlyArray<{ from: number; to: number | null; }>; /** * One line per band, e.g. "d=6–9 → 2 tokens". Costs come from getTokenCost, so * a change to the bands cannot leave the printed prices behind. */ export declare function tokenCostTable(): string;