From 764203cf4a3a3f4ec3c34ebe7049bbd9a04adca9 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:14:27 +0800 Subject: [PATCH] refactor(contracts): key the tool effect audit by public tool name (#10579) TOOL_EFFECT_AUDIT was typed Record, so a tool missing from the audit or a misspelled entry was found only by a runtime throw or the key-equality unit test. Check the audit with satisfies Record, where PublicToolName is the union of TOOL_CONTRACTS names. A missing, extra or misspelled entry is now a compile error. String lookups for callers that hold an unchecked name keep their runtime refusal. Delete the unit test that compared audit keys with contract names; the type now owns that check. --- src/contracts/toolEffects.test.ts | 6 ------ src/contracts/toolEffects.ts | 29 ++++++++++++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/contracts/toolEffects.test.ts b/src/contracts/toolEffects.test.ts index 15a57d7164..ae0d05fbd9 100644 --- a/src/contracts/toolEffects.test.ts +++ b/src/contracts/toolEffects.test.ts @@ -22,12 +22,6 @@ describe("tool effects", () => { mutatesTarget: false, }); }); - it("audits every public tool exactly once", () => { - expect(Object.keys(TOOL_EFFECTS).sort()).toEqual( - TOOL_CONTRACTS.map(({ name }) => name).sort(), - ); - }); - it("marks foreign execution and device mutation as destructive", () => { const destructive = TOOL_CONTRACTS.filter( ({ effects }) => effects.executesForeignCode || effects.mutatesDevice, diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index 382a873757..317967d132 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -1,3 +1,5 @@ +import type { TOOL_CONTRACTS } from "./toolContracts.js"; + /** Possible effects across a tool's supported inputs, including temporary work. */ export interface ToolEffects { readonly mutatesTarget: boolean; @@ -75,8 +77,11 @@ const providerUiNetworkEvidence = providerEffects(evidence, { changesUiState: true, }); +/** Every public tool name; the audit below must list each one exactly once. */ +type PublicToolName = (typeof TOOL_CONTRACTS)[number]["name"]; + /** Explicit effect audit for every public tool. */ -const TOOL_EFFECT_AUDIT: Readonly> = { +const TOOL_EFFECT_AUDIT = { inspect_analysis_settings: effects(), update_analysis_settings: effects({ mutatesSession: true }), list_cutter_sessions: effects({ accessesNetwork: true }), @@ -911,7 +916,11 @@ const TOOL_EFFECT_AUDIT: Readonly> = { record_unknown: effects({ mutatesSession: true, idempotent: false }), update_unknown: effects({ mutatesSession: true, idempotent: false }), verify_unknown_resolution: effects(), -}; +} satisfies Readonly>; + +/** Lookup by an unchecked name; a missing entry is reported by the caller. */ +const auditedEffects: Readonly> = + TOOL_EFFECT_AUDIT; const sessionEffects = (audit: EffectAudit): ToolEffects => "session" in audit ? audit.session : audit; @@ -919,17 +928,19 @@ const sessionEffects = (audit: EffectAudit): ToolEffects => /** Possible effects across every supported provider profile. */ export const TOOL_EFFECTS: Readonly> = Object.fromEntries( - Object.entries(TOOL_EFFECT_AUDIT).map(([name, audit]) => [ - name, - "session" in audit - ? effects({ ...audit.session, ...audit.global }) - : audit, - ]), + Object.entries(TOOL_EFFECT_AUDIT).map( + ([name, audit]: [string, EffectAudit]) => [ + name, + "session" in audit + ? effects({ ...audit.session, ...audit.global }) + : audit, + ], + ), ); /** Effects owned by the tool before composing the selected provider. */ export const toolSessionEffects = (name: string): ToolEffects => { - const audited = TOOL_EFFECT_AUDIT[name]; + const audited = auditedEffects[name]; if (audited === undefined) throw new Error(`Missing effect audit for ${name}`); return sessionEffects(audited);