/** * Shared identity and development/production parity facts. * * The CLI doctor and the web pipelines must answer the same question: will identity-sensitive * modules and emitted app surfaces resolve to one coherent graph? This internal seam owns path and * manifest normalization so those callers cannot quietly grow separate rules. */ import { type SingleCopyRegistration } from "@nifrajs/core/single-copy"; export interface IdentityParityCopy { readonly version: string; readonly path: string; /** * The resolved realpath of the copy. `path` is display-relative to the invoked directory, which * reads well in a report but cannot be pasted into a resolver or an editor from anywhere else. * Absent on a finding built by hand (a test fixture, an older cached result). */ readonly absolutePath?: string; readonly importers: readonly string[]; } export type IdentityParityCause = "version-skew" | "duplicate-path"; export interface IdentityParityFinding { readonly package: string; readonly copies: readonly IdentityParityCopy[]; /** Unique package versions observed across the physical copies. */ readonly versions: readonly string[]; /** `version-skew` when copies advertise different versions; otherwise the same-version path split. */ readonly cause: IdentityParityCause; readonly explanation: string; readonly remediation: string; /** * Why the copies exist, in install-topology terms: how many physical paths sit under how many * install roots, and whether any of those roots is outside the scanned project. * * A path list alone leaves the reader to reverse-engineer the shape. The two shapes need opposite * fixes: a NESTED install under the scanned root shadows the hoisted copy and one reinstall * collapses it, while a SIBLING install root (a linked checkout, a standalone app beside this one) * owns its own `node_modules` and no reinstall here can touch it. * * Absent on a finding built by hand (a test fixture, an older cached result). */ readonly topology?: string; /** * Why a copy the invoked directory does not import is still fatal here. * * The gate is workspace-wide on purpose. Scoping it to the invoked app would be more precise and * would also reintroduce the blindness this guard was built for: a copy that arrives through a * workspace-linked dependency is not visible from the app directory, and that case shipped a * broken dev server while the check reported "none". A workspace-wide answer over-reports in * exchange for never under-reporting - and over-reporting is the failure a developer can see and * act on, where under-reporting is the one nobody knows happened. * * Present only when the answer would otherwise look wrong: a subdirectory was scanned as its * workspace, and at least one copy sits outside that subdirectory. */ readonly scope?: string; /** * The copies exist but the app declared this package single-copy, so the resolver collapses them * before anything loads. Reported, never fatal - see `SingleCopyCoverage`. */ readonly deduplicated: boolean; } /** * What the app declared about deduplication, and how much of it is actually armed. * * A duplicate physical path is only a defect if something still LOADS both copies. An app consuming a * linked sibling repository cannot collapse the paths without giving up the property it chose `link:` * for - each repository owning its own `node_modules` - so nifra lets it declare the packages instead * (`"nifra": { "singleCopy": [...] }` in package.json) and verifies the declaration here rather than * failing on the raw path count. * * Bundled phases need nothing further: `buildClient`/`buildServer`/dev inject the resolver themselves. * Unbundled phases do, because Bun's runtime never offers a bare specifier to a resolver hook, so the * plugin has to be preloaded to intercept the load. `registration` is that proof, read statically out * of `bunfig.toml`. */ export interface SingleCopyCoverage { /** Declared package names and patterns, exactly as written. Empty when nothing was declared. */ readonly declared: readonly string[]; readonly registration: SingleCopyRegistration; } export interface IdentityParityResult { /** * The governing workspace root: where importer enumeration starts, and how far a copy lookup may * walk up from an importer. * * EVERY caller resolves this the same way. The doctor and the build/dev preflight used to differ * here - one anchored at the workspace, the other at the app directory - so the same project could * be told it had duplicates by one command and a clean bill by the other. Two answers from one * toolchain is worse than either answer, so the basis is now fixed and reported rather than chosen. */ readonly workspaceRoot: string; /** The directory the caller asked about. Differs from `workspaceRoot` when a package subdirectory * is governed by a workspace above it; carried so a report can state the basis it scanned on. */ readonly requestedRoot: string; /** * Enumeration stopped at `MAX_WORKSPACE_IMPORTERS`, so this scan is PARTIAL. * * An empty `findings` then means "nothing found in the part that was scanned", never "clean" - a * caller that prints a clean bill on a truncated scan is the exact silence this flag exists to * prevent. */ readonly truncated: boolean; /** Findings that no declaration covers - the ones a build must refuse to start on. */ readonly findings: readonly IdentityParityFinding[]; /** Duplicates the declaration covers. Worth printing, never worth failing. */ readonly deduplicated: readonly IdentityParityFinding[]; readonly singleCopy: SingleCopyCoverage; } export interface BuildManifestLike { readonly entry: string; readonly assets: readonly string[]; readonly routes: Readonly>; readonly publicFiles?: readonly string[]; readonly css?: readonly string[]; } export interface ParityManifest { readonly moduleGraph: { readonly routes: readonly string[]; readonly routeChunks: Readonly>; readonly emittedAssets: readonly string[]; }; readonly publicFiles: readonly string[]; readonly css: readonly string[]; } export interface DevelopmentParityInput { readonly routes: Readonly>; readonly publicFiles: readonly string[]; readonly css: readonly string[]; /** The scanned first-party source root, carried only so a css parity failure can name where the * scanner looked. Optional: callers that hand-build an input for a unit test may omit it. */ readonly sourceRoot?: string; } export type ManifestParitySection = "module-graph" | "public-files" | "css"; export interface ManifestParityDifference { readonly section: ManifestParitySection; readonly development: unknown; readonly production: unknown; } export declare const pathInside: (root: string, path: string) => boolean; /** Resolve the workspace root once, even when the caller starts in a workspace package. */ export declare function resolveParityWorkspaceRoot(start: string): Promise<{ readonly root: string; readonly package: Record; }>; export declare const resolvedInstalledCopy: (importer: string, boundary: string, name: string) => Promise<{ readonly path: string; readonly version: string; } | undefined>; export declare const displayPath: (cwd: string, path: string) => string; /** Find duplicate identity-sensitive package realpaths without reading application payloads. */ export declare function collectIdentityParity(cwd: string, rootPackage?: Record): Promise; /** One `- pkg [cause]: ...` block per finding, shared by the hard failure and the dev warning. */ export declare function formatIdentityParityFindings(findings: readonly IdentityParityFinding[]): string; /** * The basis a scan ran on, for any surface that reports its result. * * A verdict about installs is only as good as where it looked, and the reader cannot see that from a * list of relative paths. Stating it also makes a truncated scan impossible to read as a clean one. */ export declare function identityParityBasis(result: IdentityParityResult): string; /** `2 primary package findings` / `1 primary package finding`. */ export declare const identityParityHeadline: (count: number) => string; /** * Fail before dev/build when the same identity-sensitive package has multiple realpaths AND nothing * is collapsing them. * * A declared package is not a suppression: the build injects the single-copy resolver, so the duplicate * genuinely cannot reach the output. Failing on it anyway would leave the only supported answer being * to change the install topology, which is the thing an app using linked sibling repositories cannot * do. `result.deduplicated` still carries every covered duplicate for the caller to print. */ export declare function assertIdentityParity(cwd: string, rootPackage?: Record): Promise; /** Convert hashed production asset names into the stable module-graph contract. */ export declare function logicalStaticAssets(manifest: BuildManifestLike): readonly string[]; export declare function normalizeBuildManifest(manifest: BuildManifestLike): ParityManifest; export declare function createDevelopmentParityManifest(input: DevelopmentParityInput): ParityManifest; /** Read the source-side dev contract, independent of any production output. */ export declare function collectDevelopmentParityInput(routesDir: string, publicDir: string | false | undefined): DevelopmentParityInput; export declare function compareManifestParity(development: ParityManifest, production: ParityManifest): readonly ManifestParityDifference[]; /** Validate a production manifest against the source manifest a development server should serve. */ export declare function assertDevelopmentProductionParity(development: DevelopmentParityInput, production: BuildManifestLike): readonly ManifestParityDifference[]; //# sourceMappingURL=parity.d.ts.map