From 32521bcb72be757f425ce249c4cd1275a17766f1 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 18 Apr 2026 13:41:26 -0700 Subject: [PATCH] Phase 7 Step 3d (leaf 1/5): thread config into consensus-extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First leaf conversion in the singleton-audit ratcheting sequence. Drops consensus-extraction.ts from the singleton importer set (33 → 32). - src/services/consensus-extraction.ts: dropped the `import { config }` singleton binding. `consensusExtractFacts` now takes config as a required parameter typed `ConsensusExtractionConfig` (Pick-style: consensusExtractionEnabled, consensusExtractionRuns, chunkedExtractionEnabled). The new type is co-located and exported so orchestration files importing it get the narrow contract. - src/services/memory-ingest.ts: both call sites (performIngest line 66, workspace line 191) now pass `deps.config` explicitly. Already had deps.config in scope — no signature ripple into performIngest/ performWorkspaceIngest callers. - src/services/memory-service-types.ts: IngestRuntimeConfig gains chunkedExtractionEnabled, consensusExtractionEnabled, consensusExtractionRuns so deps.config satisfies the new parameter type structurally. - src/__tests__/config-singleton-audit.test.ts: MAX_SINGLETON_IMPORTS ratcheted 33 → 32 with updated justification comment referencing this step. Defaulting location: at the composition root (runtime-container builds deps.config once from the singleton), not inside the leaf module — so the leaf file itself no longer binds `config`, which is how it drops out of the audit. Any alternative where the leaf kept a "if (!config) fall back to singleton" path would have left the import in place and made the ratcheting theatre. 963/963 tests pass. tsc --noEmit clean. fallow --no-cache 0 above threshold. Plan: phase7-v1-parity item 3d, four leaves remaining (write-security.ts, cost-telemetry.ts, embedding.ts, llm.ts). Co-authored-by: Claude Opus 4.7 (1M context) --- src/__tests__/config-singleton-audit.test.ts | 10 +++++----- src/services/consensus-extraction.ts | 16 +++++++++++++++- src/services/memory-ingest.ts | 4 ++-- src/services/memory-service-types.ts | 3 +++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/__tests__/config-singleton-audit.test.ts b/src/__tests__/config-singleton-audit.test.ts index be2a772..3da3bbd 100644 --- a/src/__tests__/config-singleton-audit.test.ts +++ b/src/__tests__/config-singleton-audit.test.ts @@ -24,14 +24,14 @@ const SRC = resolve(__dirname, '..'); * Maximum allowed non-test source files that bind the runtime config * singleton value from config.js. Ratchet this DOWN after each * config-threading PR lands. - * Current baseline: 33 files after Phase 4 ingest extractions removed - * memory-ingest.ts, memory-storage.ts, memory-audn.ts, and - * memory-lineage.ts from the singleton importer set. Remaining count - * includes the index.ts re-export of config. + * Current baseline: 32 files after Phase 7 Step 3d-consensus-extraction + * dropped consensus-extraction.ts from the singleton importer set by + * accepting its config as an explicit parameter. Remaining count includes + * the index.ts re-export of config. * Includes multi-import forms (e.g. `import { config, updateRuntimeConfig }`) * and re-exports (e.g. `export { config } from`). */ -const MAX_SINGLETON_IMPORTS = 33; +const MAX_SINGLETON_IMPORTS = 32; /** * Matches any import or re-export that binds the `config` value (not diff --git a/src/services/consensus-extraction.ts b/src/services/consensus-extraction.ts index 02fcf85..af2f6f4 100644 --- a/src/services/consensus-extraction.ts +++ b/src/services/consensus-extraction.ts @@ -11,7 +11,6 @@ * N× extraction API calls. */ -import { config } from '../config.js'; import { extractFacts, type ExtractedFact } from './extraction.js'; import { cachedExtractFacts } from './extraction-cache.js'; import { chunkedExtractFacts } from './chunked-extraction.js'; @@ -20,6 +19,17 @@ import { classifyNetwork } from './memory-network.js'; const SIMILARITY_THRESHOLD = 0.90; +/** + * Config subset consumed by consensusExtractFacts. Kept narrow so callers + * only need to thread through the fields the function actually reads — + * a `Pick` of the deps.config bundle. + */ +export interface ConsensusExtractionConfig { + consensusExtractionEnabled: boolean; + consensusExtractionRuns: number; + chunkedExtractionEnabled: boolean; +} + interface FactWithEmbedding { fact: ExtractedFact; embedding: number[]; @@ -30,9 +40,13 @@ interface FactWithEmbedding { * - "consensus" (default): Keep only facts that appear in majority of runs. * - "union": Keep all unique facts found across all runs (improves recall). * Falls back to single extraction when consensus is disabled. + * + * Config is passed explicitly — consumers thread their `deps.config` + * through. This module no longer reads the module-level config singleton. */ export async function consensusExtractFacts( conversationText: string, + config: ConsensusExtractionConfig, ): Promise { if (!config.consensusExtractionEnabled) { return config.chunkedExtractionEnabled diff --git a/src/services/memory-ingest.ts b/src/services/memory-ingest.ts index 9a6d196..a3ebbc9 100644 --- a/src/services/memory-ingest.ts +++ b/src/services/memory-ingest.ts @@ -63,7 +63,7 @@ export async function performIngest( ): Promise { const ingestStart = performance.now(); const episodeId = await timed('ingest.store-episode', () => deps.stores.episode.storeEpisode({ userId, content: conversationText, sourceSite, sourceUrl })); - const facts = await timed('ingest.extract', () => consensusExtractFacts(conversationText)); + const facts = await timed('ingest.extract', () => consensusExtractFacts(conversationText, deps.config)); const acc = createIngestAccumulator(); const supersededTargets = new Set(); const entropyCtx: EntropyContext = { seenEntities: new Set(), previousEmbedding: null }; @@ -188,7 +188,7 @@ export async function performWorkspaceIngest( workspaceId: workspace.workspaceId, agentId: workspace.agentId, }), ); - const facts = await timed('ws-ingest.extract', () => consensusExtractFacts(conversationText)); + const facts = await timed('ws-ingest.extract', () => consensusExtractFacts(conversationText, deps.config)); const acc = createIngestAccumulator(); const supersededTargets = new Set(); const entropyCtx: EntropyContext = { seenEntities: new Set(), previousEmbedding: null }; diff --git a/src/services/memory-service-types.ts b/src/services/memory-service-types.ts index 7917702..ccecce0 100644 --- a/src/services/memory-service-types.ts +++ b/src/services/memory-service-types.ts @@ -178,8 +178,11 @@ export interface MemoryServiceDeps { export interface IngestRuntimeConfig { audnCandidateThreshold: number; auditLoggingEnabled: boolean; + chunkedExtractionEnabled: boolean; compositeGroupingEnabled: boolean; compositeMinClusterSize: number; + consensusExtractionEnabled: boolean; + consensusExtractionRuns: number; entityGraphEnabled: boolean; entropyGateAlpha: number; entropyGateEnabled: boolean;