import { z } from "zod"; import { AihError } from "../errors.js"; /** * ECC install manifest — the ownership record that lets a rerun tell an AIH-written * file from a user-edited one from a file AIH never touched (issue #555). * * The proof is an install manifest carrying a per-file content hash plus the ECC * source identity the install came from. Ownership is a three-state test, and the * third state is the point: * * in manifest + hash matches -> AIH wrote it, untouched -> safe to replace * in manifest + hash differs -> AIH wrote it, user edited it -> report, never replace * not in manifest -> user-owned -> never touch * * Alternatives were rejected because each collapses a state that matters: an in-file * marker cannot be written into JSON/binary destinations and cannot separate an * untouched AIH file from an edited one; recomputed content identity keeps no record, * so files AIH no longer writes can never be identified as stale; and a paths-only * manifest cannot detect local modification, so an upgrade would silently clobber * user customization. * * A missing or unreadable manifest FAILS CLOSED to "not proven ours — do not touch", * never to "safe to overwrite": every path under the managed root then reports as * unknown provenance. That is also the honest answer for installs predating this * manifest — inferring ownership from a content match against the current source * would resurrect the rejected option, since it cannot tell an AIH-written file from * a user-authored identical one. * * Scope: DETECTION. Nothing here replaces, prunes, or reconciles content — making a * stale install visible and actionable is the whole job. The manifest lives in * repo-local `.aih/` state rather than inside the target directory, so it survives * target-directory cleanup (the case where ownership evidence matters most) and never * pollutes the tool's own surface. Writer/reader mirror `binding/lock.ts`. */ export declare const ECC_INSTALL_MANIFEST_SCHEMA_VERSION = "aih.ecc.install-manifest.v1"; /** Corrupt or schema-invalid ownership evidence — fail closed, never guess. */ export declare class EccInstallManifestError extends AihError { constructor(message: string); } declare const EccManifestFileSchema: z.ZodObject<{ path: z.ZodString; sha256: z.ZodString; }, z.core.$strict>; /** * The ECC source identity an install came from. `git-checkout` installs are * identified by commit, `npm` installs by package + version. A null identifying * field means "not resolvable on that run" and is never treated as a match. */ declare const EccManifestSourceSchema: z.ZodObject<{ kind: z.ZodEnum<{ "git-checkout": "git-checkout"; npm: "npm"; }>; ref: z.ZodNullable; commit: z.ZodNullable; package: z.ZodNullable; version: z.ZodNullable; }, z.core.$strict>; /** The mechanisms that actually write files. `consult` targets install nothing. */ export declare const ECC_INSTALL_MECHANISMS: readonly ["npm", "checkout-merge", "native-script"]; export type EccInstallMechanism = (typeof ECC_INSTALL_MECHANISMS)[number]; declare const EccManifestInstallSchema: z.ZodObject<{ target: z.ZodString; mechanism: z.ZodEnum<{ "checkout-merge": "checkout-merge"; "native-script": "native-script"; npm: "npm"; }>; root: z.ZodString; installedAt: z.ZodString; source: z.ZodObject<{ kind: z.ZodEnum<{ "git-checkout": "git-checkout"; npm: "npm"; }>; ref: z.ZodNullable; commit: z.ZodNullable; package: z.ZodNullable; version: z.ZodNullable; }, z.core.$strict>; files: z.ZodArray>; }, z.core.$strict>; declare const EccInstallManifestSchema: z.ZodObject<{ schemaVersion: z.ZodLiteral<"aih.ecc.install-manifest.v1">; installs: z.ZodArray; root: z.ZodString; installedAt: z.ZodString; source: z.ZodObject<{ kind: z.ZodEnum<{ "git-checkout": "git-checkout"; npm: "npm"; }>; ref: z.ZodNullable; commit: z.ZodNullable; package: z.ZodNullable; version: z.ZodNullable; }, z.core.$strict>; files: z.ZodArray>; }, z.core.$strict>>; }, z.core.$strict>; export type EccManifestFile = z.infer; export type EccManifestSource = z.infer; export type EccManifestInstall = z.infer; export type EccInstallManifest = z.infer; export type EccInstallManifestRead = { present: true; manifest: EccInstallManifest; } | { present: false; }; /** `/.aih/ecc` — repo-local, gitignored derived state (never inside the target dir). */ export declare function eccInstallManifestDir(root: string): string; export declare function eccInstallManifestPath(root: string): string; /** * Read the ownership record. Absent => `{ present: false }` — a repo never installed * by a manifest-aware aih. A PRESENT file that is unparseable or schema-invalid throws: * damaged ownership evidence must never degrade to "empty", because empty would read * as "nothing is ours" in one direction and "nothing is user-owned" in the other. */ export declare function readEccInstallManifest(root: string): EccInstallManifestRead; /** * Atomically write the manifest (validate -> temp file with owner-only mode -> rename), * mirroring the binding lock writer. Validation runs BEFORE anything touches disk, so a * manifest naming a path outside its managed root is rejected rather than stored. */ export declare function writeEccInstallManifestAtomic(root: string, manifest: EccInstallManifest): void; /** Replace the record for one (target, root), leaving every other target's record intact. */ export declare function upsertEccInstall(manifest: EccInstallManifest, install: EccManifestInstall): EccInstallManifest; /** * Every regular file under `root`, as root-relative POSIX paths. Symlinks are SKIPPED * rather than followed: a link is not content aih wrote, and following one would let a * link inside the target dir pull an arbitrary file into the ownership record. * A missing root yields `[]` — nothing installed there yet. */ export declare function walkManagedRoot(root: string, limit?: number): string[]; /** * sha256 of one root-relative file, or `undefined` when it is absent or not a regular file. * * Reads through `readRegularFile`, which does the regular-file check with `fstat` on the * OPEN descriptor rather than a second lookup by name. Checking the path and then hashing * it are otherwise two separate resolutions of the same name (CWE-367): a symlink swapped * in between them would redirect the hash to a file outside the managed root, and this * walks a directory an external installer populates. */ export declare function hashManagedFile(root: string, relativePath: string): string | undefined; /** Evaluate one target root straight off disk — the rerun path. */ export declare function eccInstallDriftForRoot(repoRoot: string, target: string, managedRoot: string, currentSource: EccManifestSource | undefined): EccInstallDrift; export declare const ECC_OWNERSHIP_STATES: readonly ["aih-owned", "stale", "user-modified", "removed", "unknown-provenance"]; export type EccOwnershipState = (typeof ECC_OWNERSHIP_STATES)[number]; export interface EccInstallDriftSample { state: EccOwnershipState; path: string; } export interface EccInstallDrift { target: string; root: string; /** False when nothing proves ownership of this root — no manifest, or no entry for it. */ provenanceKnown: boolean; /** At least one AIH-owned, untouched file is behind the source now on disk. */ stale: boolean; counts: Record; /** Bounded, deterministic examples so a finding names real paths without dumping the tree. */ samples: EccInstallDriftSample[]; recordedSource?: EccManifestSource; currentSource?: EccManifestSource; } export interface EccInstallDriftInput { /** `undefined` when the manifest is absent or unreadable — fails closed to unknown. */ manifest: EccInstallManifest | undefined; target: string; root: string; /** Every file currently under `root`, relative to it. */ presentPaths: readonly string[]; /** Hash of the file at a root-relative path; `undefined` when it is gone. */ hashAt: (path: string) => string | undefined; /** ECC source identity on disk now; `undefined` when it could not be resolved. */ currentSource: EccManifestSource | undefined; } /** * Evaluate the three-state ownership test for one target root and return an actionable * finding: which recorded files are stale (AIH-owned, source moved on), which are * user-modified (never touch), which vanished, and how much of the tree has no * ownership evidence at all. */ export declare function evaluateEccInstallDrift(input: EccInstallDriftInput): EccInstallDrift; export {};