Version: @kaelio/ktx@0.16.0. Related to (and best done after) the extractedAt resume-hash bug.
Motivation
Description enrichment caches at whole-batch granularity: the resume hash covers the entire snapshot's tables[], and the resume record is gated all-or-nothing on it. So changing enabled_tables β or any single table's columns β re-keys the batch and re-sends every table to the LLM, discarding expensive per-table descriptions that didn't change.
Verified (built from source, extractedAt bug already fixed so it's isolated): enabled_tables=4 β ingest, then add 2 tables β ingest regenerates all 6; the 4 unchanged tables are reworded, not reused. On a 300-table warehouse, adding one table means paying to re-describe 300.
This matters most on the rate-limited claude-code backend, where re-describing unchanged tables burns the Pro/Max window.
Current design (for context)
Code comments describe two layers: spec-19 = whole-stage inputHash gate; spec-20 = a per-table resume record. Per-table recovery already exists β generateDescriptions recovers tables present in the record and only enriches the remainder β but only while the whole-stage inputHash matches, i.e. resuming an interrupted run of the same table set. A table-set change flips the hash and discards the whole record. So per-table reuse across schema/scope changes is not supported today.
The opportunity: an intended seam already exists but is unwired
KtxDescriptionGenerator already has a complete per-table and per-column and per-connection cache seam β KtxDescriptionCachePort (description-generation.ts), with get-before / set-after around every LLM call (table 567/657, connection 949/997, column 1022/1103).
It has no production implementation β only a test fake β and is never injected (local-enrichment.ts constructs the generator with no cache). That idle seam also contradicts the repo's own docs/code-design.md:
MUST NOT add an optional dep-injection slot (deps.X ?? defaultX) unless at least one test exercises the production default.
This strongly suggests per-table caching was intended to flow through this port and was left unfinished.
Proposal
Implement and wire a production KtxDescriptionCachePort backed by the existing persistent ContentResultCache (the same (namespace, scopeKey, inputHash) store findCompletedStage uses):
- Key each table/column on its content β the referenced table/column's structure +
llmIdentity β under a namespace like scan:description-table / scan:description-column.
- Inject it at the single generator construction site in
local-enrichment.ts.
Result: every table/column description is content-addressed and reused across runs regardless of what else is in the batch β surviving scope changes, unrelated schema drift, and interruptions β in one place, at the seam already designed for it. Per-column coverage comes for free.
Design caveat to get right
The port's key methods currently receive only a KtxTableRef (catalog/db/name = identity), not the table's structure. A naive identity-keyed implementation would serve stale descriptions after a column change. The production implementation must derive a content key (hold the current snapshot; hash the referenced table/column's structure), or the port's key contract should be widened to pass structure. This is the one correctness-sensitive part.
Alternative (heavier)
Extend the spec-20 resume record to store a per-table content hash and recover per table (validated against the current structure). This works but adds a parallel mechanism in the resume/flush path; wiring the existing port is the more localized, intent-aligned option and also covers column descriptions.
Related bug: #347
Version:
@kaelio/ktx@0.16.0. Related to (and best done after) theextractedAtresume-hash bug.Motivation
Description enrichment caches at whole-batch granularity: the resume hash covers the entire snapshot's
tables[], and the resume record is gated all-or-nothing on it. So changingenabled_tablesβ or any single table's columns β re-keys the batch and re-sends every table to the LLM, discarding expensive per-table descriptions that didn't change.Verified (built from source,
extractedAtbug already fixed so it's isolated):enabled_tables=4 β ingest, then add 2 tables β ingest regenerates all 6; the 4 unchanged tables are reworded, not reused. On a 300-table warehouse, adding one table means paying to re-describe 300.This matters most on the rate-limited
claude-codebackend, where re-describing unchanged tables burns the Pro/Max window.Current design (for context)
Code comments describe two layers:
spec-19= whole-stageinputHashgate;spec-20= a per-table resume record. Per-table recovery already exists βgenerateDescriptionsrecovers tables present in the record and only enriches the remainder β but only while the whole-stageinputHashmatches, i.e. resuming an interrupted run of the same table set. A table-set change flips the hash and discards the whole record. So per-table reuse across schema/scope changes is not supported today.The opportunity: an intended seam already exists but is unwired
KtxDescriptionGeneratoralready has a complete per-table and per-column and per-connection cache seam βKtxDescriptionCachePort(description-generation.ts), with get-before / set-after around every LLM call (table 567/657, connection 949/997, column 1022/1103).It has no production implementation β only a test fake β and is never injected (
local-enrichment.tsconstructs the generator with nocache). That idle seam also contradicts the repo's owndocs/code-design.md:This strongly suggests per-table caching was intended to flow through this port and was left unfinished.
Proposal
Implement and wire a production
KtxDescriptionCachePortbacked by the existing persistentContentResultCache(the same(namespace, scopeKey, inputHash)storefindCompletedStageuses):llmIdentityβ under a namespace likescan:description-table/scan:description-column.local-enrichment.ts.Result: every table/column description is content-addressed and reused across runs regardless of what else is in the batch β surviving scope changes, unrelated schema drift, and interruptions β in one place, at the seam already designed for it. Per-column coverage comes for free.
Design caveat to get right
The port's key methods currently receive only a
KtxTableRef(catalog/db/name = identity), not the table's structure. A naive identity-keyed implementation would serve stale descriptions after a column change. The production implementation must derive a content key (hold the current snapshot; hash the referenced table/column's structure), or the port's key contract should be widened to pass structure. This is the one correctness-sensitive part.Alternative (heavier)
Extend the
spec-20resume record to store a per-table content hash and recover per table (validated against the current structure). This works but adds a parallel mechanism in the resume/flush path; wiring the existing port is the more localized, intent-aligned option and also covers column descriptions.Related bug: #347