From de43ace2593f760825e4515e77ff7eb406952c46 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 20 Aug 2026 12:58:17 -0400 Subject: [PATCH] Type two functions to what they read, removing nine assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine of the gate's findings were not assertions at all in spirit — they were two function signatures narrower than their callers, paid for at every call site. `recomputeIndex0Stats` took a bare `Dict` while all five of its callers hold a real `Index0`, so each one laundered it through `as unknown as Dict`. It reads exactly three fields, so it now says so: a Pick over events, checkpoints and heads. Any Index0 satisfies it, a partial one still does, and the five casts are gone. `exportBackup` took `Record` and is genuinely called two ways: with a live VaultIndex from three places in the CLI and API, and with a round-tripped backup payload from the tests. It treats either as an opaque JSON document to canonicalize and encrypt, so the signature now names both shapes instead of making the majority caller pretend to be the minority one. No type surgery and no behaviour change: the same values reach the same code, they just no longer have to lie about what they are on the way in. 44 -> 35. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN --- js/packages/cli/src/api/routes.ts | 2 +- js/packages/cli/src/program.ts | 2 +- js/packages/cli/src/tui/menus.ts | 2 +- js/packages/core/src/vault/index0.ts | 18 ++++++++++++------ js/packages/core/src/vault/portableBackup.ts | 7 ++++++- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/js/packages/cli/src/api/routes.ts b/js/packages/cli/src/api/routes.ts index cec88e84..95ae2507 100644 --- a/js/packages/cli/src/api/routes.ts +++ b/js/packages/cli/src/api/routes.ts @@ -745,7 +745,7 @@ export function registerRoutes(server: ApiServer, ctx: ApiContext): void { const vault = await readVault(ctx); const body = bodyObject(req); const encrypt = body["plaintext"] !== true; - const wrapper = await exportBackup(vault.index as unknown as Record, { + const wrapper = await exportBackup(vault.index, { mnemonic, fingerprint: generateFingerprint(mnemonic), encrypt, diff --git a/js/packages/cli/src/program.ts b/js/packages/cli/src/program.ts index 436bbe06..cf7f8f25 100644 --- a/js/packages/cli/src/program.ts +++ b/js/packages/cli/src/program.ts @@ -2425,7 +2425,7 @@ export function buildProgram(io: ProgramIo = defaultIo): Command { existsSync(resolved) && statSync(resolved).isDirectory() ? join(resolved, defaultBackupFilename(fingerprint)) : resolved; - const wrapper = await exportBackup(vault.index as Record, { + const wrapper = await exportBackup(vault.index, { mnemonic: vault.mnemonic, fingerprint, encrypt: !o.plaintext, diff --git a/js/packages/cli/src/tui/menus.ts b/js/packages/cli/src/tui/menus.ts index a7951df5..d2c50602 100644 --- a/js/packages/cli/src/tui/menus.ts +++ b/js/packages/cli/src/tui/menus.ts @@ -1505,7 +1505,7 @@ async function exportDatabase(s: Session): Promise { } else if (!basename(dest).includes(".")) { dest = `${dest}${BACKUP_EXTENSION}`; } - const payload = await exportBackup(s.vault.index as unknown as Record, { + const payload = await exportBackup(s.vault.index, { mnemonic: s.vault.mnemonic, fingerprint: s.fingerprint, }); diff --git a/js/packages/core/src/vault/index0.ts b/js/packages/core/src/vault/index0.ts index 37ca04fc..184a2653 100644 --- a/js/packages/core/src/vault/index0.ts +++ b/js/packages/core/src/vault/index0.ts @@ -343,7 +343,13 @@ export function normalizeViewManifest(raw: unknown): Dict { }; } -export function recomputeIndex0Stats(index0: Dict): Dict { +// Typed to exactly what it reads. It used to take a bare Dict, so every one +// of its five callers — all holding a real Index0 — had to launder it through +// `as unknown as Dict` to get in. A Pick says what the function needs, admits +// any Index0 without ceremony, and still accepts a partial one. +export function recomputeIndex0Stats( + index0: Pick, +): Dict { const events = normalizeMapping(index0["events"]); const checkpoints = normalizeMapping(index0["checkpoints"]); const heads = normalizeMapping(index0["heads"]); @@ -394,7 +400,7 @@ export function normalizeIndex0(raw: unknown): Index0 { const normalizedHead = normalizeHead(head); if (normalizedHead) normalized.heads[String(writerId)] = normalizedHead; } - normalized.stats = recomputeIndex0Stats(normalized as unknown as Dict); + normalized.stats = recomputeIndex0Stats(normalized); return normalized; } @@ -544,7 +550,7 @@ export function appendIndex0Event(payload: unknown, options: AppendEventOptions) head_hash: computeHeadHash(event), modified_ts: event["modified_ts"] as number, }; - system.stats = recomputeIndex0Stats(system as unknown as Dict); + system.stats = recomputeIndex0Stats(system); return out; } @@ -658,7 +664,7 @@ export function compactIndex0( normalized as unknown as Dict, options, ); - normalized.stats = recomputeIndex0Stats(normalized as unknown as Dict); + normalized.stats = recomputeIndex0Stats(normalized); return normalized; } @@ -850,7 +856,7 @@ export function rebuildCanonicalViewsPayload( const sortedViews: Record = {}; for (const key of Object.keys(views).sort(compareCodePoints)) sortedViews[key] = views[key]!; index0.canonical_views = sortedViews; - index0.stats = recomputeIndex0Stats(index0 as unknown as Dict); + index0.stats = recomputeIndex0Stats(index0); (out["_system"] as Dict)["index0"] = index0; return out; } @@ -1038,6 +1044,6 @@ export function mergeSystemIndex0(current: unknown, incoming: unknown): Index0 { heads: sortRecords(heads), stats: {}, }; - merged.stats = recomputeIndex0Stats(merged as unknown as Dict); + merged.stats = recomputeIndex0Stats(merged); return merged; } diff --git a/js/packages/core/src/vault/portableBackup.ts b/js/packages/core/src/vault/portableBackup.ts index 532bc8a4..be0c2c11 100644 --- a/js/packages/core/src/vault/portableBackup.ts +++ b/js/packages/core/src/vault/portableBackup.ts @@ -12,6 +12,7 @@ import { base64 } from "@scure/base"; import { z } from "zod"; import { canonicalJson, canonicalHash } from "../sync/canonical.js"; import { deriveIndexKeyBytes } from "./indexKey.js"; +import type { VaultIndex } from "../schema/entries.js"; import { decryptPayload } from "./payload.js"; import { encryptV3 } from "./aead.js"; import { utf8 } from "../util/bytes.js"; @@ -102,7 +103,11 @@ export async function importBackup( /** Export an index as a portable backup wrapper (format_version 1). */ export async function exportBackup( - index: Record, + // A live VaultIndex or a round-tripped backup payload: both are passed, and + // the function treats either as an opaque JSON document to canonicalize and + // encrypt. Naming both is what lets a caller holding a real index hand it + // over without laundering it through `as unknown as Record`. + index: VaultIndex | Record, options: { mnemonic: string; fingerprint: string;