Problem — server/services/digital-twin.js (136 lines) and server/services/identity.js (78 lines) are pure re-export barrels over their digital-twin-*.js / identity/*.js leaves. Three leaf modules import a symbol back through the barrel instead of from the module that declares it, which closes three static ESM cycles spanning five modules. As with any static cycle, whichever member evaluates first sees undefined for the others' bindings — so adding a top-level const derived from an imported value anywhere in the ring turns into a boot-time TDZ crash, and the modules become impossible to load in isolation in a test.
Evidence — the three back-edges, each importing a symbol the barrel does not declare:
// server/services/taste-questionnaire.js:21
import { digitalTwinEvents } from './digital-twin.js'; // declared in digital-twin-meta.js
// server/services/digital-twin-export.js:6-7
import { getTasteProfile } from './taste-questionnaire.js';
import { getChronotype, getLongevity, getGoals } from './identity.js'; // declared in identity/{chronotype,longevity,goals}.js
// server/services/digital-twin-avatar-bio.js:21
import { getGoals } from './identity.js'; // declared in identity/goals.js
The barrels only forward:
// server/services/digital-twin.js:25-32
export { digitalTwinEvents, loadMeta, saveMeta, updateMeta, updateSettings } from './digital-twin-meta.js';
// server/services/identity.js:27-37
export { computeChronotype, ..., getChronotype, ... } from './identity/chronotype.js';
export { getLongevity, deriveLongevity } from './identity/longevity.js';
// server/services/identity.js:40-69
export { ..., getGoals, ... } from './identity/goals.js';
// server/services/identity.js:78
export { getIdentityStatus } from './identity/status.js';
A static import-graph scan of server/ (comments stripped, import() ignored) reports exactly:
taste-questionnaire.js -> digital-twin.js -> digital-twin-export.js -> taste-questionnaire.js
identity.js -> identity/status.js -> taste-questionnaire.js -> digital-twin.js -> digital-twin-export.js -> identity.js
identity.js -> identity/status.js -> taste-questionnaire.js -> digital-twin.js -> digital-twin-avatar-bio.js -> identity.js
All three disappear if the three leaves name their declaring modules. server/services/identity/status.js:2 importing ../taste-questionnaire.js is fine on its own — it is only a cycle because of the back-edges above.
Plan
server/services/taste-questionnaire.js:21 → import { digitalTwinEvents } from './digital-twin-meta.js';.
server/services/digital-twin-export.js:7 → replace the single ./identity.js import with three declaring-module imports: import { getChronotype } from './identity/chronotype.js';, import { getLongevity } from './identity/longevity.js';, import { getGoals } from './identity/goals.js';.
server/services/digital-twin-avatar-bio.js:21 → import { getGoals } from './identity/goals.js';.
- Leave both barrels and every other importer of them untouched. Decision: fix the three leaf edges rather than move code — the barrels are legitimate public entry points for callers outside the cluster (routes, tools); only members of the cluster must not consume them. This is the same rule
server/services/agentOrchestrator.js documents for the agent cluster.
- Re-run the static scan; confirm zero cycles remain containing
identity.js or digital-twin.js.
Tests
- Add
server/services/twinImportCycles.test.js modeled on the buildStaticGraph() helper in server/services/agentImportCycles.test.js: build the static graph of server/services and assert no cycle contains identity.js, digital-twin.js, taste-questionnaire.js, digital-twin-export.js, or digital-twin-avatar-bio.js. This uniquely catches a future re-introduction of a barrel back-edge, which no behavior test would notice until a boot-order change surfaced it.
- No behavior tests needed:
getTasteProfile, getChronotype, getLongevity, getGoals, and digitalTwinEvents are already exercised through their existing callers; only the import specifier changes.
Acceptance criteria
Out of scope — restructuring the barrels themselves; identity/status.js's dependency on taste-questionnaire.js; the cos.js and meatspacePost.js cycles (separate issues).
Filed by a /do:better --scan-only --issues audit (2026-09-01). Category: architecture · Severity: medium · Files: server/services/taste-questionnaire.js:21, server/services/digital-twin-export.js:6, server/services/digital-twin-export.js:7, server/services/digital-twin-avatar-bio.js:21
All labels already exist in the repo; do NOT create labels. Never add planner:* labels.
Problem —
server/services/digital-twin.js(136 lines) andserver/services/identity.js(78 lines) are pure re-export barrels over theirdigital-twin-*.js/identity/*.jsleaves. Three leaf modules import a symbol back through the barrel instead of from the module that declares it, which closes three static ESM cycles spanning five modules. As with any static cycle, whichever member evaluates first seesundefinedfor the others' bindings — so adding a top-levelconstderived from an imported value anywhere in the ring turns into a boot-time TDZ crash, and the modules become impossible to load in isolation in a test.Evidence — the three back-edges, each importing a symbol the barrel does not declare:
The barrels only forward:
A static import-graph scan of
server/(comments stripped,import()ignored) reports exactly:All three disappear if the three leaves name their declaring modules.
server/services/identity/status.js:2importing../taste-questionnaire.jsis fine on its own — it is only a cycle because of the back-edges above.Plan
server/services/taste-questionnaire.js:21→import { digitalTwinEvents } from './digital-twin-meta.js';.server/services/digital-twin-export.js:7→ replace the single./identity.jsimport with three declaring-module imports:import { getChronotype } from './identity/chronotype.js';,import { getLongevity } from './identity/longevity.js';,import { getGoals } from './identity/goals.js';.server/services/digital-twin-avatar-bio.js:21→import { getGoals } from './identity/goals.js';.server/services/agentOrchestrator.jsdocuments for the agent cluster.identity.jsordigital-twin.js.Tests
server/services/twinImportCycles.test.jsmodeled on thebuildStaticGraph()helper inserver/services/agentImportCycles.test.js: build the static graph ofserver/servicesand assert no cycle containsidentity.js,digital-twin.js,taste-questionnaire.js,digital-twin-export.js, ordigital-twin-avatar-bio.js. This uniquely catches a future re-introduction of a barrel back-edge, which no behavior test would notice until a boot-order change surfaced it.getTasteProfile,getChronotype,getLongevity,getGoals, anddigitalTwinEventsare already exercised through their existing callers; only the import specifier changes.Acceptance criteria
taste-questionnaire.js,digital-twin-export.js,digital-twin-avatar-bio.jsstatically imports./digital-twin.jsor./identity.js.server/reports zero cycles containingidentity.jsordigital-twin.js.cd server && npm testpasses.Out of scope — restructuring the barrels themselves;
identity/status.js's dependency ontaste-questionnaire.js; thecos.jsandmeatspacePost.jscycles (separate issues).Filed by a
/do:better --scan-only --issuesaudit (2026-09-01). Category:architecture· Severity:medium· Files:server/services/taste-questionnaire.js:21, server/services/digital-twin-export.js:6, server/services/digital-twin-export.js:7, server/services/digital-twin-avatar-bio.js:21All labels already exist in the repo; do NOT create labels. Never add
planner:*labels.