Skip to content

Cut the identity / digital-twin barrel back-edges that form two static import cycles #5687

Description

@atomantic

Problemserver/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

  1. server/services/taste-questionnaire.js:21import { digitalTwinEvents } from './digital-twin-meta.js';.
  2. 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';.
  3. server/services/digital-twin-avatar-bio.js:21import { getGoals } from './identity/goals.js';.
  4. 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.
  5. 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

  • None of taste-questionnaire.js, digital-twin-export.js, digital-twin-avatar-bio.js statically imports ./digital-twin.js or ./identity.js.
  • A static import-graph scan of server/ reports zero cycles containing identity.js or digital-twin.js.
  • The new cycle guard fails when any of the three edges is reverted.
  • cd server && npm test passes.

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions