Spec: Fowler-style refactoring review (cloud agent, 2026-08-05). Refactor only — no behavior change.
What to change
lib/enrichment/neon-repository.ts repeats itself, and reimplements policy owned elsewhere:
- Row → domain mapping ×3. The same camelCase +
?? [] / ?? null block appears in listThreadEnrichments (~274–293) and twice in completeJob (~512–531, ~564–583); memory-repository.ts repeats the created-path shape (~193–212). Extract mapEnrichment(row) beside the existing mapJob, with a single shared column list.
- Job SELECT ×6.
SELECT id, idempotency_key, thread_id, basis_revision, … plus its inline Array<{...}> cast appears six times. Extract loadJob(userId, jobId) / loadJobByKey(...).
- Snapshot ordering duplicated. The
basis_revision + 0.5 + index * 0.001 ordering exists in neon listPendingThreads (~219–221) and memory buildSnapshot (~77–87). Share one buildEnrichmentSnapshot.
- Permanent-failure policy drift.
requeueFailed reimplements isPermanentEnrichmentError from lib/enrichment/failures.ts as SQL LIKE clauses (~641–671) with a comment admitting it "mirrors" the TS. Make one module own the policy (filter in TS after fetch, or generate the SQL predicate from the same source).
Out of scope: splitting EnrichmentRepository into job/embedding interfaces; the optional-method cleanup; classification typing (separate ticket).
Acceptance criteria
Spec: Fowler-style refactoring review (cloud agent, 2026-08-05). Refactor only — no behavior change.
What to change
lib/enrichment/neon-repository.tsrepeats itself, and reimplements policy owned elsewhere:?? []/?? nullblock appears inlistThreadEnrichments(~274–293) and twice incompleteJob(~512–531, ~564–583);memory-repository.tsrepeats the created-path shape (~193–212). ExtractmapEnrichment(row)beside the existingmapJob, with a single shared column list.SELECT id, idempotency_key, thread_id, basis_revision, …plus its inlineArray<{...}>cast appears six times. ExtractloadJob(userId, jobId)/loadJobByKey(...).basis_revision + 0.5 + index * 0.001ordering exists in neonlistPendingThreads(~219–221) and memorybuildSnapshot(~77–87). Share onebuildEnrichmentSnapshot.requeueFailedreimplementsisPermanentEnrichmentErrorfromlib/enrichment/failures.tsas SQLLIKEclauses (~641–671) with a comment admitting it "mirrors" the TS. Make one module own the policy (filter in TS after fetch, or generate the SQL predicate from the same source).Out of scope: splitting
EnrichmentRepositoryinto job/embedding interfaces; the optional-method cleanup; classification typing (separate ticket).Acceptance criteria
mapEnrichment(row)used by every enrichment read/write path in the Neon repoisPermanentEnrichmentErrorhas a single authoritative implementation with no mirrored SQL copymise run lintpasses; enrichment specs (tests/enrichment-*.spec.ts) pass unchanged