/** * Structural-health SCORING core — extracted as a NODE-FREE leaf. * * The scoring functions (`scoreReport`/`reportDeductions`/`computeIntegrityScore`/ * `gradeFor` + the penalty weights) are pure: they take a {@link ScanReport} and * return numbers/labels, touching no filesystem. They lived in `leaderboard.ts`, * but that module ALSO holds the node-only `rankPlugins`/`pluginLabel` (which call * `scanPlugin` + `node:fs`), so importing ANY symbol from it eagerly pulled the * whole `scan.ts` → `@ast-grep/napi` (a native binary) + `node:fs` chain into the * graph — tainting `buildAuditReport` and blocking the in-browser audit engine * (`scanFiles` is node-free, but the report BUILDER re-tainted it via * audit-score/optimize/audit-verdict → leaderboard → scan). * * Splitting the pure core out here — exactly like `core/ncd.ts` was extracted from * `proofs.ts` to keep `node:crypto` out of the browser graph — lets the report * builder import scoring WITHOUT the node-only leaderboard code. `leaderboard.ts` * re-exports these for its own consumers (rankPlugins/formatters), so nothing that * imported them from `./leaderboard.js` breaks. The `ScanReport` import is * TYPE-ONLY (elided at build), so no runtime `scan.ts` dependency enters this leaf. * See src/scan-files.ts (BROWSER_ROOT) and research/report-view-and-browser-demo.md. */ import type { ScanReport } from "./scan.js"; export interface PluginScore { readonly dir: string; readonly name: string; /** 0–100 structural-health score (100 = no structural issues found). */ readonly score: number; readonly grade: "A" | "B" | "C" | "D" | "F"; /** Human-readable deductions, worst first. */ readonly issues: readonly string[]; readonly report: ScanReport; } export declare const W_MISSING_HOOK = 15; export declare const W_NO_DESCRIPTION = 10; export declare const W_DANGLING_REF = 8; export declare const W_OVERLAP = 8; export declare const W_NO_CONTRACT = 5; export declare const W_TRIFECTA = 10; /** * CAP on the TOTAL lethal-trifecta penalty, charged against the SHARE of the * model-invocable surface that holds the trifecta (see {@link trifectaExposure}). * A pure per-unit count saturates the clamp on any sizeable harness — which is * how the old model reported a HARDENED harness as strictly worse than its * unhardened self. Same value as the `feature-dev` anchor (3-of-3 units → −30), * so a fully-exposed harness still lands on a C-band ding, never an automatic F. */ export declare const W_TRIFECTA_MAX = 30; /** * The one canonical, jargon-free finding string for a HARD lethal-trifecta unit — * shared by the Safety category card (audit-score) AND the verdict sentence / overall * deduction (reportDeductions here), so the plain-language copy can't drift between * surfaces (one-detector-no-drift). It NAMES the three legs and the consequence in * plain words instead of the old "holding all three lethal-trifecta legs * (prompt-injection exfil path)" shorthand a first-time reader can't parse. The * `(s)` placeholder is resolved by the caller's pluralizer against the count. */ export declare const TRIFECTA_LABEL = "unit(s) can read data, reach the web, and run commands \u2014 the \"lethal trifecta\", so a prompt injection could exfiltrate secrets"; /** Map a 0–100 structural-health score to its letter grade (A ≥90 … F <60). */ export declare function gradeFor(score: number): PluginScore["grade"]; /** * The health score a report may NOT exceed while it carries a CONFIDENT BREAKAGE * — one point below the healthy band, so a definitively-broken harness can never * render as `●` / grade `A`. * * WHY a cap and not a heavier weight: the score is a SUM of deductions, so one * real breakage among many clean surfaces is diluted by its siblings. MEASURED * 2026-08-28 on a fixture (10 distinct-description skills + one agent naming a * never-available tool): Structure scored **92 with a `●` green dot** while * carrying a dead tool contract, and the grade was driven instead by the * lethal-trifecta pattern that {@link W_TRIFECTA} deliberately calls a ding and * not a fail. A weight big enough to fix that would cry wolf on a large clean * plugin; a cap fixes the reading without touching the arithmetic. * * The same defect, with higher stakes, is documented in an external 517-skill * catalog run of a different scanner: its weighted aggregate returned MEDIUM for * a skill carrying THREE HIGH findings, so gating on the aggregate would have * admitted exactly the skill the gate existed to stop. That team's conclusion — * judge by the worst finding, not the aggregate — is what this encodes. */ export declare const CONFIDENT_BREAKAGE_CAP = 89; /** * The findings that CAP the score — each one means a surface is definitively * dead, not merely risky: it will silently not run, not resolve, or not register. * * ENUMERATED on purpose, never derived from "severity". Two properties decide * membership, and both must hold: the finding is DECIDABLE from the artifact plus * the world (the `structural-closed` / `external-decidable` buckets of the * lint-rule-calibration rule), and its consequence is BREAKAGE rather than * exposure. So the heuristic rings stay out by construction — a lethal-trifecta * unit and a description overlap are real signals but are a capability PATTERN * and a calibrated PROXY, and capping on either is how a gate earns the reputation * that gets it switched off. * * Adding a row here is a deliberate act: it makes a finding grade-capping for * every consumer, so it belongs only to a check whose false-positive rate is * already known to be ~0. */ export declare function confidentBreakages(r: ScanReport): { readonly n: number; readonly label: string; }[]; /** * Cap a health score at {@link CONFIDENT_BREAKAGE_CAP} when the report carries any * {@link confidentBreakages} finding. Applied to the OVERALL and to the ring that * owns the finding, so the headline and the breakdown cannot disagree. */ export declare function applyBreakageCap(score: number, r: ScanReport): number; /** One deduction: a count, its per-item weight, and the label if non-zero. */ export interface Deduction { readonly n: number; readonly weight: number; readonly label: string; /** * TOTAL penalty for this deduction, overriding the default `n × weight`. Only * used where the cost is not linear in the count — today that's the trifecta * exposure penalty, which is capped against the share of the surface affected * (see {@link trifectaExposure}). `n` still drives the human-readable label. */ readonly points?: number; } /** The graded lethal-trifecta exposure of a report. */ export interface TrifectaExposure { /** Units holding all three legs — EXPLICIT (`hard`) and INHERITED alike. */ readonly exposed: number; /** Model-invocable units that could hold them (subagents + non-user-invoked skills). */ readonly assessable: number; /** The graded penalty, `min(W_TRIFECTA × exposed, W_TRIFECTA_MAX × exposed/assessable)`. */ readonly penalty: number; } /** * The lethal-trifecta exposure a report incurs — the ONE number the Safety ring * and the overall grade both read. * * TWO properties this model must have, both learned the hard way from dogfooding * (2026-08-03, a 35-skill repo): * * 1. **MONOTONE in risk.** A unit that INHERITS all tools holds the full trifecta * *implicitly* and is strictly WORSE than one whose explicit `allowed-tools` * happens to name all three legs — it holds every other capability too. The old * model graded the explicit case at −10 and left the inherited case UNGRADED, so * declaring a tool contract (a genuine risk reduction) could only ever LOWER the * score: a repo measured 70 with 31 of 35 units inheriting everything, then 0 * after `allowed-tools` was added to all 35 and the units holding the full * trifecta fell 35/35 → 17/35. The tool called the safer configuration strictly * worse and said nothing about the unsafe one. So BOTH severities count here. * * 2. **Non-saturating.** With a flat per-unit weight, 35 exposed units and 17 * exposed units both blow past the clamp and score 0 — halving your exposure * shows up as no change at all. So the total is ALSO capped against the SHARE * of the model-invocable surface that is exposed: a 35-unit harness with 3 * exposed units is genuinely safer than a 3-unit harness where all 3 are. * * The `min` of the two keeps every existing calibration anchor for small harnesses * (1 exposed unit of 1 → −10; the `feature-dev` 3-of-3 shape → −30 → C) while * giving a large harness real resolution (34/35 → −29, 17/35 → −15). */ export declare function trifectaExposure(r: ScanReport): TrifectaExposure; /** * The COMPLETE graded-penalty list a report incurs — the single source of truth * BOTH the leaderboard's single health number and the audit's category rings * read, so the overall can never drift between the two surfaces. Each entry is a * graded penalty; untested surfaces are deliberately ABSENT (they're advisory, * surfaced separately, never scored). */ export declare function reportDeductions(r: ScanReport): Deduction[]; /** * True when a report has no loadable plugin surface at all (the empty machine). * * 🔴 THE ONE PLACE the surface set is enumerated, and every scorer must ask it * rather than write the list again. `scoreReport` used to keep its own copy and * that copy had dropped `inlineHooks` — so a plugin whose entire contribution is * hooks declared inline in `plugin.json` scored 0 / F with "no loadable plugin * surface" while the same report printed `inlineHooks: 2` (#199). Measured on a * two-inline-hook fixture: this predicate said `false` and `scoreReport` said * `0`, against `auditScore`'s `100` — a 100-point disagreement between two * numbers documented to be equal. * * A hooks-only plugin is a legitimate shape, the same way a command-only or * MCP-only one is: a repo can ship gates and nothing else. Zero has to mean "no * surface of any kind", because it reads as "this plugin is broken" — a * different statement from "this plugin has no skills or subagents". * * An output style counts too: a repo can ship only a style, and "nothing to * audit" for it would hide the untested-output-style finding. * * Hooks count in BOTH forms: `hooks[]` is the script-backed ones, `inlineHooks` * the shell one-liners that carry no script file to path-check. The second is * still a gate that runs. */ export declare function isEmptyMachine(r: ScanReport): boolean; /** * THE shared integrity score — `100 − Σ(all graded penalties)`, clamped to * [0,100]. Both the leaderboard's single health number AND the audit's headline * overall read this, so the two can never disagree (the summed model is the * honest one — averaging rings would dilute a real problem). Returns the score * plus the per-item deductions so callers render their own issue/finding lists. */ export declare function computeIntegrityScore(deductions: readonly Deduction[]): { score: number; penalty: number; }; /** Deterministic structural-health score for one scanned plugin. */ export declare function scoreReport(r: ScanReport): { score: number; issues: string[]; }; //# sourceMappingURL=score-core.d.ts.map