/** * Product Analyzer — measure a *consumer product's* usage of Eddie. * * `eddie_get_adoption` answers "which repos declare a dependency on Eddie, and * how stale is the version?" — a package-manifest question at org granularity. * This module answers the next question down: **for one product, how much of the * UI is actually built from Eddie, is it used correctly, how many values are * hardcoded instead of tokens, and how far has it drifted?** Those are the * numbers the Product Inspection Kit's adoption/coverage stations are built on, * and the before/after evidence for "adopting a design system raised quality." * * It is deliberately: * - **Deterministic.** Every number is a count you can reproduce; no LLM. * - **Dependency-free.** HTML is scanned with a small tag tokenizer rather * than pulling a DOM parser into the brain's build. This is a *coverage* * measure, not a spec-compliant parser — it counts tags and tracks parent * nesting via a stack, which is all the metrics need. * - **Pure.** `analyzeProduct()` takes strings + the graph and returns data. * Filesystem/URL collection lives in the caller (see `collectFromPath`), * so the core is trivially testable and works in a disk-less deployment. * - **Per-file.** Inputs are analyzed file by file and aggregated, so every * sample and correctness finding carries a real `file` + `line`, and the * `files` breakdown can answer "which page is the most bespoke?". The * legacy single-string `html`/`css` inputs still work (treated as one * pseudo-file each). * * The Eddie inventory (what "counts as Eddie") comes from the live * ComponentIndex + TokenTaxonomy — the same source of truth the rest of the * brain uses — so coverage denominators track the real catalog, not a snapshot. */ import type { KnowledgeGraph } from '../knowledge-graph/index.js'; import type { ComponentEntry } from '../types.js'; /** A file's repo-relative path + contents, the unit of per-file analysis. */ export interface NamedFile { file: string; content: string; } export interface ProductAnalysisInput { /** Rendered HTML markup (a page's DOM as a string). Prefer `htmlFiles` for multi-file repos. */ html?: string; /** Stylesheet / app CSS text. Prefer `cssFiles` for multi-file repos. */ css?: string; /** Per-file markup inputs — enables per-file attribution. Takes precedence over `html`. */ htmlFiles?: NamedFile[]; /** Per-file stylesheet inputs — enables per-file attribution. Takes precedence over `css`. */ cssFiles?: NamedFile[]; /** Template sources (njk/liquid/vue/jsx/…), used only by the pattern inventory (adoption report). */ templateFiles?: NamedFile[]; /** Consumer package.json contents, for the declared Eddie version. */ packageJson?: string; /** Theme to resolve token colors against for nearest-token mapping. Default 'bfw'. */ theme?: string; /** Optional label (a URL or path) recorded on the result for provenance. */ source?: string; /** Vendored DS files skipped during collection — echoed on the result, never analyzed. */ skippedVendorFiles?: string[]; /** * Eddie package name → latest published version. When present, version drift * is computed deterministically (no network in the pure core). Obtain via * `fetchLatestEddieVersions()` when the caller opts into a drift check. */ latestVersions?: Record; } export interface ComponentCoverage { /** Every element start-tag seen (excludes comments/doctype/script+style). */ elementsTotal: number; /** Elements that are known Eddie custom elements (tag in the catalog). */ eddieElements: number; /** Elements with an `ed-` prefix that are NOT in the catalog (typo/newer/drift). */ unknownEddieElements: number; /** Custom elements (hyphenated tags) that are not Eddie at all. */ nonEddieCustomElements: number; /** Plain HTML elements (div, span, button, …). */ plainHtmlElements: number; /** eddieElements / elementsTotal, 0–1 (0 when no elements). */ coverageRatio: number; /** Distinct Eddie components used, with instance counts, most-used first. */ componentsUsed: Array<{ tagName: string; count: number; }>; /** Distinct non-Eddie custom-element tags (candidate bespoke components), with counts. */ bespokeCustomElements: Array<{ tagName: string; count: number; }>; /** How many of the catalog's components appear at all. */ catalogComponentsUsed: number; catalogComponentsTotal: number; } export interface CorrectnessFinding { kind: 'invented-slot' | 'missing-required-child' | 'floating-prose'; tagName: string; detail: string; /** Repo-relative file the finding was seen in (when per-file inputs were provided). */ file?: string; /** 1-based line within that file. */ line?: number; } export interface HardcodedValue { type: 'color' | 'length' | 'font-family'; value: string; line?: number; /** Repo-relative file the value was seen in (when per-file inputs were provided). */ file?: string; /** For colors: the nearest Eddie token, if one could be resolved. */ nearestToken?: { name: string; value: string; tier: string; distance: number; }; } export interface TokenCoverage { /** Count of var(--ed-*) references. */ tokenReferences: number; /** Count of hardcoded values that should likely be tokens. */ hardcodedValues: number; /** tokenReferences / (tokenReferences + hardcodedValues), 0–1. */ tokenCoverageRatio: number; /** A capped sample of hardcoded values with nearest-token mappings. */ samples: HardcodedValue[]; byType: { color: number; length: number; fontFamily: number; }; } export interface OverrideSurface { /** Declarations using !important inside a rule that targets an ed-* class/tag. */ importantOnEddie: number; /** Rules whose selector re-styles Eddie internals (an `.ed-c-` / `.ed-r-` / `.ed-p-` class). */ eddieInternalSelectors: number; samples: string[]; } /** Per-file roll-up so callers can rank pages/stylesheets by bespokeness. */ export interface FileBreakdown { file: string; kind: 'html' | 'css'; elementsTotal?: number; eddieElements?: number; coverageRatio?: number; correctnessFindings?: number; tokenReferences?: number; hardcodedValues?: number; } export interface ProductAnalysis { source?: string; theme: string; inputs: { html: boolean; css: boolean; packageJson: boolean; }; componentCoverage?: ComponentCoverage; correctness?: { findings: CorrectnessFinding[]; count: number; }; tokenCoverage?: TokenCoverage; overrides?: OverrideSurface; version?: { packages: Array<{ name: string; version: string; latest?: string; upToDate?: boolean; }>; /** True when latest published versions were supplied and drift was computed. */ driftChecked?: boolean; }; /** Per-file breakdown (capped at 200 entries; `filesOmitted` counts the rest). */ files?: FileBreakdown[]; filesOmitted?: number; /** Vendored DS build output found in the product tree — reported, not analyzed as product CSS. */ skippedVendorFiles?: string[]; /** 0–10 Station-7-aligned score + a red/yellow/green light. */ coverageScore: { score: number; light: 'red' | 'yellow' | 'green'; rationale: string[]; }; } export interface RequiredChildRule { parent: string; /** The compound children that are the parent's only meaningful direct children. */ children: string[]; note: string; } /** * Derive required-child rules from the catalog's compound relationships * (`parentComponent` backlinks + `childComponents`, unioned — the live graph * only populates the backlink side). Deliberately conservative: a rule exists * only when the parent's sole slot is `default` AND at least half its compound * children carry a container suffix (item/row/cell/…). That admits the known * container families (ed-grid, ed-link-list, ed-table, ed-accordion, navs) * while excluding compounds that coexist with arbitrary content (ed-page, * ed-card, ed-layout, ed-tooltip). */ export declare function deriveRequiredChildRules(catalog: ComponentEntry[]): RequiredChildRule[]; /** List the Eddie packages declared in a consumer package.json string. */ export declare function listEddiePackages(packageJson: string): string[]; /** * Best-effort lookup of the latest published version for each package via * `npm view` (network). Never throws — packages that can't be resolved are * simply absent from the result. Callers opt in (e.g. `checkDrift: true`); * the pure analysis core takes the returned map as data. */ export declare function fetchLatestEddieVersions(packageNames: string[]): Record; /** Resolve the effective per-file inputs (files take precedence over legacy strings). */ export declare function resolveInputFiles(input: ProductAnalysisInput): { htmlFiles: NamedFile[]; cssFiles: NamedFile[]; }; /** * Analyze a product's Eddie usage from any combination of rendered HTML, CSS, * and a package.json. Pure: no filesystem or network. The caller decides where * the strings come from (a repo scan, a fetched URL, a paste). Multi-file * inputs (`htmlFiles`/`cssFiles`) are analyzed per file and aggregated so every * sample and finding carries a real `file` + `line`. */ export declare function analyzeProduct(graph: KnowledgeGraph, input: ProductAnalysisInput): ProductAnalysis; /** * Detect CSS that is Eddie's *own build output* vendored into a product tree * (e.g. passthrough-copied `/eddie/` bundles). Scanning it as product CSS * produces absurd "overrides of Eddie internals" counts — the DS is not an * override of itself. Two heuristics: * * 1. Path: an `eddie/` directory segment + a DS bundle basename * (tokens/utilities/fonts/components/eddie*.css). * 2. Content: >50% of rule selectors *define* DS classes (`.ed-c-`/`.ed-u-`/ * `.ed-r-`/`.ed-p-`/`.ed-l-`), or the file defines 100+ `--ed-*` custom * properties (a vendored token bundle). */ export declare function isVendoredDsCss(relPath: string, content: string): boolean; export interface CollectOptions { /** Cap per file category (html / css / templates). Default 200. */ maxFiles?: number; /** Also collect template files for the pattern inventory (adoption report). */ includeTemplates?: boolean; /** Skip files larger than this many bytes. Default 2 MiB. */ maxFileBytes?: number; } /** * Gather `ProductAnalysisInput` from a filesystem path. This is the impure edge * that lets `eddie_analyze_product` (and the CLI) take a repo path or a single * file. Kept out of `analyzeProduct()` so the core stays pure and testable. * * - **A single `.html`/`.htm` file** → that file's markup. * - **A single `.css`/`.scss` file** → that file's styles. * - **A directory** → collects every `*.html`/`*.htm` and `*.css`/`*.scss` * (up to a cap) as per-file inputs, and reads a top-level `package.json` for * the declared Eddie version. Build output, deps, and caches are excluded * (see IGNORED_DIRS), and vendored Eddie CSS bundles are skipped and * reported via `skippedVendorFiles` rather than scanned as product CSS. * * Framework template files (.vue/.jsx/.svelte/.astro/.njk/…) are NOT treated * as HTML — their custom syntax would pollute the tag scan. With * `includeTemplates: true` they are collected separately for regex-level * class-attribute extraction (the pattern inventory) only. */ export declare function collectFromPath(path: string, opts?: CollectOptions): ProductAnalysisInput; //# sourceMappingURL=product-analyzer.d.ts.map