LearnNewsExamplesServices
Frontmatter
id16474
titleThe SEO stage imports the whole services barrel to read labels, so Body-tier CI needs chromadb
stateClosed
labels
bugaibuild
assigneesneo-opus-ada
createdAtAug 4, 2026, 1:28 AM
updatedAtAug 4, 2026, 1:48 AM
githubUrlhttps://github.com/neomjs/neo/issues/16474
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 4, 2026, 1:48 AM

The SEO stage imports the whole services barrel to read labels, so Body-tier CI needs chromadb

neo-opus-ada
neo-opus-ada commented on Aug 4, 2026, 1:28 AM

Context

The Data Sync Pipeline has been failing on dev since the last success at 2026-08-02T20:13:14Z — 9 consecutive runs, tracked by the standing alarm #16428 (auto-maintained, self-closing on recovery; not a close-target).

Run 30861274995, stage content indexes and SEO:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'chromadb'
  imported from /home/runner/work/neo/neo/ai/services/knowledge-base/ChromaManager.mjs

Live latest-open sweep: latest 20 open issues at 2026-08-03T23:27:44Z; no equivalent found. A2A in-flight claim sweep at the same minute (15 messages, all read-states): no overlapping [lane-claim].

The Problem

A docs/SEO job requires a vector-database client, and it does so to read GitHub labels.

The chain, verified by reading each hop:

buildScripts/dataSyncPipeline.mjs      stage "content indexes and SEO"
└─ buildScripts/docs/rebuildContentIndexesAndSeo.mjs --include-labels
   └─ :66  await import('./index/labels.mjs')          ← only this flag reaches it
      └─ buildScripts/docs/index/labels.mjs:5
         import {GH_LabelService} from '../../../ai/services.mjs'
         └─ ai/services.mjs                            ← 66-import EAGER barrel
            └─ ai/services/knowledge-base/ChromaManager.mjs
               └─ 'chromadb'

The stage needs one methodGH_LabelService.listLabels() — and pays for the entire Brain services graph.

The over-import is old; #16389 only stopped paying for it. The two-path install tier (#16364 / #16389, merged 2026-08-02T21:42:15Z, i.e. after the last green run) moved chromadb into package.brain.json. The Body tier declares zero dependencies and 38 devDependencies, and already ships everything this script genuinely needs — fs-extra, commander, gray-matter, fast-glob, js-yaml, dotenv. Nothing about the split is wrong; it exposed a latent coupling.

The Architectural Reality

  • ai/services.mjs is a barrel with 66 top-level imports spanning every MCP server. Importing any one export constructs all of them.
  • ai/services/github-workflow/LabelService.mjs imports only src/core/Base.mjs, GraphqlService.mjs, ai/mcp/server/github-workflow/config.mjs and queries/labelQueries.mjsnothing Brain-tier.
  • The barrel wraps services in makeSafe(service, spec) (ai/services.mjs:114), which Zod-validates and marshals input arguments from an object into positional params.

makeSafe is a provable no-op at this call site. list_labels in ai/mcp/server/github-workflow/openapi.yaml:135 declares no parameters and no requestBody, and the raw method is async listLabels() — zero params, reads config internally. The call is await GH_LabelService.listLabels() with no arguments, so the wrapper resolves to zodSchema.parse({}){}, paramNames[], then currentMethod.call(service). Identical to the unwrapped call. No validation is lost by importing directly.

But a bare direct import does not work, and this is the part worth recording:

ReferenceError: Neo is not defined
  at src/core/Compare.mjs:166

The barrel was also performing the Neo namespace bootstrap. LabelService is a Neo.setupClass class, so the entry point must populate globalThis.Neo first. Verified: import Neo from 'src/Neo.mjs' + import * as core from 'src/core/_export.mjs' is sufficientInstanceManager is not required here — and yields className = Neo.ai.services.github-workflow.LabelService with listLabels callable.

The Fix

In buildScripts/docs/index/labels.mjs, replace the barrel import with the narrow one plus the entry-point bootstrap:

import Neo             from '../../../src/Neo.mjs';
import * as core       from '../../../src/core/_export.mjs';
import GH_LabelService from '../../../ai/services/github-workflow/LabelService.mjs';

LabelService.mjs default-exports Neo.setupClass(LabelService), so this is a default import, not a named one.

Acceptance Criteria

  • buildScripts/docs/index/labels.mjs no longer imports ai/services.mjs, and its transitive closure does not reach chromadb.
  • node ./buildScripts/docs/rebuildContentIndexesAndSeo.mjs --include-labels produces the same label index as before the change — the output is what matters, not merely that it runs.
  • The script runs against a Body-tier install (no package.brain.json deps present). This is the acceptance shape: a green run on a machine that already has chromadb proves nothing.
  • GH_LabelService.listLabels() still returns the same shape; the makeSafe no-op equivalence is asserted rather than assumed.
  • Post-merge: the Data Sync Pipeline reaches the content indexes and SEO stage without ERR_MODULE_NOT_FOUND, and #16428 self-closes on recovery.

Out of Scope

  • Restructuring ai/services.mjs. Making the barrel lazy would fix this class everywhere and is the better long-term answer, but it changes a surface every MCP server imports. Narrow this consumer first; file the barrel work separately if the pattern recurs.
  • The install-tier split. #16389 is correct and is not being reverted or softened.
  • Whether other Body-tier consumers import the barrel. Worth a census, but not this ticket — see Related.

Avoided Traps

  • Do not add chromadb to the Body tier. That reverses #16389 to satisfy an import that should not exist, and every Body install then carries a vector-DB client.
  • Do not drop the Neo bootstrap on the assumption a direct import is self-sufficient. It is not — measured above, it throws at src/core/Compare.mjs:166.
  • Do not assume makeSafe is always removable. It is a no-op here because the operation takes no arguments. For a method with parameters it also marshals object → positional, so a naive narrowing would change the calling convention.

Related

  • #16428 — standing data-sync alarm (auto-maintained; do not use as a close-target).
  • #16364 / #16389 — the two-path install tier that exposed this.

Origin Session ID: eeacb603-97f1-4241-9b2f-3a542cab6d2c

Retrieval Hint: query_raw_memories("services.mjs barrel eager import chromadb data-sync SEO labels Body tier")