/** * Inspect and resolve the portable project identity (T12353 · ADR-094 · T12716). * * The tracked identity is `.cleo/project.json` `{schemaVersion, id, name}` * (T12716), with the ADR-094 `.cleo/project-id` kept as a legacy mirror. * `project-info.json` is untracked and device-local; its `projectId` is a * cache of the tracked id. The tracked id always wins at runtime. This module * is the ONLY place that migrates or re-keys identity files — nothing does it * on open, init or upgrade: * * - {@link inspectProjectIdentity} is read-only. It classifies the three * sources (project.json / project-id / project-info.json), checks whether * git actually tracks the canonical file, and returns the exact remedy. * - {@link inspectProjectNameDrift} is read-only. It compares the registry's * label with the declared display name. * - {@link resolveProjectIdentity} applies the remedy. A legacy project gets * `project.json` written from its `project-id` and `project-info.json` name * (no id ever changes). For a conflict it re-keys the LOCAL side to the * tracked id: the registry row's primary key and every alias pointing at the * old id are updated in one registry transaction, and the old id is itself * recorded as an alias. Nothing is deleted: rows elsewhere that still carry * the old id (audit log, session manifest, background jobs) keep resolving * through the alias table, and credentials sealed under it are re-wrapped on * their next open (`previousProjectIds`). * * The id in a tracked file is never modified here. It is write-once, and a * malformed file is restored from version control, never regenerated. * * T12557: `projectRoot` is a path fact, derived from the real root at runtime. * A persisted copy in `project-info.json` or `project-context.json` goes stale * on every move, so the inspection lists it under `derivedFields` and * `--resolve` strips it. Each removed value is kept in `project-info.json` * under `strippedFields`, the same in-file receipt pattern as * `previousProjectIds`. `projectHash` is NOT a path fact: it is a write-once * identity key, and nothing here ever touches it. * * @task T12353 * @task T12557 * @task T12716 * @see ADR-094 — write-once portable project identity (amends ADR-013 §9) * @see ADR-096 — one committed `.cleo/project.json` {id, name} (amends ADR-094) */ /** * Classification of a project's identity sources. * * - `ok` — the tracked id and the cache agree, `project.json` and its * `project-id` mirror agree, and git tracks `.cleo/project.json`. * - `uninitialized` — no identity file exists. * - `not-adopted` — only tracked files exist (a fresh clone before `cleo init`). * - `missing` — `project-info.json` has an id, but no tracked file exists. * - `legacy` — only the ADR-094 `.cleo/project-id` is tracked; `--resolve` * writes `.cleo/project.json` from it (T12716). * - `mirror-missing` — `.cleo/project.json` exists, the legacy * `.cleo/project-id` mirror does not; `--resolve` writes it (T12716). * - `mirror-conflict` — `.cleo/project.json` and `.cleo/project-id` both * exist and disagree (or the mirror is malformed). The `project.json` id * wins; the files are fixed by hand from version control (T12716). * - `conflict` — the tracked id and the `project-info.json` cache disagree. * - `invalid` — a tracked file that decides the id cannot be parsed. * - `info-invalid` — `project-info.json` exists but has no usable id. * - `untracked` — consistent, but git does not track `.cleo/project.json` yet. * - `ignored` — consistent, but a gitignore rule excludes `.cleo/project.json`. */ export type ProjectIdentityState = 'ok' | 'uninitialized' | 'not-adopted' | 'missing' | 'legacy' | 'mirror-missing' | 'mirror-conflict' | 'conflict' | 'invalid' | 'info-invalid' | 'untracked' | 'ignored'; /** A persisted path fact in a metadata file (T12557). */ export interface DerivedFieldFinding { /** File under `.cleo/` that carries the field. */ readonly file: 'project-info.json' | 'project-context.json'; /** The field name. */ readonly field: 'projectRoot'; /** The persisted value, kept so a strip receipt can restore it. */ readonly value: unknown; } /** Read-only report produced by {@link inspectProjectIdentity}. */ export interface ProjectIdentityInspection { /** Absolute project root that was inspected. */ readonly projectRoot: string; /** Classification of the identity sources. */ readonly state: ProjectIdentityState; /** * The tracked id the runtime uses (`.cleo/project.json`, else the legacy * `.cleo/project-id`), when valid. */ readonly trackedId: string | null; /** Id in `.cleo/project.json`, when valid (T12716). */ readonly manifestId: string | null; /** Id in the legacy `.cleo/project-id`, when valid (T12716). */ readonly legacyId: string | null; /** Display name declared in `.cleo/project.json`, when valid (T12716). */ readonly declaredName: string | null; /** Id in `project-info.json` (the device-local cache), when present. */ readonly localId: string | null; /** One-line explanation. */ readonly message: string; /** Exact command(s) that fix it, or `null` when nothing needs fixing. */ readonly remedy: string | null; /** * Legacy path-derived fields persisted on disk (T12557). They are ignored at * runtime and do not change `state`; `--resolve` strips them. */ readonly derivedFields: readonly DerivedFieldFinding[]; } /** One step {@link resolveProjectIdentity} took or would take. */ export interface IdentityResolutionStep { /** What the step does. */ readonly action: 'write-tracked-id' | 'write-project-json' | 'write-legacy-mirror' | 'rekey-registry-row' | 'merge-registry-row' | 'repoint-aliases' | 'alias-old-id' | 'drop-inverted-alias' | 'rewrite-project-info' | 'confirm-candidate-location' | 'strip-derived-fields' | 'allow-project-json' | 'rewrap-credentials' | 'sync-registry-name'; /** Human-readable detail with the ids and counts involved. */ readonly detail: string; } /** Result of {@link resolveProjectIdentity}. */ export interface IdentityResolution { /** `true` when nothing was written. */ readonly dryRun: boolean; /** State before resolving. */ readonly before: ProjectIdentityInspection; /** Steps, in order. When `dryRun` is `true`, none of them were applied. */ readonly steps: readonly IdentityResolutionStep[]; /** * Registry row count before and after. Equal counts show no row was lost; a * `merge-registry-row` step lowers it by one, and that row is kept whole in a * `merge-identity` nexus audit receipt. */ readonly registryRows: { readonly before: number; readonly after: number; }; /** * Why no steps were planned. Set when the state needs no resolution, or * when resolving is refused (the message then carries the remedy). */ readonly refused: string | null; } /** * Classify the identity sources of a project, read-only. * * @param projectRoot - Absolute project root. * @returns The inspection with the exact remedy command. * * @example * ```ts * const report = inspectProjectIdentity('/repo'); * if (report.remedy) console.log(report.remedy); * ``` */ export declare function inspectProjectIdentity(projectRoot: string): ProjectIdentityInspection; /** * Return `info` with `findings` appended to its `strippedFields` receipt * (capped at the schema's `maxItems`) and, when `stripInfo`, its own path * facts removed. Pure; the caller writes the result. * * @param info - Parsed `project-info.json`. * @param findings - Path facts being removed, with their values. * @param now - ISO timestamp recorded on each receipt entry. * @param stripInfo - Remove `DERIVED_FIELDS` from `info` itself. * @returns A new object; `info` is not mutated. * @example * ```ts * const next = withStrippedFieldsReceipt(info, findings, new Date().toISOString(), true); * ``` * @task T12557 */ export declare function withStrippedFieldsReceipt(info: Readonly>, findings: readonly DerivedFieldFinding[], now: string, stripInfo: boolean): Record; /** Options for {@link resolveProjectIdentity}. */ export interface ResolveProjectIdentityOptions { /** Plan only; write nothing. */ dryRun?: boolean; /** Global CLEO home whose registry is re-keyed (defaults to the current one). */ cleoHome?: string; } /** * Resolve the identity state that {@link inspectProjectIdentity} reported — * the ONLY migration path from the legacy files to `.cleo/project.json` * (T12716); nothing migrates on open, init or upgrade. * * - `legacy` writes `.cleo/project.json` (create-only) from the legacy * `.cleo/project-id` id and the `project-info.json` name (else the * basename). `project-id` stays as the legacy mirror; no id changes. * - `missing` writes `.cleo/project.json` and the `project-id` mirror from the * local id (create-only). * - `mirror-missing` writes the legacy `.cleo/project-id` mirror (create-only). * - Whenever `project.json` is (or is about to be) ignored by a pre-T12716 * `.cleo/.gitignore` — including the `ignored` state — `!project.json` is * inserted there, so the file can be committed; nothing else in it changes. * - `info-invalid` re-runs the scaffold decision (tracked id or registry re-link). * - `conflict` re-keys local state to the tracked id. One registry transaction * renames the row's primary key, re-points aliases and records the old id as * an alias. Then `project-info.json` is rewritten, keeping the old id under * `previousProjectIds`, and `project.json` is written when only the legacy * file existed. * - In every state, persisted `projectRoot` fields are stripped first * (T12557). The removed values are kept in `project-info.json` * `strippedFields`. `projectHash` is never touched. * - Last, a registry label that drifted from the declared name is synced * (see {@link inspectProjectNameDrift}), unless another project holds it. * * Every other state (`invalid`, `mirror-conflict`, `untracked`, …) is refused * with its remedy. The operation is idempotent: re-running after a partial * apply completes the remaining steps. * * @param projectRoot - Absolute project root. * @param options - `dryRun` and registry home. * @returns The plan (dry run) or the applied steps, with registry row counts. * * @example * ```ts * const plan = await resolveProjectIdentity(root, { dryRun: true }); * if (!plan.refused) await resolveProjectIdentity(root); * ``` */ export declare function resolveProjectIdentity(projectRoot: string, options?: ResolveProjectIdentityOptions): Promise; /** Registry label vs declared display name (T12716). */ export interface ProjectNameDrift { /** * - `ok` — the registry label equals the declared name. * - `drift` — they differ and `--resolve` can sync the label. * - `taken` — they differ, but another project on this device holds the * declared name as its label; rename one of them. * - `not-registered` — no registry row for the declared id on this device. * - `no-declared-name` — no `.cleo/project.json` yet (a legacy project); * there is no committed name to compare against. * - `undeclared` — the project declares no id. * - `unavailable` — the registry could not be read (coverage missing). */ readonly state: 'ok' | 'drift' | 'taken' | 'not-registered' | 'no-declared-name' | 'undeclared' | 'unavailable'; /** The declared id whose row was compared. */ readonly projectId: string | null; /** The committed name in `.cleo/project.json` (or the name a planned migration writes). */ readonly declaredName: string | null; /** The registry row's label. */ readonly registryName: string | null; /** One-line explanation. */ readonly message: string; /** Exact command that fixes it, or `null`. */ readonly remedy: string | null; } /** * Compare the global registry's label for this project with the name the * project declares in its committed `.cleo/project.json` (T12716), read-only. * Only a committed name counts: a legacy project's cached name or basename * differs per checkout, so it is never a drift source. * * @param projectRoot - Absolute project root. * @param cleoHome - Global CLEO home whose registry is read. * @param plannedName - The name `project.json` will hold once a planned * migration writes it (used by `--resolve` so its dry-run matches the apply). * @param rowId - The id the registry row is keyed by right now, when it * differs from the declared id (a conflict whose re-key is only planned). * @returns The drift report; an unreadable registry is `unavailable`, never `ok`. * * @example * ```ts * const drift = await inspectProjectNameDrift('/repo'); * if (drift.state === 'drift') console.log(drift.remedy); * ``` * @task T12716 */ export declare function inspectProjectNameDrift(projectRoot: string, cleoHome?: string, plannedName?: string, rowId?: string): Promise; //# sourceMappingURL=project-identity.d.ts.map