export type Severity = "error" | "warning" | "info"; export type AxisName = "tokens" | "a11y" | "components" | "stories" | "ai-surface" | "ai-governance"; export type BuiltInRuleId = "tokens/no-hardcoded-color" | "tokens/no-hardcoded-spacing" | "tokens/no-hardcoded-typography" | "tokens/no-hardcoded-radii" | "tokens/no-hardcoded-shadow" | "tokens/no-hardcoded-motion-duration" | "tokens/no-hardcoded-motion-easing" | "tokens/no-hardcoded-breakpoints" | "tokens/no-hardcoded-z-index" | "tokens/no-hardcoded-opacity" | "tokens/no-hardcoded-border-width" | "tokens/dtcg-conformance" | "tokens/description-coverage" | "tokens/deprecated-token-usage" | "components/no-native-shadows" | "components/contracts-strictness" | "components/standardized-variant-props" | "naming/component-pascalcase" | "naming/hook-prefix" | "naming/prop-camelcase" | "a11y/essentials" | "a11y/contrast-tokens" | "stories/coverage" | "stories/variant-coverage" | "stories/props-documented" | "stories/usage-examples" | "ai-surface/agents-md-quality" | "ai-surface/component-manifest-json" | "ai-surface/ds-index-exported" | "ai-surface/mcp-config-present" | "ai-surface/llms-txt-structure" | "ai-surface/shadcn-registry-valid" | "ai-surface/agent-instruction-files" | "versioning/changelog-present" | "versioning/semver-versioning" | "versioning/migration-guide-present" | "ai-governance/ai-content-live-region" | "ai-governance/ai-loading-error-states" | "ai-governance/ai-marker-component-present" | "ai-governance/feedback-control-present"; export type RuleId = BuiltInRuleId | string; export interface SourceLocation { file: string; line: number; column: number; } /** Three-way verdict from the LLM precision filter (Phase D). */ export type LlmVerdict = "violation" | "fp" | "uncertain"; /** * LLM precision-filter judgement attached to a kept finding. `confidence` is the * model's self-reported certainty in `verdict`, in [0, 1]. Consumed by the * conformal scoring gate (Phase D): only findings whose judgement clears the * calibrated threshold contribute to the score; the rest stay reported-only. */ export interface LlmJudgement { verdict: LlmVerdict; confidence: number; } /** A root-cause fix shared by N findings: one drifted value → one token. */ export interface FixGroup { /** Grouping key, e.g. "tokens/no-hardcoded-color::#3b82f6". */ key: string; /** The drifted literal, e.g. "#3b82f6". */ from: string; /** Resolved target token path; omitted when zero or many candidates. */ to?: string; } export interface Finding { ruleId: RuleId; axis: AxisName; severity: Severity; location: SourceLocation; message: string; suggestion?: string; context?: string; confidence?: Confidence; llmJudgement?: LlmJudgement; fixGroup?: FixGroup; } export interface PerRuleOpportunity { ruleId: string; axis: AxisName; opportunities: number; } export interface RuleContext { repoRoot: string; tokens: TokenMap | null; componentsModule: string | null; componentInventory: ComponentInventoryEntry[]; storyIndex: StoryIndex | null; excludePaths: string[]; /** * True when the repo being audited IS the design system (workspace * detection found a DS-export package in the same monorepo). Some rules * (no-native-shadows, stories/coverage) skip in this mode because their * semantics target consumer-of-DS audits, not DS-self audits. Full * DS-self rule semantics planned for v0.2. */ dsSelfMode?: boolean; /** * The reified Design System Graph for this audit. Added in P1 (additive). The * flat fields above (`tokens`/`componentInventory`/`storyIndex`) remain as * graph-derived aliases; rules migrate to `graph` in P2. */ graph?: import("./graph/types.js").DesignSystemGraph; /** * Four-class value resolver, built once per audit from `graph`. Optional for * the same reason `graph` is: MCP and single-file rule paths construct their * own contexts and keep the legacy exact-match behaviour. */ resolver?: import("./graph/resolve/types.js").Resolver; /** * Computed token readings from the opt-in render layer. Present only when * `lyse audit --render` ran and the browser was available; absent otherwise. * Rules that need rendered data (tokens/rendered-token-fidelity) return N/A * (opportunities 0) when this is undefined. */ rendered?: import("./render/types.js").ComputedTokenReading[]; /** * DTCG canonical token map (path → resolved value) built from the design * system's tokens.json. Present only when `lyse audit --render` loaded a * DTCG source; absent otherwise. Consumed by tokens/rendered-token-fidelity. * Populated by R7′ pipeline wiring. */ canonicalTokens?: Map; /** * Runtime axe-core violations collected per story under `lyse audit --render` * with a resolved Storybook. Present (possibly empty) only when the axe * render sub-stage ran; absent otherwise. Consumed by a11y/runtime-axe. */ axeViolations?: import("./render/axe-runner.js").AxeViolation[]; /** Number of stories successfully probed by axe — the rule's denominator. */ axeStoriesProbed?: number; } export interface TokenMap { colors: Map; spacing: Map; typography: Map; radii: Map; shadows: Map; motion: Map; breakpoints: Map; zIndex: Map; opacity: Map; borderWidth: Map; source: "tailwind-v3" | "tailwind-v4" | "dtcg" | "css-vars" | "style-dictionary" | "tokens-studio" | "figma-variables" | "mixed"; } /** A single prop extracted from a component's TypeScript type annotation. */ export interface ComponentPropEntry { /** Prop identifier, e.g. "variant" */ name: string; /** * Raw TypeScript type text, e.g. '"primary" | "secondary" | "ghost"'. * For props imported from another file, this will be the reference name * (e.g. "ButtonProps") without cross-file resolution (v0.2 enhancement). */ typeText?: string; /** True when the prop is optional (`?:`). */ isOptional?: boolean; /** Default value extracted from destructuring, e.g. "primary" for `{ variant = "primary" }`. */ defaultValue?: string; /** * True when `typeText` is a string-literal union (all members are string literals), * e.g. `'"primary" | "secondary"`. */ isVariantUnion?: boolean; /** Extracted string-literal values when `isVariantUnion` is true. */ variants?: string[]; } export interface ComponentInventoryEntry { name: string; module: string; usageCount: number; /** * Props extracted from the component's TypeScript prop type definition. * Populated by buildComponentInventory when the loader can parse the source files. * Absent when the component source is not available for analysis. */ props?: ComponentPropEntry[]; } /** * A single named export from a CSF v3 story file. * Extracted from `export const Primary = { args: { variant: "primary" } }`. */ export interface StoryExport { /** Export name, e.g. "Primary" */ name: string; /** * Simple literal args extracted from the story's `args` object. * Only string/number/boolean literals are extracted; complex expressions are skipped. * Example: { variant: "primary", size: "md", disabled: false } */ args?: Record; } /** * Per-file story entry with CSF v3 export data. * Extends the original minimal `{ id, importPath }` shape with parsed story exports. */ export interface StoryEntry { id: string; importPath: string; /** * Component name from the default export's `component` field (best-effort). * e.g. `export default { component: Button }` → componentName = "Button" * Absent when the default export is complex or uses a variable reference * not visible as a direct identifier. */ componentName?: string; /** * Named story exports from the file. * e.g. `export const Primary = { args: { variant: "primary" } }` → * stories = [{ name: "Primary", args: { variant: "primary" } }] * Absent when no exports were extracted (complex factory patterns, parse errors). */ stories?: StoryExport[]; /** * True when the story's default-export meta declares an `argTypes` object * (the canonical CSF prop-documentation signal). Presence only — the value * is not inspected. Set by `loadStories` when the story parses (`false` when * the meta has no `argTypes`); absent only when the file failed to parse. */ hasArgTypes?: boolean; /** * True when the story's default-export meta declares an `args` object (the * CSF3 autodocs prop-documentation signal — meta-level args document the * component's props for every story). Presence only. Set by `loadStories`; * absent only when the file failed to parse. */ hasArgs?: boolean; } export interface StoryIndex { byTitle: Map; } export interface Rule { id: RuleId; axis: AxisName; evaluate(ctx: RuleContext, parsedFiles: ParsedFiles): Promise; classifyConfidence?: (finding: Finding, ctx: ClassifyContext) => Confidence; applyCodemod?: (finding: Finding, ctx: CodemodContext) => CodemodResult; /** * True when the rule produces meaningful results from a single parsed file * with no repo-wide index (component inventory, story index). The MCP * `audit_file` tool runs only these; repo-wide rules need full `lyse audit`. */ singleFileCapable?: boolean; } export interface ParsedFiles { ts: ParsedTsFile[]; css: ParsedCssFile[]; cssInJs: ExtractedCssInJsBlock[]; } export interface ParsedTsFile { path: string; ast: unknown; source: string; imports: ImportRecord[]; } export interface ImportRecord { module: string; named: string[]; default: string | null; line: number; } export interface ParsedCssFile { path: string; source: string; /** * Set to `true` for `.sass` (indented syntax) files which are still skipped * in v0.1. `.scss` is fully parsed via `parsers/scss-transform.ts` and the * transformed CSS-equivalent source is returned in `source`. * Also set to `true` if the SCSS transform throws unexpectedly so the * pipeline degrades gracefully instead of crashing the audit. */ skipped?: true; } export interface ExtractedCssInJsBlock { path: string; line: number; content: string; } export interface ParseError { file: string; reason: string; } export interface RuleEvalResult { findings: Finding[]; opportunities: number; /** * Files the rule attempted to analyze but had to skip because its parser * could not understand them. Surfaced as `meta.parseErrors` in the audit * output so users can see what was NOT analyzed (transparency over silent * 100/100 scores). v0.1 limitation — see issue #155. */ parseErrors?: ParseError[]; } export interface AxisScore { axis: AxisName; score: number | "N/A"; findings: number; opportunities: number; } export interface Layer4Meta { /** True when the LLM response was served from cache. */ cacheHit?: boolean; /** USD spent on LLM calls (0 on cache hit). */ usdSpent?: number; /** Model identifier as reported by the connector. */ modelUsed?: string; /** "higher" for frontier models, "lower" for local/free-tier. */ llmQuality?: "higher" | "lower"; /** Number of LLM-proposed findings dropped by the validator. */ droppedHallucinations?: number; /** Set to true when Layer 4 was intentionally skipped via --static-only. */ staticOnly?: boolean; /** Non-fatal error that occurred during Layer 4 (audit continues with Layers 1+2 score). */ error?: { kind: string; message: string; }; /** True when the LLM precision filter (#115) ran (a real verdict was obtained). */ filterRan?: boolean; /** Number of color/spacing findings dropped by the LLM precision filter. */ filteredCount?: number; } /** * One ranked, actionable fix-group entry in `meta.projection.top` — how much * Health Score is on the table if this group is fixed. Produced by * `report/fix-groups.ts` (`computeProjection`); consumed by the terminal * reporter and the handoff payload. */ export interface ProjectionEntry { /** `fixGroup.key` when present, else `ruleId` — same key as `FindingGroup.key`. */ key: string; ruleId: string; from?: string; to?: string; /** Findings in this group. */ count: number; /** Distinct files touched by this group. */ files: number; /** Health Score points gained if this group's findings were fixed (0-floored). */ gain: number; /** True when `files` meets or exceeds the migration-scale threshold. */ migrationScale: boolean; } /** * Deterministic score-projection summary attached to `meta.projection`. * Presentation-only math on top of the existing scorer — never changes the * Health Score itself. */ export interface ProjectionMeta { /** Up to `cap` (default 3) highest-gain fix groups, sorted gain desc, count desc, key asc. */ top: ProjectionEntry[]; /** Score gain from fixing every group in `top` at once (not the sum of individual gains). */ totalGainTop3: number; } export interface CoverageMeta { /** Count of source files actually walked by the scanner (NOT a generic find of the repo). */ scannedFiles: number; /** Audit pipeline duration in milliseconds (excludes Node boot and CLI argument parsing). */ durationMs: number; /** Resolved path to the user's .lyse.yaml, or `null` when no config file was discovered. */ configPath: string | null; /** * Files a rule attempted to analyze but could not parse. Deterministic (same * file → same reason), sorted by `file` ascending. Omitted when no rule reported * a parse failure. Tracks #155. */ parseErrors?: ParseError[]; } export type Grade = "A" | "B" | "C" | "Fail"; export interface GradeResult { grade: Grade | "N/A"; autoFailed: boolean; /** Human-readable auto-fail reasons; empty unless `autoFailed`. */ reasons: string[]; } export interface AuditResult { schemaVersion: 2 | 3; rulesVersion: string; toolVersion: string; /** * The pinned scoring-formula version stamped on every emitted audit artifact. * Bumping is a semver-major event — same input may produce a different score. * (ADR 0017 + spec §3 falsifiable claim 1.) */ scoringVersion: string; repoRoot: string; timestamp: string; stack: string[]; finalScore: number | "N/A"; tier: string; /** A/B/C/Fail letter grade + auto-fail conditions (Track #87). */ grade?: GradeResult; axes: AxisScore[]; findings: Finding[]; /** * Findings dropped by inline `lyse-disable` directives. Excluded from the * score (never counted), but surfaced in SARIF as in-source suppressions so * code-scanning consumers keep dedup/trend data. Omitted when none. */ suppressedFindings?: Finding[]; /** ADR-0015: Layer 4 LLM augmentation metadata + #156 audit-perimeter signals. */ meta?: { layer4?: Layer4Meta; /** Phase 1 of #156 — audit-perimeter signals so the score has a visible denominator. Includes `parseErrors` (#155) as a deterministic subfield. */ coverage?: CoverageMeta; /** Render-layer metadata; present only when `lyse audit --render` ran. */ render?: import("./render/types.js").RenderMeta; /** Deterministic score projection for the largest fix groups (Sprint 1 actionable findings). */ projection?: ProjectionMeta; /** P1 extraction report — per-extractor status/evidence/remediation + token conflicts. Deterministic. */ extraction?: import("./graph/types.js").ExtractionReport; /** P0: true when this was a self-DS audit (the repo IS the design system). Additive. */ dsSelfMode?: boolean; /** * Count of distinct (axis, value) pairs the value resolver could not * classify (`Resolver.abstentions()`). The resolver is built once per audit * and read after `runRules`, since the count only exists once resolution * has happened; 0 until a rule calls `ctx.resolver.resolve()`. */ abstentions?: number; }; } export interface RuleMeta { id: RuleId; axis: AxisName; defaultSeverity: Severity; shortDescription: string; fullDescription: string; helpUri: string; rationale: string; examples: { good: string; bad: string; }[]; allowlist: string[]; } export interface RulesManifest { schemaVersion: "1.0.0"; rulesVersion: string; rules: RuleMeta[]; } export type Confidence = "high" | "medium" | "low"; export declare function isValidConfidence(v: unknown): v is Confidence; export interface RuleConfigEntry { severity?: "error" | "warning" | "info" | "off"; tolerance?: number; disable?: string[]; } export interface LyseConfig { designSystem?: { componentsModule?: string; elements?: Record; excludePaths?: string[]; }; /** * Per-rule configuration keyed by rule id. `"off"` (or `{ severity: "off" }`) * disables the rule. Rule ids are validated against the registry at audit * start — an unknown id is a hard error (CLI) / warning (MCP), not a silent * no-op. `severity` overrides (to a real level) and per-rule options * (`tolerance`, `disable`) are validated but not yet applied — tracked * separately. */ rules?: Record; /** Scoring tuning. `aiGovernanceGraceWindow` ramps the ai-governance axis in over N AI markers (#89 / ADR-0018; default 5). */ scoring?: { aiGovernanceGraceWindow?: number; /** Scoring formula to use: "v2" (legacy, default) or "v3" (opt-in). See `scoreAudit` in scorer.ts. */ model?: "v2" | "v3"; /** v3 only: minimum per-axis opportunity count before an axis scores (else "N/A"). Default `MIN_SAMPLE_SIZE` (30). */ minSampleSize?: number; }; i18n?: { locales?: string[]; vocabulary?: { aiNouns?: string[]; disclaimerPhrases?: string[]; controlLabels?: string[]; gatePhrases?: string[]; loadingPhrases?: string[]; }; }; llm?: { provider?: 'anthropic' | 'openai' | 'openai-compatible' | 'mcp' | 'none' | 'auto' | 'agent-cli'; model?: string; endpoint?: string; /** ADR-0015: ConnectorResolver fields (Task 2) */ connector?: 'auto' | 'mcp-host' | 'openrouter' | 'direct-api-key' | 'ollama' | 'agent-cli'; costCapUsd?: number; cacheMaxAgeDays?: number; staticOnly?: boolean; }; /** * Advisory-only tuning — does not affect the Health Score. `migrationScaleFileCount` * is the distinct-file threshold above which a fix group's projection entry is * flagged `migrationScale` (large blast radius — sample before you sweep). * Default 40 (`MIGRATION_SCALE_FILE_COUNT_DEFAULT` in `report/fix-groups.ts`). */ advisory?: { migrationScaleFileCount?: number; }; /** * `lyse handoff` safety mode. `review: true` is the same as passing * `--review`: the agent launches under its own default permission model * (prompts per-action) instead of bypassed permissions, and skips the * pre-spawn confirmation. Overridden by `--review` / `LYSE_HANDOFF_REVIEW=1`. */ handoff?: { review?: boolean; }; } export interface ClassifyContext { tokens: TokenMap; components: Set; config: LyseConfig; /** * Absolute path to the repository root being audited. Optional because * historical callers built `ClassifyContext` without this field; rules * that need on-demand semantic resolution (e.g. via ts-morph) should * degrade gracefully when it is absent. */ repoRoot?: string; /** * The SAME four-class resolver the rules ran against (`RuleContext.resolver`), * built once per audit. `tokens` above is the FLAT TokenMap from * `loaders/tokens.ts`, which does not see CSS custom properties or SCSS * variables at all — those reach only the graph. A hook that answers "is * there a token for this value?" from `tokens` alone therefore reports "no" * on a whole class of repo and demotes a genuine resolver `exact` to `low`. * Hooks that ask that question must prefer this when it is present. * * Optional: absent on the MCP / single-file / codemod paths, which have no * resolver either, and the hook then keeps its pre-resolver behaviour. */ resolver?: import("./graph/resolve/types.js").Resolver; } export interface CodemodContext extends ClassifyContext { fileContent: string; parsedAst: unknown; } export interface CodemodResult { diff: string; importsAdded: string[]; confidence: Confidence; warnings?: string[]; }