Reproduced case / TypeScript API drift

TypeScript 編譯通過,但 dependency signature 已漂移

危險的 dependency change 不一定讓 build 立刻變紅。Application file 沒改、既有 call 仍然 type-correct,tsc --noEmit 也回傳 0;但當初寫下程式所依賴的 external assumption 已經不再成立。

Evidrift 偵測 TypeScript dependency signature drift 的實際 CLI 動畫
Reproduce it

先自己跑一次

npx --yes evidrift@0.3.2 demo

不需要帳號、API key、cloud service、package-code execution 或 LLM verdict。指令會在 .evidrift-demo/signature-drift 建立有 marker 的可丟棄 workspace。

Before

Dependency 原本的 contract

本機 fixture package 一開始提供這份 declaration:

export interface ParseOptions { strict?: boolean; }
export interface ParseResult { value: string; }

export declare function parseConfig(
  input: string,
  options?: ParseOptions,
): ParseResult;

Consumer application 已經傳入 options

import { parseConfig } from '@evidrift/demo-contract';

const options = { strict: true } as const;
export const parsed = parseConfig('demo', options);

因為這個 call 有提供第二個 argument,所以不論 parameter 是 optional 或 required,這一行都能通過 TypeScript compiler。

Record

Evidrift 記錄了什麼

Evidrift 記錄實際安裝的 declaration、package version、resolved declaration path、affected call site、normalized signature 與 signature hash。這個 fixture 的 claim 刻意保持狹窄:

parseConfig accepts the optional options parameter used by this demo.

第一次檢查會重新計算 declaration,結果通過:

PASS sha256:01c766cf…
Expected signature: parseConfig(input:string,options?:ParseOptions):ParseResult
Current signature: parseConfig(input:string,options?:ParseOptions):ParseResult
Affected code location: app/src/index.ts:3
Receipt ID: sha256:01c766cf…
Summary: 1 pass, 0 warning, 0 fail
After

Dependency 改了,consumer 沒改

Fixture 只修改 declaration:

 export declare function parseConfig(
   input: string,
-  options?: ParseOptions,
+  options: ParseOptions,
 ): ParseResult;

Application 原本就有傳 options,所以 drifted workspace 仍然能編譯。本次 evidrift-0.3.2.tgz packed-install UAT 在 2026-07-17 對 demo 產生的 drifted workspace 重跑 tsc --noEmit --strict,TypeScript exit code 是 0

但這不代表 external contract 沒變。Parameter 已經不再 optional。之後任何省略 options 的 call、generated branch、test fixture,或依賴舊 optionality 的 Coding Agent,都拿著過期的 evidence。

CI result

Evidrift 看見的差異

evidrift check 會重新載入 source 與計算 signature,不相信 Receipt 裡儲存的 matchedverified flag:

FAIL contract_mismatch sha256:01c766cf…
Message: Deterministic TypeScript contract mismatch:
Previously recorded TypeScript signature was not found.
Claim: parseConfig accepts the optional options parameter used by this demo.
Expected signature: parseConfig(input:string,options?:ParseOptions):ParseResult
Current signature: parseConfig(input:string,options:ParseOptions):ParseResult
Affected code location: app/src/index.ts:3
Receipt ID: sha256:01c766cf…
Action: Review the dependency change and affected code, then intentionally record new evidence.
Summary: 0 pass, 0 warning, 1 fail

結果是 deterministic:selected normalized signature 改變,因此 check exit code 是 1。沒有 semantic similarity score,也沒有 LLM judge。

Why it matters

為什麼這個缺口值得處理

  • Consumer diff 可以是空的,但 installed dependency declaration 已改變。
  • TypeScript 能證明這個 call 現在 type-correct,卻不保存「parameter 原本可省略」這項 historical assumption。
  • Code review 與一般 static analysis 通常檢查正在改的程式,不會保留舊程式當初成立的外部理由。
  • Receipt 讓 CI 能把 old contract、current contract、affected file 與 content-addressed ID 交回 developer 或 Coding Agent。
Human decision

FAIL 之後該怎麼做

不要直接重新產生 Receipt 來讓 CI 變綠。

  1. 閱讀 dependency changelog,確認 signature change 是否刻意。
  2. 檢查 affected call 與可能省略 options 的其他 path。
  3. 若接受新 contract,更新 code、tests 與 written claim。
  4. 刻意執行 evidrift record,建立新的 evidence。
Honest boundary

它沒有證明什麼

Evidrift 不證明 runtime correctness、不證明每個 call site 都相容,也不判斷 dependency change 是好是壞。它證明的是一件更小但可驗證的事:為這個 affected line 記錄的 deterministic external contract,已經不是目前安裝的 contract。