“Generate a query” ends up written three times: once as a chat tool, once inside the dashboard builder, once for a page’s form. Each version grounds a little differently, and the fixes land in one and not the others.

Split by who reads the result:

  Who reads it Example
Tool the model, in its context lookup returns rows the model summarizes
Workflow the user, as one card. The model gets a short summary query makes a query card; dashboard makes a dashboard
Engine code. Never exposed by itself the evidence engine: questions in, grounded queries and rows out
                     ┌──────────── engine: answerQuestion / collectEvidence ────────────┐
                     │   one way to turn a question into a checked query and rows  │
                     └──────▲──────────────────▲─────────────────────▲────────────┘
                            │                  │                     │
        tool: lookup ─────┘   workflow: query┘     workflows: dashboard, goal page, form fields
        (model reads rows)      (user gets card)     (call the engine directly)

Rules

  1. One door. Outside the engine’s folder, code imports only its index.ts. An import-boundary lint rule fails the build otherwise (dependency-cruiser, ESLint no-restricted-imports, Nx module boundaries, whatever you already run).
  2. Workflows call engines, never tools or other workflows.
  3. Every composer has the same shape: plan (one model call) → evidence (one parallel batch) → write (one model call). An open-ended tool loop inside a workflow is the thing to remove.
  4. Tools return to the model, workflows return to the user. Anything else the user should see goes on a step as an inspectable.
// e.g. a dependency-cruiser rule
{
  name: "query-engine-one-door",
  severity: "error",
  from: { path: "^src/", pathNot: ["^src/engines/query/", "\\.test\\.ts$"] },
  to:   { path: "^src/engines/query/", pathNot: ["^src/engines/query/index\\.ts$"] },
}

Why it works

A grounding fix lands once and every exposure gets it. And the rule is enforced by the build, not by memory.