/** * The DELIBERATE grammar diagnostic — the one call that asks parseman "is anything * wrong with this grammar?" and answers in a form a machine can gate on. * * Why this exists as its own entry point, and why nothing in `compile()` prints any * more: a diagnostic that rides along with the thing that produces the artifact is a * diagnostic nobody chose to run. Importing one example grammar used to print ~51 * lines of gating advice through `console.warn` before a single byte was parsed — * advice that was correct, detailed, and read by no one, because it arrived unasked * in the middle of an unrelated build log. The same principle is already settled one * layer down for codegen ("anything 'diagnostic' doesn't end up in codegen"); this is * that principle applied at build time. Compiling produces an artifact and says * nothing. Asking for a diagnosis produces a diagnosis. * * Design rules this surface holds itself to: * * 1. MACHINE-READABLE FIRST. `diagnoseGrammar()` returns a plain, JSON-serializable * object with a versioned `schema` tag. The human rendering is a separate, * optional function over that object — never the primary product. A CI job gates * on `ok`; a person reads `formatGrammarDiagnosis()`. * 2. DETERMINISTIC. Findings are sorted by (severity, code, id). Two runs over the * same grammar produce byte-identical JSON, so a diagnosis can be committed as a * snapshot and diffed. * 3. FAILS CLOSED. An analysis that could not run is NOT a pass. `unanalysable` stays * authoritative (see `GatingReport.unanalysable`), it is a BLOCKING finding, and a * diagnosis whose analysis THREW is reported as a blocking finding rather than as * an empty, clean-looking report. * 4. ONE ENTRY POINT. It accepts a combinator, a rule-name→combinator map, a `rules()` * map, or a `compose()` result, and figures out which it got. Choosing between * `analyzeGating` / `analyzeGatingRules` / `analyzeGrammarGating` is exactly the * kind of decision that makes people not bother. */ import type { Combinator } from '../types.ts'; import { type AnalyzeGatingOptions, type GatingReport } from './gating.ts'; import { type AnalysableGrammar } from './grammar.ts'; import { type Degradation } from '../compiler/degradation.ts'; /** Anything `diagnoseGrammar()` knows how to read. */ export type DiagnosableGrammar = Combinator | AnalysableGrammar | ReadonlyArray]>; /** * `blocking` fails `ok` (and therefore a CI gate). `advisory` is reported but does not * fail: the author either already acknowledged it (`accepted`) or has nothing to act on. */ export type DiagnosisSeverity = 'blocking' | 'advisory'; /** Stable, greppable finding class. New codes may be added; existing ones are not renamed. */ export type DiagnosisCode = /** A hot choice with no first-char dispatch — every position enters doomed arms. */ 'ungated-choice' /** An API-misuse pattern in a choice's arms (double-not, leading-not, keyword-regex). */ | 'anti-pattern' /** Part of the grammar could not be examined. A clean report over it is NOT a pass. */ | 'unanalysable' /** The compiler took a correct-but-slower path. Mirrors the `[parseman] degraded` channel. */ | 'degraded' /** An `accept` entry that matched no ungated choice — a stale snapshot line to prune. */ | 'stale-accept'; export type DiagnosisFinding = { /** Stable identity: the choice id, `rule#arm`, the rule name, or the degradation code. */ id: string; code: DiagnosisCode; severity: DiagnosisSeverity; /** Rule / node type the finding lands on. */ rule: string; /** One-line statement of what is wrong. */ message: string; /** Arm-level evidence and concrete fixes, one entry per contributing cause. */ details: string[]; /** The `accept` snapshot key that would silence this finding, when one exists. */ acceptKey?: string; }; export type GrammarDiagnosis = { /** Versioned so a committed snapshot can be migrated rather than silently reinterpreted. */ schema: 'parseman.diagnosis/1'; /** * True only when there is no blocking finding. A CI gate is * `process.exit(diagnoseGrammar(g).ok ? 0 : 1)` — nothing else to remember. */ ok: boolean; summary: { totalChoices: number; gated: number; recoverable: number; ungated: number; accepted: number; deferred: number; antiPatterns: number; unanalysable: number; degraded: number; staleAccepts: number; }; /** Sorted (severity, code, id). Deterministic across runs. */ findings: DiagnosisFinding[]; /** * Every blocking-choice id, sorted — paste straight into `{ accept: [...] }` to * acknowledge the current state as intentional. */ acceptSnapshot: string[]; /** The full underlying gating report, for callers that want the raw per-choice detail. */ gating: GatingReport; /** * Degradations recorded WHILE this analysis ran (e.g. an opaque composed artifact). * NOT the compile-time set — compiling is a separate act; see `PARSEMAN_DEGRADATION`. * * Empty when `PARSEMAN_DEGRADATION=off`, because `recordDegradation` short-circuits on * the level. That is not a blind spot: everything this analysis can record is ALSO * present in `gating.unanalysable`, which no env var can switch off. */ degradations: Degradation[]; }; export type DiagnoseOptions = Pick; /** * Diagnose a grammar. Never throws: an analysis that cannot run is reported as a * blocking `unanalysable` finding, because a thrown diagnostic and a clean grammar * must not look the same to a caller that wrapped this in a try/catch. */ export declare function diagnoseGrammar(grammar: DiagnosableGrammar, opts?: DiagnoseOptions): GrammarDiagnosis; /** * The analysis examined NOTHING: no choice was walked, and rules were skipped. * * This is NOT "problems were found" and must never be presented as a finding count. A * diagnosis over a fully opaque grammar has one blocking finding per skipped rule, which * `findings.length` then reports as "176 problems, 176 failing the check" — a sentence * that reads as 176 discovered defects and actually means the tool inspected zero * choices. `ok` is correctly false either way, so `ok` alone cannot tell the two apart; * a caller that needs to distinguish "measured, and it is bad" from "could not measure" * asks here. The CLI maps this to exit 2 (COULD NOT ANALYSE), not exit 1. * * `unanalysable > 0` is required, so a genuinely choice-free grammar (nothing to walk, * nothing skipped) stays an ordinary clean pass rather than a measurement failure. */ export declare function examinedNothing(d: GrammarDiagnosis): boolean; /** Render a diagnosis for a human. The structured object stays the product of record. */ export declare function formatGrammarDiagnosis(d: GrammarDiagnosis): string[]; //# sourceMappingURL=diagnose.d.ts.map