/** * User-profile hygiene: keep operation receipts out of `nexus_user_profile` * and repair the rows that already got in (T12543). * * ## The defect * * The dialectic hook in the CQRS dispatcher fed every successful mutate * operation to the evaluator as a "turn": `cleo {params}` paired * with `Operation succeeded in domain ""`. The evaluator minted * user traits from those envelopes — "Operation succeeded in domain 'check'", * "gate set to true for T1655" — and, because the table is GLOBAL, facts * lifted from one project's task params were injected into every other * project's spawn prompts. Measured 2026-09-27: 399 of 404 rows were * `dialectic:` rows, each seen exactly once. * * ## What this module provides * * - {@link classifyReceiptTrait} — the text classifier for status lines and * operation receipts. REPAIR ONLY: at write time the structural signal * (`DialecticTurn.origin === 'operation-envelope'`) is the guard, because a * text rule at write time would also block legitimate preferences. * - {@link pruneReceiptTraits} — dry-run / apply repair. Apply writes a * backup file FIRST (a `user_profile.json`-shaped envelope carrying the full * rows), then deletes each row only if it is unchanged since it was read. * - {@link restorePrunedTraits} — re-inserts rows from that backup, never * overwriting a row that was re-created since. * * @task T12543 * @epic T12515 */ import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; /** Drizzle handle over a database holding `nexus_user_profile`. */ type NexusDb = NodeSQLiteDatabase; /** * Classify a trait as an operation receipt / status line. * * @param traitKey - Trait key (kebab-case semantic key). * @param traitValue - Trait value text. * @returns The id of the first matching rule, or `null` when the trait does * not look like a receipt. * * @example * ```ts * classifyReceiptTrait('task-successful', 'Cleo tasks update operation succeeded'); // 'operation-status' * classifyReceiptTrait('strict-typescript', 'never use any'); // null * ``` * * @task T12543 */ export declare function classifyReceiptTrait(traitKey: string, traitValue: string): string | null; /** * Structural rule id: a legacy `dialectic:*` row with no project stamp. Before * T12543 the only writer of dialectic global traits was the dispatcher hook, * whose "turn" was always an operation envelope — so every such row is a * receipt by construction, whatever its text says. */ export declare const OPERATION_ENVELOPE_RULE: "operation-envelope"; /** One trait matched by the repair. */ export interface TraitPruneCandidate { /** Trait key (primary key). */ readonly traitKey: string; /** Trait value at read time. */ readonly traitValue: string; /** Trait source (e.g. `dialectic:`). */ readonly source: string; /** Originating project id, or `null` for unknown origin. */ readonly projectId: string | null; /** Matching rule id (a receipt rule, or {@link OPERATION_ENVELOPE_RULE}). */ readonly rule: string; } /** Options for {@link pruneReceiptTraits}. */ export interface TraitPruneOptions { /** Delete matched rows (after writing a backup). Default: dry run. */ readonly apply?: boolean; /** * Also match every legacy `dialectic:*` row with no project stamp * ({@link OPERATION_ENVELOPE_RULE}). Default: receipt-text rules only. */ readonly includeEnvelopeDerived?: boolean; /** Directory for the backup file. Default: `/backups/user-profile`. */ readonly backupDir?: string; } /** Outcome (and, when applied, receipt) of {@link pruneReceiptTraits}. */ export interface TraitPruneResult { /** Whether rows were deleted (`false` = dry run). */ readonly applied: boolean; /** Row count before the run. */ readonly before: number; /** Row count after the run (equals `before` on a dry run). */ readonly after: number; /** Number of rows matched. */ readonly matched: number; /** Per-rule match counts. */ readonly byRule: Record; /** Every matched trait key, in key order. */ readonly candidateKeys: readonly string[]; /** Up to 10 matched rows, so a dry run shows what would go. */ readonly sample: readonly TraitPruneCandidate[]; /** * Unknown-origin rows (`project_id IS NULL`, scope `project`) that this run * leaves in place. They are excluded from every spawn prompt and remain * queryable via the `nexus.profile.view` operation. */ readonly unknownOriginRetained: number; /** Keys actually deleted (apply only). */ readonly removedKeys?: readonly string[]; /** Keys skipped because the row changed after it was read (apply only). */ readonly changedSkipped?: readonly string[]; /** ISO-8601 instant of the deletion (apply only). */ readonly removedAt?: string; /** Absolute path of the backup written BEFORE any deletion (apply only). */ readonly backupPath?: string; /** SHA-256 of the backup file bytes (apply only). */ readonly backupSha256?: string; /** Command that reverses the apply (apply only). */ readonly restoreCommand?: string; } /** * List — and with `apply`, remove — receipt-style user-profile traits. * * Apply is reversible: the full matched rows are written to a backup file * before anything is deleted, and each delete is guarded on the row being * unchanged since it was read (a re-derived or edited row is skipped and * reported). Reverse with {@link restorePrunedTraits} / * `cleo memory prune-traits --restore `. * * @param nexusDb - Drizzle handle over the database holding `nexus_user_profile`. * @param options - Apply flag, rule selection and backup location. * @returns Counts, per-rule breakdown, sample, and — when applied — the receipt. * * @example * ```ts * const preview = await pruneReceiptTraits(db); * if (preview.matched > 0) await pruneReceiptTraits(db, { apply: true }); * ``` * * @task T12543 */ export declare function pruneReceiptTraits(nexusDb: NexusDb, options?: TraitPruneOptions): Promise; /** Outcome of {@link restorePrunedTraits}. */ export interface TraitRestoreResult { /** Backup file read. */ readonly backupPath: string; /** SHA-256 of the backup bytes read (compare with the prune receipt). */ readonly backupSha256: string; /** Keys re-inserted. */ readonly restoredKeys: readonly string[]; /** Keys skipped because a row with that key exists again. */ readonly skippedExisting: readonly string[]; } /** * Reverse a {@link pruneReceiptTraits} apply from its backup file. * * Re-inserts each backed-up row verbatim (value, provenance, project stamp, * scope, timestamps). A key that exists again is left untouched — a newer * row always wins over a restore. * * @param nexusDb - Drizzle handle over the database holding `nexus_user_profile`. * @param backupPath - Path from the prune receipt's `backupPath`. * @param expectedSha256 - The receipt's `backupSha256`. When given, a backup * whose bytes differ is refused before anything is written. * @returns The restored and skipped keys plus the backup digest. * * @task T12543 */ export declare function restorePrunedTraits(nexusDb: NexusDb, backupPath: string, expectedSha256?: string): Promise; export {}; //# sourceMappingURL=user-profile-hygiene.d.ts.map