From 55b97b9c2261278d3c92ee1f3000d0a80920432d Mon Sep 17 00:00:00 2001 From: Aaron Olin Date: Sun, 5 Jul 2026 09:43:29 -0500 Subject: [PATCH 1/2] fix(reranker): Score passages with the supported tokenizer pair API The released branch was loading transformers 3.8.1 correctly, but the reranker was calling the tokenizer in a shape that ignored the passage. Keep the dependency on the released package version and make the recovery test assert the text_pair call so this does not slip back. --- src/core/reranker.ts | 15 +++++++++++---- src/embeddings/transformers.ts | 2 +- tests/reranker-recovery.test.ts | 34 ++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/core/reranker.ts b/src/core/reranker.ts index 47de513..2dda09c 100644 --- a/src/core/reranker.ts +++ b/src/core/reranker.ts @@ -21,9 +21,15 @@ const AMBIGUITY_THRESHOLD = 0.08; interface CrossEncoderTokenizer { ( - query: string, - passage: string, - options: { padding: boolean; truncation: boolean; max_length: number } + text: string, + options?: { + text_pair?: string | null; + padding?: boolean | 'max_length'; + truncation?: boolean | null; + max_length?: number | null; + return_tensor?: boolean; + return_token_type_ids?: boolean | null; + } ): unknown; } @@ -152,7 +158,8 @@ async function scorePair(query: string, passage: string): Promise { throw new Error('[reranker] Model not loaded — call ensureModelLoaded() first'); } - const inputs = cachedTokenizer(query, passage, { + const inputs = cachedTokenizer(query, { + text_pair: passage, padding: true, truncation: true, max_length: 512 diff --git a/src/embeddings/transformers.ts b/src/embeddings/transformers.ts index 0a2c581..04db102 100644 --- a/src/embeddings/transformers.ts +++ b/src/embeddings/transformers.ts @@ -70,7 +70,7 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { const { pipeline } = await import('@huggingface/transformers'); // TS2590: pipeline() resolves AllTasks[T] — a union too complex for TSC to represent. - // Cast to a simpler signature; the actual return type IS FeatureExtractionPipelineType. + // Cast to a simpler feature-extraction signature for both v3 and v4-compatible types. type PipelineFn = ( task: 'feature-extraction', model: string, diff --git a/tests/reranker-recovery.test.ts b/tests/reranker-recovery.test.ts index 7cb77ca..6bf3325 100644 --- a/tests/reranker-recovery.test.ts +++ b/tests/reranker-recovery.test.ts @@ -63,7 +63,12 @@ describe('reranker corruption recovery', () => { await fs.mkdir(modelCacheDir, { recursive: true }); await fs.writeFile(path.join(modelCacheDir, 'model.onnx'), 'corrupt'); - const tokenizer = vi.fn((query: string, passage: string) => ({ query, passage })); + const tokenizer = vi.fn( + (query: string, options?: { text_pair?: string | null }) => ({ + query, + passage: options?.text_pair ?? '' + }) + ); const model = vi.fn(async (inputs: { passage: string }) => { if (inputs.passage.includes('/a.ts')) return { logits: { data: [1] } }; if (inputs.passage.includes('/b.ts')) return { logits: { data: [3] } }; @@ -89,6 +94,33 @@ describe('reranker corruption recovery', () => { const reranked = await rerank('auth token', results); expect(transformersMocks.tokenizerFromPretrained).toHaveBeenCalledTimes(2); expect(transformersMocks.modelFromPretrained).toHaveBeenCalledTimes(2); + expect(tokenizer).toHaveBeenCalledWith( + 'auth token', + expect.objectContaining({ + text_pair: expect.stringContaining('/a.ts'), + padding: true, + truncation: true, + max_length: 512 + }) + ); + expect(tokenizer).toHaveBeenCalledWith( + 'auth token', + expect.objectContaining({ + text_pair: expect.stringContaining('/b.ts'), + padding: true, + truncation: true, + max_length: 512 + }) + ); + expect(tokenizer).toHaveBeenCalledWith( + 'auth token', + expect.objectContaining({ + text_pair: expect.stringContaining('/c.ts'), + padding: true, + truncation: true, + max_length: 512 + }) + ); expect(reranked.map((result) => result.filePath)).toEqual(['/b.ts', '/c.ts', '/a.ts']); expect(getRerankerStatus()).toBe('active'); }); From b51adeb96817ec6d85e17fd4edfd4b5e5a0a91bf Mon Sep 17 00:00:00 2001 From: Aaron Olin Date: Sun, 5 Jul 2026 09:43:38 -0500 Subject: [PATCH 2/2] test(release): Keep current version checks from going stale The repo is already on 2.3.0, but the release truth test and manual publish fallback were still pinned to 2.2.0. Derive the assertions from package metadata and normalize macOS temp paths so the full suite checks the real contract instead of local path spelling. --- .github/workflows/publish-npm-on-release.yml | 4 +-- .../contextbench-baseline-schema-gate.test.ts | 25 ++++++++++++++++--- tests/release-truth-surfaces.test.ts | 14 +++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish-npm-on-release.yml b/.github/workflows/publish-npm-on-release.yml index 023126a..4ca7cf4 100644 --- a/.github/workflows/publish-npm-on-release.yml +++ b/.github/workflows/publish-npm-on-release.yml @@ -8,9 +8,9 @@ on: workflow_dispatch: inputs: tag: - description: 'Tag to publish (e.g. v2.2.0)' + description: 'Tag to publish (e.g. v2.3.0)' required: true - default: 'v2.2.0' + default: 'v2.3.0' permissions: contents: read diff --git a/tests/contextbench-baseline-schema-gate.test.ts b/tests/contextbench-baseline-schema-gate.test.ts index ad6fc0a..69b7a93 100644 --- a/tests/contextbench-baseline-schema-gate.test.ts +++ b/tests/contextbench-baseline-schema-gate.test.ts @@ -1,5 +1,12 @@ import { execFileSync } from 'node:child_process'; -import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync +} from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -55,6 +62,10 @@ function cleanupSessionRoot(sessionRoot: string): void { } } +function normalizeFilesystemPath(filePath: string): string { + return realpathSync(filePath); +} + function tempSessionRoot(phase: 'phase40' | 'phase41' = 'phase40'): string { return path.join( mkdtempSync(path.join(tmpdir(), `contextbench-${phase}-schema-gate-`)), @@ -398,7 +409,9 @@ describe('ContextBench Phase 40 schema gate', () => { repoCheckoutPath: repoPath, verificationStrict: false }); - expect(readFileSync(cwdCapturePath, 'utf8')).toBe(repoPath); + expect(normalizeFilesystemPath(readFileSync(cwdCapturePath, 'utf8'))).toBe( + normalizeFilesystemPath(repoPath) + ); const stdin = readFileSync(stdinCapturePath, 'utf8'); expect(stdin).toContain('Problem statement:'); expect(stdin).toContain('Fix the failing ContextBench task'); @@ -584,7 +597,9 @@ describe('ContextBench Phase 40 schema gate', () => { ], { encoding: 'utf8', env } ); - expect(readFileSync(cwdPath, 'utf8')).toBe(repoPath); + expect(normalizeFilesystemPath(readFileSync(cwdPath, 'utf8'))).toBe( + normalizeFilesystemPath(repoPath) + ); } execFileSync( 'node', @@ -681,7 +696,9 @@ describe('ContextBench Phase 40 schema gate', () => { (candidate) => candidate.scoring && 'baselineArmId' in candidate.scoring ); expect(row).toMatchObject({ status: 'completed' }); - expect(readFileSync(cwdCapturePath, 'utf8')).toBe(repoPath); + expect(normalizeFilesystemPath(readFileSync(cwdCapturePath, 'utf8'))).toBe( + normalizeFilesystemPath(repoPath) + ); const stdin = readFileSync(stdinCapturePath, 'utf8'); expect(stdin).toContain('Problem statement:'); expect(stdin).toContain('Run the diagnostic arm with materialized task text.'); diff --git a/tests/release-truth-surfaces.test.ts b/tests/release-truth-surfaces.test.ts index c6d8e60..f362ba4 100644 --- a/tests/release-truth-surfaces.test.ts +++ b/tests/release-truth-surfaces.test.ts @@ -73,6 +73,7 @@ function extractMarkdownLinks(markdown: string): string[] { describe('release truth surfaces', () => { const packageJson = readJson('package.json'); const releaseManifest = readJson('.release-please-manifest.json'); + const expectedVersion = packageJson.version; const changelog = readText('CHANGELOG.md'); const readme = readText('README.md'); const workflow = readText('.github/workflows/publish-npm-on-release.yml'); @@ -80,10 +81,9 @@ describe('release truth surfaces', () => { const visualsDoc = readOptionalText('docs/visuals.md'); const packagedPaths = ['README.md', 'LICENSE', ...(packageJson.files ?? [])]; - it('keeps package metadata, release manifest, and changelog on 2.2.0', () => { - expect(packageJson.version).toBe('2.2.0'); - expect(releaseManifest['.']).toBe('2.2.0'); - expect(changelog).toContain('## [2.2.0]'); + it('keeps package metadata, release manifest, and changelog aligned', () => { + expect(releaseManifest['.']).toBe(expectedVersion); + expect(changelog).toContain(`## [${expectedVersion}]`); expect(changelog).not.toContain('## Unreleased'); }); @@ -114,8 +114,8 @@ describe('release truth surfaces', () => { expect(visualsDoc).toContain('Historical snapshot'); }); - it('keeps the manual publish fallback aligned to v2.2.0', () => { - expect(workflow).toContain("description: 'Tag to publish (e.g. v2.2.0)'"); - expect(workflow).toContain("default: 'v2.2.0'"); + it('keeps the manual publish fallback aligned to the package version', () => { + expect(workflow).toContain(`description: 'Tag to publish (e.g. v${expectedVersion})'`); + expect(workflow).toContain(`default: 'v${expectedVersion}'`); }); });