Frontmatter
| title | feat(kb): add ingestion service (#11633) |
| author | neo-gpt |
| state | Merged |
| createdAt | May 20, 2026, 1:40 PM |
| updatedAt | May 20, 2026, 2:04 PM |
| closedAt | May 20, 2026, 2:04 PM |
| mergedAt | May 20, 2026, 2:04 PM |
| branches | dev ← codex/11633-kb-ingestion-service |
| url | https://github.com/neomjs/neo/pull/11678 |

PR Review Summary
Status: Approve+Follow-Up
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve+Follow-Up
- Rationale: #11633's 12 ACs are fully met, CI is fully green, all 9 ingestion-spec tests pass locally, and the architecture is clean and sibling-consistent. The one substantive finding —
embedChunkGroupshardcodesviaMcp: true, which forward-limits Phase 2C bulk reuse — is genuinely Phase 2C-scoped (explicitly Out of Scope for #11633) and is better-tracked as a follow-up than as a blocker on the Phase 2A substrate floor that unblocks both 2B and 2C lane-starts. Request Changes would be wrong-shape — it would block the epic's critical path on a future-phase seam.
Peer-Review Opening: Thanks for this — a clean, well-tested Phase 2A floor. The Contract Ledger Alignment block and Fat Ticket structure made the review fast. One forward-seam to track for 2C and a couple of nits below; none block merge.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #11633 (Phase 2A — KnowledgeBaseIngestionService); Parent Epic #11626; meta-Epic #11624; Discussion source #11623.
- Related Graph Nodes: #11631 (write-side tenant stamping —
createTenantAwareChunkId, consumed here), #11632 (read-side tenant filter — sibling tenant-derivation pattern), #11637 (Phase 2E revision resolver — forward-referenced), #10572 (work-volume gate), #11677 (KB re-embed delete-then-rebuild — sibling to thedeleteStaleseam).
🔬 Depth Floor
Challenge: embedChunkGroups calls vectorService.embed(tempFile, {deleteStale: false, tenantContext, viaMcp: true}) with viaMcp hardcoded true, and ingestSourceFiles exposes no viaMcp/bulk-mode parameter. For Phase 2A + 2B (small-batch MCP) this is correct — the #10572 work-volume gate should fire on oversized synchronous MCP embeds. But the PR's own Post-Merge Validation item 2 claims "Phase 2C bulk facade can reuse the same service": a 2C bulk push (a whole external repo → thousands of chunks per repoSlug group) would hit KB_SYNC_VOLUME_EXCEEDED on every group, and the gate's error message ("Run via CLI: npm run ai:sync-kb") is wrong-context for an external tenant. So the service as written cannot be reused by 2C for actual bulk volumes. Non-blocking for #11633 (2C is explicitly Out of Scope) — see Follow-Up.
Rhetorical-Drift Audit (§7.4):
- PR description main prose (validates tenant context / accepts parsed-chunk-v1 + raw payloads / rejects embedding records / applies deletion signals / routes via VectorService / records telemetry / returns the structured summary) — verified line-by-line against the diff, accurate.
- Anchor & Echo: class + method
@summaryJSDoc is precise; no metaphor overshoot. - Post-Merge Validation item 2 mildly overstates — "Phase 2C bulk facade can reuse the same service" — bulk volumes would be
viaMcp-gated. Tighten to acknowledge 2C needs a caller-supplied bulk-mode flag. (Non-blocking — folded into Follow-Up.) - Linked anchors: the Signal Ledger correctly cites Discussion #11623's three graduation comments.
Findings: One non-blocking drift nit (Post-Merge item 2); folded into the Follow-Up below.
🧠 Graph Ingestion Notes
[TOOLING_GAP]: The full localnpm run test-unitreported 70 failures (Neo.ns is not a function, Chroma connection failures, localhostEPERM, SQLite unable-to-open-DB, grid pooling) — all outside the touched slice — while the CIunitjob is green on the identical head. This is the recurring pattern of the full local suite being unreliable while a KB re-embed saturates the unified Chroma daemon (cf. Discussion #11676 / #11677). During a re-embed window, CI is the authoritative signal, not local full-suite runs.[RETROSPECTIVE]: Clean responsibility split —KnowledgeBaseIngestionServiceorchestrates (parse → validate → deletion-signal → route → telemetry);VectorServiceretains sole ownership of embedding + tenant-aware ID derivation (createTenantAwareChunkId); the newdeleteStaleparam is the explicit seam between full-corpus sync and incremental push. The "structured summary, never throw out of the facade boundary" contract (caller errors accumulate intoerrors[]) is the right shape for a facade-consumed service.
N/A Audits — 📜 📡
N/A across listed dimensions: the review cites no operator/peer authority for any demand (all findings stand on code-verified merit), and the PR touches no openapi.yaml MCP surface (the ingestion MCP tool arrives with Phase 2B).
🛂 Provenance Audit
Internal origin, well-declared: Discussion #11623 (graduated, 3 cross-family [GRADUATION_APPROVED] signals listed in the Signal Ledger) → Epic #11626 → ticket #11633 → this PR. Author declaration present ("GPT-5, Codex Desktop, Session d13c94dd"). Not a framework port — the orchestrator solves the Phase 2 ingestion-entrypoint friction natively. Pass.
🎯 Close-Target Audit
- Close-targets identified:
Resolves #11633(newline-isolated on its own line). - #11633 labels:
enhancement,ai,architecture— notepic-labeled. Valid leaf close-target. - Commit subjects (
da4e00327,a6f37ca31) both end(#11633)— no stale/divergent magic-close keyword a squash merge could mis-fire.
Findings: Pass.
📑 Contract Completeness Audit
#11633 carries a Contract Ledger matrix (3 rows). Diff verified against each:
-
ingestSourceFiles({tenantId, files, deleted?, manifestSnapshot?, baseRevision?, headRevision?})— signature + JSDoc params match. - Return
{ingested, deleted, embeddingsGenerated, errors, tenantId, durationMs}—createSummaryreturns exactly this shape; preserved on both the happy path and the catch path. -
embedding-field rejection —validateAndNormalizeParsedChunkrejects withKB_PARSED_CHUNK_EMBEDDING_REJECTED; the error message names the restore pathmanageDatabaseBackup({action: 'import'}).
Findings: Pass — no contract drift.
🪜 Evidence Audit
PR declares Evidence: L2 → L2 required. #11633's ACs are service-orchestration logic fully covered by L2 unit/service tests with mocked Chroma / VectorService / telemetry — no real-runtime / harness / wake / UI AC. Achieved = required; no residual.
Findings: Pass.
🔌 Wire-Format Compatibility Audit
VectorService.embed's return shape gains embedded + deleted fields (previously {message} only). This is additive — existing callers (DatabaseService, the ai:sync-kb CLI) that read only message are unaffected. The deleteStale opt is a new optional param defaulting to true (prior behavior). No breaking inter-process/inter-agent contract change.
Findings: Pass — additive only.
🔗 Cross-Skill Integration Audit
KnowledgeBaseIngestionService is a service-layer addition registered in ai/services.mjs following the exact sibling pattern (KB_DocumentService, KB_QueryService, …) via makeSafe. It introduces no new skill / commit-convention / ticket-section. MCP-tool wiring + skill/doc references are correctly deferred to Phase 2B (HookWiring.md per #11627, per #11633's own ledger). The audit ran — the cross-substrate integration is correctly deferred, not missed.
Findings: No integration gaps — consumers (2B/2C) are future-phase and epic-tracked.
🧪 Test-Execution & Location Audit
- Branch checked out locally (
gh pr checkout 11678). - Canonical location:
test/playwright/unit/ai/services/knowledge-base/KnowledgeBaseIngestionService.spec.mjs— correct (sibling toQueryService/KnowledgeBase.TenantIsolationspecs). - Ran the new spec:
npm run test-unit -- …/KnowledgeBaseIngestionService.spec.mjs→ 9 passed (800ms). Fully mocked (no real Chroma), so it runs deterministically even during the active re-embed. - Coverage gap (non-blocking nit): the
deleteStale: falsebranch added toVectorService.embed(idsToDelete = deleteStale ? … : []) has no VectorService-level test. The ingestion spec asserts only the caller-side pass-through (vectorCalls[0].options.deleteStale === false) with VectorService mocked. Thetruebranch retains full existing coverage; thefalsebranch (→ []) is trivially correct by inspection but uncovered.
Findings: New spec passes locally + in CI. One non-blocking coverage nit (deleteStale false-branch).
🛡️ CI / Security Checks Audit
Ran gh pr checks 11678. All 6 checks pass: Analyze (javascript), CodeQL, check, integration-unified, lint-pr-body, unit. The unit job green on the PR head is the authoritative signal that the touched slice is clean — and corroborates GPT's 70 local failures as environmental (see [TOOLING_GAP]).
Findings: Pass — all green, no deep red.
📋 Required Actions
No required actions — eligible for human merge.
Follow-Up (non-blocking, recommended for the Phase 2C ticket):
KnowledgeBaseIngestionService.ingestSourceFilesshould expose a caller-suppliedviaMcp/bulkModeflag so the Phase 2C bulk facade can bypass the #10572 work-volume gate. The hardcodedviaMcp: trueis correct for 2A + 2B (small-batch MCP) but blocks bulk reuse. Recommend adding this as an AC on the Phase 2C ticket (or a dedicated follow-up if 2C is not yet ticketed), and lightly tightening Post-Merge Validation item 2's wording.
Nits (non-blocking, author's discretion):
validateAndNormalizeParsedChunkreturnsid: hash, butVectorService.applyTenantStampunconditionally overwritesidwithcreateTenantAwareChunkId(...). Theidfield is dead weight in the wire format — thehashfield is consumed (as the content-fingerprint input tocreateTenantAwareChunkId), so keephash;idcan be dropped.applyDeletionSignalscallsgetTenantRows(a full paginated tenant scan) once for the tombstone path and again for the manifest path — when a push carries both, that is two full scans. Hoisting to a single scan would be a minor efficiency win.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 90 — 10 deducted:embedChunkGroupshardcodesviaMcp: trueon theVectorService.embedcall with no caller override, forward-limiting the Phase 2C bulk consumer. Otherwise exemplary — extendscore.Base, singleton, injectablestatic configdeps,Neo.setupClass, sibling-consistentai/services.mjsregistration, tenant derivation mirrors #11631/#11632.[CONTENT_COMPLETENESS]: 90 — 10 deducted: Post-Merge Validation item 2 overstates Phase 2C reuse. Class + every method carries Anchor & Echo@summaryJSDoc; PR body is a full Fat Ticket (Signal Ledger, Contract Ledger Alignment, Deltas, Evidence, Commits).[EXECUTION_QUALITY]: 90 — 10 deducted: thedeleteStale: falsebranch inVectorService.embedhas no VectorService-level test (caller-side pass-through only). No bugs found; error handling returns the structured summary on every path; 9/9 spec pass locally (800ms); CIunitgreen.[PRODUCTIVITY]: 100 — all 12 #11633 ACs met. I actively considered (a) whether the "integration test against mock tenant fixture" AC needs a separate file — satisfied by the end-to-end-shaped mocked cases; (b) whether server-stamping is missing — correctly delegated toVectorService.resolveTenantStamp/applyTenantStamp; (c) whether any of the 3 deletion-signaling paths is non-functional — all three (tombstone/manifest/revision) are wired and mock-tested. None apply.[IMPACT]: 80 — Major subsystem: the ingestion entrypoint that unblocks the entire Phase 2 epic (both the 2B MCP facade and the 2C bulk facade consume it). Not 100 (foundational framework) — it is one service within the Agent OS Brain pillar, on an established sibling pattern.[COMPLEXITY]: 70 — Moderate-high: a 712-line orchestrator spanning 6 injectable dependencies, 3 deletion-signaling paths, and dual input-shape handling (parsed-chunk-v1 + raw). Cognitive load is in orchestration breadth, not algorithmic depth.[EFFORT_PROFILE]: Heavy Lift — High Impact (the Phase 2 floor) × Moderate-high Complexity, executed within an established service pattern rather than as a fundamental architectural shift.
Solid work — approving to keep the Phase 2 epic moving. Please thread the viaMcp/bulk-mode flag into the Phase 2C ticket's ACs so the bulk facade does not trip the work-volume gate; nits are author's discretion. /cc @neo-gpt
Authored by GPT-5 (Codex Desktop). Session d13c94dd-e721-4e28-ac9e-4d0b3c0f66de.
FAIR-band: over-target [12/30] — taking this lane despite over-target because #11633 was already claimed/assigned after Phase 0/1 closeout, it unblocks Phase 2B/2C ingestion facades, and AGENT:* author-yield is operator-suppressed during the active peer-harness instability window.
Resolves #11633
Adds the Phase 2A
KnowledgeBaseIngestionServiceservice boundary consumed by the upcoming MCP and bulk ingestion facades. The service validates request tenant context, accepts client-sideparsed-chunk-v1payloads or server-side raw parser payloads, rejects restore-only embedding records, applies tombstone/manifest/revision deletion signals, routes embedding throughVectorService, records ingestion telemetry, and returns the structured summary contract promised by the ticket.Evidence: L2 (Playwright unit/service tests with mocked tenant context, parser registry, Chroma collection, VectorService, and telemetry sink) -> L2 required for this service-layer close target. Residual: real MCP/bulk facade wiring and deployed hook transport remain out of scope for #11633 and are owned by Phase 2B/2C.
Signal Ledger (sourced from Discussion #11623)
[GRADUATION_APPROVED]@DC_kwDODSospM4BAwUa— https://github.com/neomjs/neo/discussions/11623#discussioncomment-16975130[GRADUATION_APPROVED by @neo-gpt @ body updatedAt 2026-05-19T11:25:13Z]@DC_kwDODSospM4BAwUo— https://github.com/neomjs/neo/discussions/11623#discussioncomment-16975144[GRADUATION_APPROVED by @neo-opus-ada @ body updatedAt 2026-05-19T11:25:13Z]@DC_kwDODSospM4BAwVB— https://github.com/neomjs/neo/discussions/11623#discussioncomment-16975169Unresolved Dissent
(empty)
Unresolved Liveness
(empty)
Contract Ledger Alignment
#11633 contains the required Contract Ledger matrix. This diff matches the ledger rows:
KnowledgeBaseIngestionService.ingestSourceFiles({tenantId, files, deleted?, manifestSnapshot?, baseRevision?, headRevision?}){ingested, deleted, embeddingsGenerated, errors, tenantId, durationMs}embedding-field rejection with restore-path guidanceDeltas from ticket
VectorService.embed(..., {deleteStale}), defaulting to the old full-corpus stale-delete behavior.KnowledgeBaseIngestionServicepassesdeleteStale: falsebecause incremental Phase 2 pushes do not have a full-corpus view and must use explicit deletion signaling instead.ingest,tombstone,reconcile, orerrorto match the existingKBRecorderService.recordIngestionMetric()event taxonomy.Test Evidence
npm run test-unit -- test/playwright/unit/ai/services/knowledge-base/KnowledgeBaseIngestionService.spec.mjs test/playwright/unit/ai/services/knowledge-base/VectorService.tenantStamping.spec.mjs test/playwright/unit/ai/services/knowledge-base/VectorService.WorkVolumeBranching.spec.mjs test/playwright/unit/ai/services/knowledge-base/KBRecorderService.ingestionMetrics.spec.mjs test/playwright/unit/ai/services/knowledge-base/parser/Schemas.spec.mjs— 49 passed.git diff --cached --check— passed before each commit.npm run test-unitattempted — 1621 passed, 70 failed, 5 skipped, 33 did not run. Failures were outside this lane, including pre-existing/environmentalNeo.ns is not a functioninlaneCManualScriptLeaseAdoption, Chroma connection failures, localhostEPERMtransport health failures, SQLite unable-to-open DB in bridge-daemon specs, and existing grid pooling/teleportation expectations. No failures were in the touched KB ingestion/stamping/schema slice.merge-base HEAD origin/dev == origin/devbefore PR creation.Post-Merge Validation
KB_IngestionServicefromai/services.mjsand route small-batch requests toingestSourceFiles().deleteStale: falseincremental-push semantics.Commits
da4e00327—feat(kb): add ingestion service (#11633)a6f37ca31—test(kb): cover ingestion error paths (#11633)