godcrm/backend/services/dcd/scopeRouter.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

105 lines
4.7 KiB
JavaScript

/**
* DCD Scope Router (ADR-157) — the "narrow scope BEFORE retrieval" step.
*
* DCD (Domain → Collection → Document) is a retrieval *policy*, not a storage
* model — our liquid substrate (ADR-156) already provides the hierarchy
* physically (Space → Widget/Table/`room` → Row). This module is the explicit
* router that picks the Collection-level scope for a memory query, so retrieval
* is constrained before the semantic search instead of filtered after.
*
* For Hindsight `memory_recall`, the Collection step IS the `room` filter — the
* one that already buys +34% precision when applied before search. So the
* router's whole job here is: query → `room` (or "widen": no room).
*
* IMPLEMENTATION NOTE (Ponytail / ADR-157): this is a deterministic lexical
* router, mirroring `CommandClassifier`. The ADR explicitly allows "a cheap
* model (Haiku) OR a rule-of-thumb threshold". The rule-of-thumb is chosen for
* the prototype because it validates the *contract* — `{ room, confidence }` +
* widen-on-low-confidence — with zero new dependency, zero secret, zero
* latency, and full offline testability. An LLM structured-output backing is a
* drop-in replacement behind this same signature; nothing downstream changes.
*/
// Canonical Hindsight room vocabulary (mirrors the `room` enum documented in
// agent-tools/tool-definitions/memory.js). The router can only ever emit one of
// these or null (= widen to domain).
export const ROOMS = [
'auth', 'pipeline', 'schema', 'tax', 'hr', 'legal', 'compliance',
'infrastructure', 'ui', 'api', 'deployment', 'monitoring', 'agent', 'general',
];
// Keyword hints per room. A hit is a whole-word match of a hint in the query.
// `general` is intentionally absent — it is the "no specific collection" room,
// reached only as the widen target, never as a positive route.
const ROOM_HINTS = {
auth: ['auth', 'login', 'token', 'jwt', 'password', 'oauth', 'session', 'permission', 'permissions'],
pipeline: ['pipeline', 'deal', 'deals', 'stage', 'funnel', 'lead', 'leads'],
schema: ['schema', 'column', 'columns', 'migration', 'field', 'fields'],
tax: ['tax', 'taxes', 'vat', 'invoice', 'invoices', 'accounting'],
hr: ['hr', 'employee', 'employees', 'hire', 'hiring', 'salary', 'payroll', 'vacation'],
legal: ['legal', 'contract', 'contracts', 'nda', 'gdpr', 'license', 'licensing'],
compliance: ['compliance', 'audit', 'audits', 'policy', 'regulation', 'regulations'],
infrastructure: ['infra', 'infrastructure', 'server', 'servers', 'nginx', 'pm2', 'docker', 'vpn', 'host'],
ui: ['ui', 'button', 'css', 'component', 'components', 'frontend', 'react', 'layout', 'widget'],
api: ['api', 'endpoint', 'endpoints', 'route', 'routes', 'rest', 'request', 'response'],
deployment: ['deployment', 'deploy', 'release', 'rollout', 'ci', 'cd', 'build', 'builds'],
monitoring: ['monitor', 'monitoring', 'log', 'logs', 'metric', 'metrics', 'alert', 'alerts', 'telemetry', 'observability'],
agent: ['agent', 'agents', 'prompt', 'prompts', 'llm', 'memory', 'hindsight', 'tool', 'tools'],
};
// Confidence below this floor means "do not trust the route" → widen to domain
// (no room). A single weak keyword hit (0.5) widens; ≥2 distinct hits (0.9)
// applies. Exported so the caller's threshold stays in lockstep with the router.
export const ROUTE_CONFIDENCE_FLOOR = 0.6;
const WORD = /[a-z0-9]+/g;
function tokenize(text) {
return new Set(String(text || '').toLowerCase().match(WORD) || []);
}
/**
* Route a memory query to a Hindsight `room` (the Collection step of DCD).
*
* @param {string} query - the recall query
* @returns {{ room: string|null, confidence: number, reason: string }}
* `room === null` means "widen to domain" — search unscoped. `confidence` is
* in [0,1]; the caller applies the route only when it clears
* `ROUTE_CONFIDENCE_FLOOR`.
*/
export function routeMemoryScope(query) {
const tokens = tokenize(query);
if (tokens.size === 0) {
return { room: null, confidence: 0, reason: 'empty query → widen to domain' };
}
let best = null;
let bestHits = 0;
for (const [room, hints] of Object.entries(ROOM_HINTS)) {
let hits = 0;
for (const h of hints) if (tokens.has(h)) hits += 1;
if (hits > bestHits) {
bestHits = hits;
best = room;
}
}
if (bestHits === 0) {
return { room: null, confidence: 0, reason: 'no room keywords matched → widen to domain' };
}
const confidence = bestHits >= 2 ? 0.9 : 0.5;
if (confidence < ROUTE_CONFIDENCE_FLOOR) {
return {
room: null,
confidence,
reason: `weak match for "${best}" (1 keyword) → widen to domain`,
};
}
return {
room: best,
confidence,
reason: `routed to "${best}" (${bestHits} keyword hits)`,
};
}