DTModule Interface

August 18, 2026 · View on GitHub

Table of Contents

Overview

The DTModule interface is the core contract that all Dethernety modules must implement. It defines the methods for retrieving module metadata, class templates, and security evaluation (exposures/countermeasures).

Source File: packages/dt-module/src/interfaces/module-interface.ts

┌─────────────────────────────────────────────────────────────────────────┐
│                          DTModule Interface                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                    Required Methods                             │    │
│  │  • getMetadata(): DTMetadata                                    │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                    Template Methods (Optional)                  │    │
│  │  • getModuleTemplate(): string                                  │    │
│  │  • getClassTemplate(id, token): string                          │    │
│  │  • getClassGuide(id, token): string                             │    │
│  │  • isContentCallerVariant(): boolean                            │    │
│  │      └ declares template/guide content may vary by caller       │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                  Security Evaluation (Optional)                 │    │
│  │  • getExposures(id, classId, token): Exposure[]                 │    │
│  │  • getCountermeasures(id, classId, token): Countermeasure[]     │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                   Analysis Methods (Optional)                   │    │
│  │  • runAnalysis(id, classId, scope, pubSub, params, token)       │    │
│  │  • startChat(id, classId, scope, question, pubSub, params)      │    │
│  │  • resumeAnalysis(id, classId, input, pubSub)                   │    │
│  │  • getAnalysisStatus(id): AnalysisStatus                        │    │
│  │  • getAnalysisValueKeys(id): string[]                           │    │
│  │  • getAnalysisValues(id, valueKey): object                      │    │
│  │  • getDocument(id, classId, scope, filter): object              │    │
│  │  • deleteAnalysis(id): boolean                                  │    │
│  │  • stopAnalysis(id): boolean                                    │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                   Issue Sync Methods (Optional)                 │    │
│  │  • getSyncedIssueAttributes(issueId, attributes, lastSyncAt)    │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                 Schema Extension (Optional)                     │    │
│  │  • getSchemaExtension(): string                                 │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │              Custom Resolvers (Optional)                        │    │
│  │  • getResolvers(context): ResolverMap                           │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │              Lifecycle Hooks (Optional)                         │    │
│  │  • onModelDeleted(tx, modelId, analysisIds)                     │    │
│  │  • onOrphanSweep(tx, { apply })                                 │    │
│  │  • afterInstall(ctx)                                            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

DTModule Interface

// packages/dt-module/src/interfaces/module-interface.ts

export interface ExtendedPubSubEngine extends PubSubEngine {
  asyncIterator<T>(triggers: string | string[]): AsyncIterator<T>;
}

// Context handed to the post-commit afterInstall hook. Unlike the in-transaction
// hooks, this carries the raw driver — the hook opens its own session.
export interface ModuleInstallContext {
  driver: any;          // Raw neo4j-driver Driver (typed `any` to keep neo4j out of the base lib)
  moduleName: string;   // This module's name — equals its `:Module {name}` in the graph
  databaseName: string; // The database installed into — for `driver.session({ database })`
}

export interface DTModule {
  // Required - Module metadata
  getMetadata(): DTMetadata | Promise<DTMetadata>;

  // Optional - Lifecycle. Called by the platform before an instance is discarded
  // (reload, replacement, failed load). Must be idempotent and never throw in
  // normal operation. DtFileOpaModule uses it to free its in-process Rego engines:
  // the WASM heap is not reclaimed by garbage collection, so a discarded instance
  // that is not disposed permanently strands its policy set.
  dispose?(): void;

  // Optional - Configuration templates
  getModuleTemplate?(): Promise<string>;
  getClassTemplate?(id: string, token?: string): Promise<string>;
  getClassGuide?(id: string, token?: string): Promise<string>;

  // Optional - Declares whether template/guide content varies by caller (default false)
  isContentCallerVariant?(): boolean;

  // Optional - Security evaluation
  getExposures?(id: string, classId: string, token?: string): Promise<Exposure[]>;
  getCountermeasures?(id: string, classId: string, token?: string): Promise<Countermeasure[]>;

  // Optional - Analysis methods (for modules providing analysis capabilities)
  runAnalysis?(
    id: string,
    analysisClassId: string,
    scope: string,
    pubSub: ExtendedPubSubEngine,
    additionalParams?: object,
    token?: string
  ): Promise<AnalysisSession>;

  startChat?(
    id: string,
    analysisClassId: string,
    scope: string,
    userQuestion: string,
    pubSub: ExtendedPubSubEngine,
    additionalParams?: object
  ): Promise<AnalysisSession>;

  resumeAnalysis?(
    id: string,
    analysisClassId: string,
    input: any,
    pubSub: ExtendedPubSubEngine
  ): Promise<AnalysisSession>;

  getAnalysisStatus?(id: string): Promise<AnalysisStatus>;
  getAnalysisValueKeys?(id: string): Promise<string[]>;
  getAnalysisValues?(id: string, valueKey: string): Promise<object>;
  getDocument?(
    id: string,
    analysisClassId: string,
    scope: string,
    filter: object
  ): Promise<object>;
  deleteAnalysis?(id: string): Promise<boolean>;
  stopAnalysis?(id: string): Promise<boolean>;

  // Optional - Issue synchronization
  getSyncedIssueAttributes?(
    issueId: string,
    attributes: string,
    lastSyncAt: string
  ): Promise<string>;

  // Optional - GraphQL schema extension
  getSchemaExtension?(): string | Promise<string | undefined> | undefined;

  // Optional - Custom GraphQL resolvers for fields declared in getSchemaExtension()
  getResolvers?(context: ModuleResolverContext): ResolverMap | Promise<ResolverMap>;

  // Optional - Pre-computed class embeddings (offline-install support)
  getEmbedding?(className: string, embeddingModel: string): number[] | null;

  // Optional - Lifecycle hooks (see Lifecycle Hooks section below)
  onModelDeleted?(
    tx: any,
    modelId: string,
    analysisIds: string[],
  ): Promise<{ nodesDeleted: number; relationshipsDeleted: number } | void>;
  onOrphanSweep?(
    tx: any,
    opts: { apply: boolean },
  ): Promise<{
    byLabel: Record<string, number>;
    nodesDeleted: number;
    relationshipsDeleted: number;
  } | void>;
  afterInstall?(ctx: ModuleInstallContext): Promise<void>;
}

getEmbedding?(className, embeddingModel)

Returns a pre-computed embedding vector for className under the slugified embeddingModel, or null if no vector is available. When present, the platform uses it instead of calling the embedding endpoint — enabling offline install.

  • embeddingModel is the slugified model identifier (produced by slugifyModelName() in @dethernety/dt-module/embedding), so it is always safe to use as a filename segment. A model like sentence-transformers/all-MiniLM-L6-v2 is slugified to sentence-transformers-all-MiniLM-L6-v2.
  • The base class DtFileOpaModule implements this by reading {classDir}/embeddings/{slug}.json. See Pre-Computed Embeddings Spec.
  • Generate vectors with the module-manager embed CLI; see Development Guide → Pre-computed Embeddings.

DTMetadata Interface

The DTMetadata interface defines the structure returned by getMetadata(). It contains the module identity and all class definitions.

Source File: packages/dt-module/src/interfaces/module-metadata-interface.ts

export interface DTMetadata {
  name: string;                                    // Module identifier
  description?: string;                            // Human-readable description
  icon?: string;                                   // Icon identifier for UI
  version?: string;                                // Semantic version
  author?: string;                                 // Module author

  // Class definitions
  componentClasses?: ComponentClassMetadata[];     // System components
  dataFlowClasses?: DataFlowClassMetadata[];       // Data flow types
  securityBoundaryClasses?: SecurityBoundaryClassMetadata[];  // Trust zones
  dataClasses?: DataClassMetadata[];               // Data classifications
  controlClasses?: ControlClassMetadata[];         // Security controls
  analysisClasses?: AnalysisClassMetadata[];       // AI analysis types
  issueClasses?: IssueClassMetadata[];             // Issue tracking types
}

Metadata Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                        Metadata Registration Flow                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Module Registry Service                                                │
│       │                                                                 │
│       │  1. Loads module                                                │
│       │                                                                 │
│       ▼                                                                 │
│  ┌─────────────────┐                                                    │
│  │ module.getMetadata()                                                 │
│  └────────┬────────┘                                                    │
│           │                                                             │
│           │  2. Returns DTMetadata                                      │
│           ▼                                                             │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  DTMetadata                                                     │    │
│  │  {                                                              │    │
│  │    name: "dethernety-general",                                  │    │
│  │    version: "1.0.0",                                            │    │
│  │    componentClasses: [...],                                     │    │
│  │    dataFlowClasses: [...],                                      │    │
│  │    securityBoundaryClasses: [...],                              │    │
│  │    ...                                                          │    │
│  │  }                                                              │    │
│  └────────┬────────────────────────────────────────────────────────┘    │
│           │                                                             │
│           │  3. Store in graph database                                 │
│           ▼                                                             │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  Graph Database                                                 │    │
│  │                                                                 │    │
│  │  (DTModule {name: "dethernety-general"})                        │    │
│  │       │                                                         │    │
│  │       ├──[:MODULE_PROVIDES_CLASS]──>(DTComponentClass)          │    │
│  │       ├──[:MODULE_PROVIDES_CLASS]──>(DTDataFlowClass)           │    │
│  │       ├──[:MODULE_PROVIDES_CLASS]──>(DTSecurityBoundaryClass)   │    │
│  │       └──[:MODULE_PROVIDES_CLASS]──>(DTControlClass)            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Class Identity Contract

Every class metadata entry (ComponentClassMetadata, AnalysisClassMetadata, …) carries an optional id. That id is the stable, operational identity of the class within the deployment — it survives renames, is referenced by exported models, and is what :IS_INSTANCE_OF edges in the graph store.

Stable ids

Module authors should set id to a deployment-stable value (UUID or any opaque string) and not change it across versions. The name is the human-readable identifier and is allowed to drift; the id is what the platform uses for instance edges, control library entries, and export references.

If the module omits id, the platform mints one at install time. That auto-minted id is then re-used on subsequent installs as long as the same (module, className) pair is present — the install flow uses name as the match key when reconciling existing classes.

Rebind behaviour

When a module's declared id for a class diverges from the DB-resident id (typical cause: an author changed the source id after the first install), the install flow follows the module's idRebindPolicy:

PolicyBehaviour
audit (default)Rebind the DB id to the module-declared id; emit a rebind event into the class-identity log
silentSame as audit but suppresses the event emission
strictRefuse to rebind; mark the install as unavailable (or partial if only some classes conflict) and surface the conflict via Module.rebindConflicts for operator resolution

Set the policy in your module's DTMetadata if your module is source-controlled and you'd rather catch unintended id changes than have them silently applied. Most first-party modules use the default (audit).

Orphaned classes

A class that exists in the DB but is no longer declared by the module's metadata is orphaned, not deleted. At the graph level this is the rename HAS_CLASSHAS_ORPHANED_CLASS on the (Module)→(Class) edge — instance edges (:IS_INSTANCE_OF) are preserved, so existing analyses, components, etc. that reference the class continue to work.

Operators can revive (un-orphan) or hard-delete orphaned classes through the admin Operations tab in the modules page. Module authors should not delete a class outright from metadata without first confirming there are no live instances — the platform's safe default is to orphan rather than break references.

Cross-references


Class Metadata Interfaces

All class metadata interfaces share a common structure with slight variations for specific class types.

ComponentClassMetadata

Source File: packages/dt-module/src/interfaces/component-class-metadata-interface.ts

export interface ComponentClassMetadata {
  id?: string;              // Unique identifier (auto-generated if not provided)
  name: string;             // Display name
  description?: string;     // Description for tooltips/documentation
  type: string;             // Component type: PROCESS, EXTERNAL_ENTITY, STORE
  category: string;         // Grouping category (e.g., "Web", "Database", "API")
  icon?: string;            // Icon identifier
  properties?: object;      // Additional metadata
}

Component Types:

  • PROCESS - Internal system processes
  • EXTERNAL_ENTITY - External actors or systems
  • STORE - Data stores (databases, file systems)

DataFlowClassMetadata

Source File: packages/dt-module/src/interfaces/dataflow-class-metadata-interface.ts

export interface DataFlowClassMetadata {
  id?: string;
  name: string;
  description?: string;
  type: string;                    // Flow type (e.g., "HTTP", "gRPC", "TCP")
  category: string;                // Grouping category
  compatibleTypes?: string[];      // Component types this can connect
  compatibleCategories?: string[]; // Component categories this can connect
  icon?: string;
  properties?: object;
}

SecurityBoundaryClassMetadata

Source File: packages/dt-module/src/interfaces/securityboundary-class-metadata-interface.ts

export interface SecurityBoundaryClassMetadata {
  id?: string;
  name: string;
  description?: string;
  type: string;             // Boundary type: BOUNDARY, SECURITY_BOUNDARY
  category: string;         // Trust level (e.g., "Internal", "DMZ", "External")
  icon?: string;
  properties?: object;
}

DataClassMetadata

Source File: packages/dt-module/src/interfaces/data-class-metadata-interface.ts

export interface DataClassMetadata {
  id?: string;
  name: string;
  description?: string;
  type: string;             // Data type classification
  category: string;         // Sensitivity category (e.g., "PII", "Credentials")
  icon?: string;
  properties?: object;
}

ControlClassMetadata

Source File: packages/dt-module/src/interfaces/control-class-metadata-interface.ts

export interface ControlClassMetadata {
  id?: string;
  name: string;
  description?: string;
  type: string;                    // Control type (e.g., "Authentication", "Encryption")
  category: string;                // Control framework category
  compatibleTypes?: string[];      // Element types this control applies to
  compatibleCategories?: string[]; // Element categories this control applies to
  icon?: string;
  properties?: object;
}

AnalysisClassMetadata

Source File: packages/dt-module/src/interfaces/analysis-class-metadata-interface.ts

export interface AnalysisClassMetadata {
  id?: string;              // Identifies the analysis workflow (e.g., LangGraph assistant_id when using DtLgModule)
  name: string;             // Analysis name
  description?: string;     // Analysis description
  type: string;             // Analysis type (e.g., "model_analysis")
  category: string;         // Category (e.g., "attack_scenario", "threat")
  icon?: string;
  properties?: object;
}

IssueClassMetadata

Source File: packages/dt-module/src/interfaces/issue-class-metadata-interface.ts

export interface IssueClassMetadata {
  id?: string;
  name: string;
  description?: string;
  type: string;             // Issue type (e.g., "vulnerability", "finding")
  category: string;         // Issue category
  icon?: string;
  properties?: object;
}

Exposure and Countermeasure Interfaces

MitreRef

A reference from a finding (Exposure / Countermeasure) to a MITRE node. label + property + value self-describe the target node and its key (e.g. MitreAttackTechnique / attack_id / T1078). The relationship type (edge name) is decided by the field the ref sits under, not by the ref itself. attributes is free-form provenance (e.g. justification) copied onto the graph edge — values must be primitives.

Source File: packages/dt-module/src/interfaces/mitre-ref-interface.ts

export interface MitreRef {
  label: string;
  property: string;
  value: string;
  attributes?: Record<string, string | number | boolean>;
}

Exposure

Represents a security vulnerability or weakness detected for a model element.

Source File: packages/dt-module/src/interfaces/exposure-interface.ts

import { MitreRef } from './mitre-ref-interface';

export interface Exposure {
  id?: string;
  name: string;                  // Exposure name
  description?: string;          // Detailed description
  type: string;                  // Exposure type
  category: string;              // Exposure category
  score?: number;                // Severity score (0-10)
  reference?: string;            // External reference (e.g., CVE, CWE)
  attackVector?: string;         // CVSS v3.1 attack vector (NETWORK, ADJACENT, LOCAL, PHYSICAL, UNSPECIFIED)
  mitigationTechniques?: string[]; // Recommended mitigations
  detectionTechniques?: string[];  // Detection methods
  tags?: string[];               // Classification tags

  // → EXPLOITED_BY edges to the MITRE node(s) that exploit this exposure.
  // MitreRef form may carry edge `attributes`; the bare-string fallback is preserved.
  exploitedBy?: MitreRef[] | string[];
}

Countermeasure

Represents a security control that addresses exposures.

Source File: packages/dt-module/src/interfaces/countermeasure-interface.ts

import { MitreRef } from './mitre-ref-interface';

export interface Countermeasure {
  id?: string;
  name: string;                   // Countermeasure name
  description?: string;           // Detailed description
  type: string;                   // Countermeasure type
  category: string;               // Countermeasure category
  score?: number;                 // Effectiveness score (0-10)
  reference?: string;             // External reference
  addressedExposures?: string[];  // Exposures this countermeasure addresses
  tags?: string[];                // Classification tags

  // Identity block → RESPONDS_WITH edges (the ATT&CK Mitigation + D3FEND technique
  // this control implements). MitreRef form may carry edge `attributes`; bare-string preserved.
  respondsWith?: MitreRef[] | string[];

  // Verb blocks → COUNTERMEASURE_<VERB> edges to MitreAttackTechnique (how this control
  // counters each technique). Each countermeasure populates only the verbs its policy emits.
  // The first four are surfaced as GraphQL fields; the last four are written ahead but not yet queryable.
  mitigates?: MitreRef[];
  protectsAgainst?: MitreRef[];
  detects?: MitreRef[];
  isolates?: MitreRef[];
  deceives?: MitreRef[];
  evicts?: MitreRef[];
  restores?: MitreRef[];
  respondsTo?: MitreRef[];
}

Exposure/Countermeasure Evaluation Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                     Exposure Evaluation Flow                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  User configures component attributes in UI                             │
│       │                                                                 │
│       │  1. Attributes saved to graph database                          │
│       ▼                                                                 │
│  ┌───────────────────────────────────────────────────────────┐          │
│  │  Component Instance                                       │          │
│  │  {                                                        │          │
│  │    id: "comp-123",                                        │          │
│  │    classId: "class-456",                                  │          │
│  │    attributes: {                                          │          │
│  │      "authentication_enabled": false,                     │          │
│  │      "encryption_at_rest": false                          │          │
│  │    }                                                      │          │
│  │  }                                                        │          │
│  └─────────────────────────┬─────────────────────────────────┘          │
│                            │                                            │
│                            │  2. module.getExposures(id, classId)       │
│                            ▼                                            │
│  ┌───────────────────────────────────────────────────────────┐          │
│  │  In-Process Rego Policy Evaluation                        │          │
│  │                                                           │          │
│  │  package dethernety.webserver                             │          │
│  │                                                           │          │
│  │  exposures[exp] {                                         │          │
│  │    not input.authentication_enabled                       │          │
│  │    exp := {                                               │          │
│  │      "name": "Missing Authentication",                    │          │
│  │      "type": "vulnerability",                             │          │
│  │      "score": 8                                           │          │
│  │    }                                                      │          │
│  │  }                                                        │          │
│  └─────────────────────────┬─────────────────────────────────┘          │
│                            │                                            │
│                            │  3. Return exposures                       │
│                            ▼                                            │
│  ┌───────────────────────────────────────────────────────────┐          │
│  │  Exposure[]                                               │          │
│  │  [                                                        │          │
│  │    {                                                      │          │
│  │      name: "Missing Authentication",                      │          │
│  │      type: "vulnerability",                               │          │
│  │      category: "access_control",                          │          │
│  │      score: 8,                                            │          │
│  │      exploitedBy: ["T1078"]                               │          │
│  │    }                                                      │          │
│  │  ]                                                        │          │
│  └───────────────────────────────────────────────────────────┘          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Analysis Interfaces

LgGraphConfig

Configuration for a single LangGraph analysis graph.

Source File: packages/dt-module/src/interfaces/lg-analysis-config-interface.ts

export interface LgGraphConfig {
  description?: string;        // Human-readable description
  type?: string;               // Analysis type classification
  category?: string;           // Category for grouping

  // Document storage location
  index_document?: (
    scope: string,
    analysisId?: string
  ) => Promise<{ namespace: string[]; key: string }>;

  // Input payload builder (required)
  input: (
    scope: string,
    analysisId?: string,
    driver?: any,
    additionalParams?: any
  ) => Promise<any>;

  // Optional post-processing
  post_process?: (result: any) => Promise<any>;
}

LgAnalysisConfig

Maps graph names to their configurations.

export interface LgAnalysisConfig {
  graphs: {
    [graphName: string]: LgGraphConfig;
  };
}

LgModuleMetadata

Static metadata for LangGraph modules.

export interface LgModuleMetadata {
  description: string;
  version: string;
  author: string;
  icon?: string;
}

LgModuleOptions

Constructor options for DtLgModule.

export interface LgModuleOptions {
  langgraphApiUrl?: string;         // LangGraph API URL
  langgraphTimeoutMs?: number;      // Control-plane request timeout (default 30000; the run stream is exempt)
  analysisConfig: LgAnalysisConfig; // Graph configurations
  metadata: LgModuleMetadata;       // Module metadata
}

Asset-context fields exposed to modules

Analysis modules read the model graph through the per-request GraphQLContext (the driver + sessionConfig available in getResolvers/analysis). Alongside structure, classes, controls, and exposures, the graph carries author-asserted asset context — what the modeller declared about scope, value, and data sensitivity. These are inputs to analysis, not computed verdicts: treat the model-level fields as seeds for the analysis-phase scope, not direct risk-math inputs, and the per-element fields as evidence to weigh.

No module-API change is required — these are ordinary node properties on Model, Component, and Data, selectable in any module query.

Model scope (five flat fields on Model)

FieldTypeMeaning
depthModelingDepthARCHITECTURE | DESIGN | IMPLEMENTATIONHow deep the model reasons (architecture, design, or implementation fidelity).
modelingIntentModelingIntentINITIAL | SECURITY_REVIEW | COMPLIANCE | INCIDENT_RESPONSEWhy the model exists; frames which findings matter.
complianceDrivers[String!]Regulatory/standards obligations in scope (e.g. PCI-DSS, HIPAA). Free-text.
exclusions[String!]What the modeller deliberately put out of scope; analysis should not fault their absence.
trustAssumptions[String!]What the model treats as trusted (e.g. "cloud control plane"); analysis may surface these as assumptions to test.

Per-element fields

  • Component.crownJewel: Boolean — the modeller marked this component a high-value asset. Crown-jewel marks exist on components only.
  • Data.sensitivity: SensitivityLevel (PUBLIC < INTERNAL < CONFIDENTIAL < RESTRICTED) — author-asserted classification. Rank an element by the maximum sensitivity of the data it handles (but see the coalescing contract below: null-sensitivity data is excluded from that max and surfaced separately, never treated as PUBLIC).
  • Data.regulatoryFlags: [String!] — free-text compliance labels; see the canonical vocabulary for the recommended set and casing.

Coalescing contract (legacy + un-set nodes)

All asset-context fields are nullable — legacy nodes and un-classified elements read null. Coalesce consistently:

Fieldnull means
crownJewelnot a crown jewel (treat as false)
regulatoryFlagsnone declared (treat as [])
sensitivityunclassified — not public

Treat sensitivity: null as unknown, surfaced separately — never as a public floor. An element handling only null-sensitivity data is unrated, not low-risk; folding it into a max-sensitivity aggregation as public would silently under-rate genuinely-sensitive-but-unclassified data. Report the unclassified gap rather than scoring through it.

dataInRegulatoryScope(flag: String!): [Data!]!

Returns every Data node whose regulatoryFlags contains flag. For module authors:

  • Exact, case-sensitive match — a producer typo ("phi" vs "PHI") returns [] silently, not an error. Query with the canonical casing.
  • O(|Data|) full scanregulatoryFlags is a list and is not indexable on Memgraph, so cost is linear in the number of Data nodes. Fine for analysis-phase use; don't call it in a tight loop.
  • Direct handlers only — it finds the data items carrying the flag, not the components/flows adjacent to them. Cardholder-data-environment (CDE) adjacency — which components touch regulated data — is an analysis-phase traversal you compose on top, not something this query does.

Not available (deliberately, in this version)

  • No crownJewel on Data, SecurityBoundary, or DataFlow — crown-jewel marks on those live only in local plugin files and are not synced to the platform.
  • No persisted adversary classes / threat-actor model on the graph.
  • Monitoring-coverage and credential-handling asset context are not yet first-class graph fields — they remain local enrichment pending separate node authoring.

Method Details

getMetadata()

Required. Returns the module's metadata including all class definitions.

async getMetadata(): Promise<DTMetadata>

Called by: Module Registry Service at startup

Returns: DTMetadata containing module identity and all class arrays


getModuleTemplate()

Optional. Returns a JSON string containing the JSON Schema and UI Schema for module-wide configuration. A module implements it only if it has genuine module-wide settings to expose; when a module does not define it, the platform's template resolver returns its documented fallback.

DtFileOpaModule does not implement getModuleTemplate(): Rego policies evaluate in-process and need no configuration, so there is no module-wide setting to surface. DtLgModule likewise does not: its only conceivable module-wide setting (the LangGraph API URL) is resolved from constructor options/environment, not from a template.

getModuleTemplate?(): Promise<string>

Returns: JSON string with schema and uischema properties

Example Response:

{
  "schema": {
    "type": "object",
    "properties": {
      "report_verbosity": {
        "type": "string",
        "enum": ["summary", "detailed"]
      }
    }
  },
  "uischema": {
    "type": "VerticalLayout",
    "elements": [
      { "type": "Control", "scope": "#/properties/report_verbosity" }
    ]
  }
}

getClassTemplate(id, token?)

Returns the JSON Schema template for a specific class's attributes.

async getClassTemplate(id: string, token?: string): Promise<string>

Parameters:

  • id - The class instance ID (component, dataflow, boundary, etc.)
  • token? - The raw bearer token of the calling request: an opaque credential to forward to an upstream service on the caller's behalf. Never decode it for identity, and never log it. Absent (undefined) in dev/NOAUTH or when the request carries no bearer — the implementation must tolerate absence.

Returns: JSON string with schema and uischema for class configuration


getClassGuide(id, token?)

Returns usage guidance for configuring a specific class.

async getClassGuide(id: string, token?: string): Promise<string>

Parameters:

  • id - The class instance ID
  • token? - Same as getClassTemplate — the raw bearer token to forward upstream; opaque, never decoded for identity, never logged; undefined when absent.

Returns: YAML or JSON string with configuration guidance


getExposures(id, classId, token?)

Evaluates and returns exposures for a model element based on its attributes.

async getExposures(id: string, classId: string, token?: string): Promise<Exposure[]>

Parameters:

  • id - The element instance ID
  • classId - The class ID assigned to the element
  • token? - The raw bearer token to forward upstream; opaque, never decoded for identity, never logged; undefined when absent. Unlike the template/guide methods, results here are persisted to the shared model graph, so they must not vary by caller (see isContentCallerVariant).

Returns: Array of Exposure objects


getCountermeasures(id, classId, token?)

Evaluates and returns countermeasures for a model element.

async getCountermeasures(id: string, classId: string, token?: string): Promise<Countermeasure[]>

Parameters:

  • id - The element instance ID
  • classId - The class ID assigned to the element
  • token? - The raw bearer token to forward upstream; opaque, never decoded for identity, never logged; undefined when absent. As with getExposures, results persist to the shared graph and must not vary by caller.

Returns: Array of Countermeasure objects


isContentCallerVariant()

Declares whether this module's template/guide output may depend on the calling user (the token passed to getClassTemplate/getClassGuide).

isContentCallerVariant?(): boolean

Returns: true if template/guide content is caller-dependent; absent or false (the default) means caller-independent and freely cacheable — the case for every module that answers from static, on-disk content.

When to return true. Only when getClassTemplate/getClassGuide genuinely produce different content for different callers. The platform then bypasses its template cache for this module, so no caller ever receives content generated for another.

Invariant — template/guide only. getExposures/getCountermeasures must never vary by caller regardless of this predicate: their results are persisted to the shared model graph and read back by every caller of that element, so a per-caller value would leak across callers (a durable leak the cache bypass cannot prevent). Returning true licenses per-caller template/guide content and nothing else.


runAnalysis(...)

Starts an analysis workflow. DtLgModule delegates this to a LangGraph server; direct implementations handle execution themselves.

async runAnalysis(
  id: string,
  analysisClassId: string,
  scope: string,
  pubSub: ExtendedPubSubEngine,
  additionalParams?: object,
  token?: string
): Promise<AnalysisSession>

Parameters:

  • id - Analysis session ID
  • analysisClassId - The analysis class ID
  • scope - Analysis scope (usually model ID)
  • pubSub - GraphQL subscription engine for streaming results
  • additionalParams - Optional parameters to pass to the graph
  • token - The caller's bearer, for a module whose analysis reaches an entitled service. Passed as its own argument rather than inside additionalParams, because that object is caller-supplied and is merged into the module's attributed parameters — not somewhere a credential may go. Modules that ignore it are unaffected.

Returns: AnalysisSession with sessionId


startChat(...)

Starts an interactive chat session with an analysis module.

async startChat(
  id: string,
  analysisClassId: string,
  scope: string,
  userQuestion: string,
  pubSub: ExtendedPubSubEngine,
  additionalParams?: object
): Promise<AnalysisSession>

Parameters:

  • id - Chat session ID
  • analysisClassId - The analysis class ID
  • scope - Analysis scope
  • userQuestion - User's question
  • pubSub - Subscription engine
  • additionalParams - Optional parameters

Returns: AnalysisSession with sessionId


resumeAnalysis(...)

Resumes a paused or interrupted analysis.

async resumeAnalysis(
  id: string,
  analysisClassId: string,
  input: any,
  pubSub: ExtendedPubSubEngine
): Promise<AnalysisSession>

Parameters:

  • id - Analysis session ID
  • analysisClassId - The analysis class ID
  • input - Input to provide for resumption (e.g., human feedback)
  • pubSub - Subscription engine

Returns: AnalysisSession


getAnalysisStatus(id)

Gets the current status of an analysis session.

async getAnalysisStatus(id: string): Promise<AnalysisStatus>

Returns: AnalysisStatus with status, hasDocument, messages, interrupts, metadata

hasDocument (optional on the interface, defaulted to false at the GraphQL resolver) should be true once a run has completed successfully and a viewable result exists, so the UI can tell a never-run analysis from a completed one (both report status: idle).


deleteAnalysis(id) / stopAnalysis(id)

Deletes or stops an analysis session.

async deleteAnalysis(id: string): Promise<boolean>
async stopAnalysis(id: string): Promise<boolean>

Returns: true if successful


getSchemaExtension()

Returns a GraphQL SDL string that extends the platform's base schema. The ModuleRegistryService calls this on each loaded module at startup and stores the result in ModuleEntry.schemaFragment. The SchemaService merges all valid fragments into the base schema before constructing the Neo4jGraphQL instance.

getSchemaExtension?(): string | Promise<string | undefined> | undefined

Called by: Module Registry Service at startup

Returns: GraphQL SDL string, or undefined if the module does not extend the schema

Rules:

  • Define new types only. Do not redefine existing platform types.
  • Invalid fragments (those that fail graphql.parse()) are skipped at startup with a warning.

Example Return Value:

type ThreatIntel {
  id: ID!
  name: String!
  severity: String
  source: String
}

DtLgModule provides a default implementation that reads schema.graphql from the compiled module directory using the readSchemaExtension() utility. See BASE_CLASSES.md for details.


getResolvers(context)

Returns custom GraphQL resolver functions for fields declared in this module's schema extension. This allows modules to back their SDL types with non-Cypher logic (external API calls, procedural operations, policy evaluation) without hardcoding resolver services in the platform.

Source File: packages/dt-module/src/interfaces/module-interface.ts

getResolvers?(context: ModuleResolverContext): ResolverMap | Promise<ResolverMap>

Called by: Module Registry Service at startup (once, not per-request)

Parameters:

  • context — A ModuleResolverContext providing shared resources for constructing resolver closures

Returns: A ResolverMap mapping TypeName.fieldName to resolver functions, or a Promise thereof

Contract rules:

  • Only called if getSchemaExtension() returned a non-empty SDL fragment
  • The returned resolver map must only contain fields that appear in the module's SDL
  • Resolvers for undeclared fields are silently rejected at startup
  • Subscription resolvers are not supported — any Subscription key is rejected
  • If getResolvers() throws, the module remains healthy; resolvers are a best-effort addition
  • Resolver functions are closures — capture shared resources from context at construction time; per-request data (auth token, user) arrives via the standard resolver function signature (parent, args, context, info)

Security:

  • All module resolvers are wrapped with auth enforcement — a valid JWT is required even if the module's SDL omits @authentication
  • All module resolvers are wrapped with a 30-second timeout
  • Module errors are sanitized before reaching clients (internal details hidden in production)
  • Hardcoded platform resolvers always take precedence over module resolvers on conflict

For detailed architecture and security model, see MODULE_CUSTOM_RESOLVERS.md.

ModuleResolverContext

Source File: packages/dt-module/src/interfaces/module-resolver-interface.ts

Context passed to getResolvers() at startup. Use this to construct resolver functions that close over shared resources. This is NOT a per-request context.

export interface ModuleResolverContext {
  /** Neo4j/Memgraph driver -- same driver the module received at construction time */
  driver: any;
  /** Logger scoped to the module */
  logger: Logger;
  /** Database name for session creation */
  databaseName: string;
}

ResolverMap / ResolverFunction

Source File: packages/dt-module/src/interfaces/module-resolver-interface.ts

export interface ResolverMap {
  [typeName: string]: {
    [fieldName: string]: ResolverFunction;
  };
}

export interface ResolverFunction {
  (parent: any, args: any, context: any, info: any): any;
}

The context parameter in ResolverFunction is typed as any intentionally — modules must not depend on platform-internal types. At runtime, it is the per-request GraphQLContext containing token, jwt, driver, sessionConfig, etc.


Lifecycle Hooks (Optional)

Lifecycle hooks are push-style callbacks: the platform invokes them on a platform event or a maintenance operation, rather than the module pulling state on a user request.

They split into two kinds by when they run relative to the platform's transaction:

  • In-transaction hooksonModelDeleted and onOrphanSweep. The platform hands the hook a live tx and the module's writes commit or roll back together with the platform's.
  • Post-commit hookafterInstall. The platform hands the hook the raw driver (not a tx) after its write transaction has committed; the hook opens its own session and the platform does not roll back its writes. See afterInstall(ctx) for the carve-out that follows from this timing.

The division of responsibility is by label ownership. The platform owns the core/structural labels (Model, SecurityBoundary, Component, DataFlow, Data, Exposure) and removes them itself. Each module owns — and is the only participant that removes — the labels it defines. A hook is the seam through which the platform tells a module "a model was deleted" or "remove your orphans" without the platform surface ever naming a module's labels.

The in-transaction hooks (onModelDeleted, onOrphanSweep) share the same discipline (afterInstall deliberately differs — see its subsection):

  • Transaction-bound. Perform graph operations only on the passed tx. Do not open your own session or transaction — a rollback must be able to revert the hook's writes.
  • Idempotent. The platform runs the work inside a managed transaction that may re-run the whole callback (and therefore the hook) on a retriable error. Re-running DETACH DELETE-style graph operations is safe; a second invocation on already-clean data is a no-op.
  • Side-effect-free. No event emit, external call, counter increment, or any off-tx write. A non-transactional side effect would be doubled on a retry and never rolled back.
  • Throw-to-abort. A throw from any hook aborts the whole operation — the transaction rolls back and the throw surfaces as an error. Invocation order across modules is unspecified, so each implementation must be self-contained from its arguments and must not depend on another module's hook running first.

onModelDeleted(tx, modelId, analysisIds)

onModelDeleted?(
  tx: any,
  modelId: string,
  analysisIds: string[],
): Promise<{ nodesDeleted: number; relationshipsDeleted: number } | void>

Removes the module's own model-scoped nodes when the platform deletes a model.

Called by: the platform's model-delete path, inside the single write transaction that also runs the structural delete. The platform pre-enumerates the model's owned analysis ids and dispatches this hook to every loaded module before running the structural delete; everything commits or rolls back together.

Parameters:

  • tx — the active write transaction. Typed any to match this package's transaction-callback convention (so the base library carries no driver dependency). All of the hook's graph operations must run on this tx.
  • modelId — the id of the model being deleted.
  • analysisIds — the model's owned analysis ids, pre-collected by the platform so the hook need not re-enumerate them.

Returns: { nodesDeleted, relationshipsDeleted } for the platform to fold into its deletion stats, or void.

Contract rules:

  • Operate only on the passed tx — open no session or transaction of your own.
  • Be idempotent: the managed transaction may re-run the callback, and therefore this hook, on a retriable error.
  • Perform no non-transactional side effects (no event emit, external call, counter, or off-tx write).
  • Be self-contained from { modelId, analysisIds } — invocation order across modules is unspecified, so do not depend on another module's nodes already being gone.
  • A throw aborts the whole delete: the transaction rolls back and the error propagates.

onOrphanSweep(tx, opts)

onOrphanSweep?(
  tx: any,
  opts: { apply: boolean },
): Promise<{
  byLabel: Record<string, number>;
  nodesDeleted: number;
  relationshipsDeleted: number;
} | void>

Counts or removes the module's own orphaned nodes during an admin-run, graph-wide sweep of pre-existing orphans (nodes whose owner was deleted before the delete path cascaded fully).

Called by: the admin orphan-sweep operation, on a single transaction shared with the platform's core sweep. The platform aggregates each module's per-label counts into one operator-facing report; it never names a module's labels itself.

Parameters:

  • tx — the active transaction. Typed any per this package's transaction-callback convention. Read transaction on dry-run, write transaction on apply.
  • opts.apply:
    • false (dry-run) — count only. Must not mutate the graph; the platform runs this on a read transaction. Return the would-delete counts so an operator can preview the blast radius.
    • truedelete the orphans and return actual counts. The platform runs this on a write transaction.

Returns: { byLabel, nodesDeleted, relationshipsDeleted } to fold into the platform's report, or void. byLabel maps each of the module's own labels to its node count.

Contract rules:

  • Operate only on the passed tx; respect the mode — never mutate the graph when apply is false.
  • Report node counts only on the dry-run; the dry-run count and the apply count for the same graph state must agree per label (the sweep's self-consistency contract).
  • Be idempotent: a second sweep over already-clean data is a no-op ({}), and the managed transaction may re-run the callback on a retriable error.
  • Perform no non-transactional side effects.
  • A throw aborts the whole sweep — for example, a violated data-integrity precondition that would make deletion risk live data. The throw rolls the transaction back and surfaces as an error.

afterInstall(ctx)

afterInstall?(ctx: ModuleInstallContext): Promise<void>

Runs graph work that must reference the module's own committed :Module node — for example, a module that links a bespoke node it seeds to its (:Module {name}).

Unlike the in-transaction hooks above, this one runs post-commit on a session the module opens itself. It fires once per install/reinstall, strictly after the multi-module write transaction commits — the first (and only) lifecycle point at which the module's own :Module node is committed and visible to a fresh session. Every other module hook fires before that node is written. Because the platform hands over the raw driver rather than a tx, the module's writes are not part of a platform transaction and are not rolled back by the platform.

Called by: the platform's post-commit install step, once the module upsert transaction has committed. It runs on both freshly-installed modules and content-hash-skipped (unchanged) modules — an unchanged module still re-runs its hook on each boot. See ModuleManagementService → afterInstall invocation for the mechanism.

Parameters:

  • ctx — a ModuleInstallContext with exactly three fields:
    • driver — the raw neo4j-driver Driver. Typed any to keep a neo4j-driver dependency out of the base library. The hook opens its own session from it (ctx.driver.session({ database: ctx.databaseName })); this is not a tx.
    • moduleName — this module's name, equal to its :Module {name} in the graph. Use it to MATCH the module's own node.
    • databaseName — the database the platform installed into, for opening the session.

Returns: Promise<void>.

Contract rules:

  • Open your own session from ctx.driver and close it before returning — do not hold it open beyond the call. (This is the deliberate opposite of the in-transaction hooks, which forbid opening a session.)
  • Be idempotent — MERGE, not CREATE. The hook re-runs on every boot for an unchanged module, and it re-runs after a self-heal reinstall, so it may execute more than once for the same logical state.
  • Failure is isolated and self-healing. A throw — or exceeding the module-load timeout (MODULE_LOAD_TIMEOUT, default 30 000 ms) — is caught and logged, and downgrades only this module (SET m.lastInstallStatus = 'partial') so the content-hash skip gate reinstalls and re-invokes it on the next boot. Sibling modules in the same batch are unaffected, and the install itself never fails.
  • The platform does not roll back the hook's writes. Because it runs post-commit on your own session, a partial write survives a later throw; idempotent MERGE-based writes keep re-runs safe.

Example — link the module's own :Module node to a node it seeds, proving post-commit visibility:

import type { ModuleInstallContext } from '@dethernety/dt-module';

async afterInstall(ctx: ModuleInstallContext): Promise<void> {
  const session = ctx.driver.session({ database: ctx.databaseName });
  try {
    await session.executeWrite((tx: any) =>
      tx.run(
        `MATCH (m:Module {name: $name})
         MERGE (m)-[:AFTER_INSTALL_MARKER]->(:ReferenceData {n: $name})`,
        { name: ctx.moduleName },
      ),
    );
  } finally {
    await session.close();
  }
}

The MATCH (m:Module {name}) succeeds only because the hook runs post-commit — this is the guarantee no earlier hook can offer. The MERGE keeps the write idempotent across the re-runs described above.


Remote content modules (DtRemoteModule)

DtRemoteModule is a sibling of DtFileOpaModule that implements the same DTModule contract, but serves metadata, class templates, guides, embeddings, and evaluation from an HTTP content service over the module content wire protocol instead of from a local data directory. Every difference from a file-backed module (network, caching, denial, not-evaluated) is expressed through the DTModule contract — a returned template, a returned finding list, or a thrown error — never a new platform hook, so the platform (and dt-ui) stay unaware that the module is remote.

Source file: packages/dt-module/src/dt-remote-module.ts

Mounting

An operator mounts a remote module with a trivial stub whose default export constructs a DtRemoteModule for one module key at one pinned content version:

import { DtRemoteModule } from '@dethernety/dt-module';

export default class MyRemoteModule extends DtRemoteModule {
  constructor(driver, logger) {
    super({ moduleKey: 'my-module', pin: 'sha256:…' }, driver, logger);
  }
}

Configuration

SettingScopeMeaning
MODULE_CONTENT_BASE_URLDeployment (env)The content service base URL. No default — an unset value leaves the module inert (it registers nothing and reports unavailable).
MODULE_CONTENT_CACHE_DIRDeployment (env)Where the metadata/content caches live. Must be co-durable with the graph database — the caches and the classes they protect have to survive a restart together. An unset or ephemeral directory logs a loud boot warning; see below.
moduleKey + pinPer module (stub literal)Which module, at which immutable content-hash version.

Boot, caching, and the pin

  • Boot is credential-free and completes offline from cache. getMetadata() fetches the module document + embeddings and returns the platform's metadata verbatim; getEmbedding() answers synchronously from vectors prefetched at registration.
  • The metadata cache is placed co-durable with the graph database so that a warm deployment boots offline and a registered module never has to fail its load. A remote module that failed to load would be swept along with its classes' bindings; the client's rule is therefore to never throw once it has registered — on an offline pin-miss it serves the newest cached document for the module, keeping ids stable. This is why the cache directory placement is a correctness concern, not just a latency one, and why an ephemeral/unset directory warns loudly.
  • The pin is an immutable content hash. All reads serve at that pin; an assessment can state exactly what content produced it. The operator advances a version by editing the stub's pin and restarting — registration re-registers the same class ids at the new content (ids are stable across versions), so an upgrade never orphans or rebinds.

Entitlement, denial, and not-evaluated

  • Template/guide calls forward the caller's bearer token. A caller who is not entitled (or a deployment with no cloud credential) receives valid, self-sanitized read-only fallback content — the platform renders it like any other module template. Server-authored denial text is escaped to inert plain text and length-bounded before it is embedded, and any action URL is honoured only on the service-declared portal origin.
  • Evaluation reads the element's attributes locally, sends only the class-schema-declared keys to the service (payload minimization), and returns the findings. A denied or unavailable evaluation throws a typed error — never an empty result, which would masquerade as evaluated-clean and overwrite prior findings. A recalled content version (410) surfaces the operator reason and stops serving that pin's cached content.

DtRemoteModule implements the catalog, content, and evaluation surfaces of the wire protocol; other surfaces are separate modules.


Remote knowledge-graph modules (DtRemoteKnowledgeGraphModule)

DtRemoteKnowledgeGraphModule is a sibling of DtRemoteModule that implements the same DTModule contract, but answers knowledge-graph queries — rules, the threats they address, and the attributes they read — from a service instead of from nodes ingested into the deployment's own graph. It contributes a schema fragment and resolvers, and nothing else: everything below them is the KgClient, where the choice between a local graph and a service already lives. As with a remote content module, every difference from a locally-served knowledge graph is expressed through the module contract — a returned payload or a thrown error — never a new platform hook.

Source file: packages/dt-module/src/kg/remote-kg-module.ts

Mounting

An operator mounts it with a trivial stub whose default export extends the class from @dethernety/dt-module 0.12.0 or later, and which carries no per-module value at all:

import { DtRemoteKnowledgeGraphModule } from '@dethernety/dt-module';

export default class KnowledgeGraphModule extends DtRemoteKnowledgeGraphModule {
  constructor(driver, logger) {
    super(driver, logger);
  }
}

Configuration

SettingScopeMeaning
MODULE_KG_BASE_URLDeployment (env)The knowledge-graph service origin. No default — an unset value selects the local mode, so an unconfigured deployment never points itself at a host.
MODULE_KG_VERSIONDeployment (env)The pinned knowledge-graph version: a sha256: content digest and nothing else. No default, and no fallback to "latest" — see below.
(none)Per module (stub literal)Nothing. Unlike a content mount, the stub names no module and pins no version; both values are deployment-global.

Which implementation a deployment gets follows those two variables alone — the module's resolver context carries a driver, a logger and a database name, and has no view of what else is loaded:

MODULE_KG_BASE_URLMODULE_KG_VERSIONResult
seta valid digestRemote — queries are answered by the service
setabsent or malformedUnavailable — queries are refused, and the misconfiguration is logged once at construction
unsetLocal — answered from the deployment's own graph, and reported unavailable when that graph holds no knowledge-graph nodes

Mode selection and the pin

  • This module and a locally-installed knowledge-graph module are mutually exclusive, and keeping them apart is the mounter's job — not this module's. Both would declare the same types and the same kgCapability field, which is a schema conflict rather than a merge. Nothing here detects the other: the module registers under the name knowledge-graph, deliberately not the name a locally-installed one uses, so a deployment carrying both presents as two modules answering one question rather than as an upgrade of one into the other. That makes the conflict legible; it does not prevent it. Mount one or the other.
  • Neither variable has a default, and a missing pin never falls back to "latest." The service publishes a newest version, and taking it would silently advance the knowledge graph under a deployment that pinned deliberately — so a half-configured remote mode is exactly as inert as an unconfigured one. This makes the pin a correctness concern rather than a freshness one: an answer nobody can attribute to a stated version is worse than no answer.
  • A base URL with no usable pin is logged once, at construction, as a misconfiguration. That is the only moment an operator can be told, because every later symptom is an absence — the deployment named a service and then never asked it anything. The configured value itself is not logged.
  • The pin is deployment-global, not per-module. Where DtRemoteModule carries its pin as a stub literal — one module, one content version — a knowledge-graph version describes the whole corpus the deployment answers from, so it belongs to the deployment rather than to a mount. One value, one writer. For how a console-driven deployment chooses and writes that pair, see the knowledge-graph connection.

The interface, entitlement, and the answer shape

  • A consumer sits above KgClient and is never told which implementation it holds. The same queries return the same keyed maps, carrying the same payload objects, in either mode — that property is what the whole design exists to provide, and it is why mode selection is configuration rather than something every caller re-implements. packages/dt-module/src/interfaces/kg-client-interface.ts is the contract; the implementations sit behind createKgClient.
  • Every requested key is present in the returned map. A key that matched nothing carries an empty array rather than being absent, so a caller may treat a missing key as a bug instead of as an answer, and "no matches" never has to be inferred from omission.
  • Being unentitled is never an empty result. A caller the service refuses gets a typed denial (EvaluationNotEntitledError), and a call with no bearer at all gets the session error that same request would have produced — never an empty map, which at the call site is indistinguishable from a knowledge graph that genuinely holds no matches. A deployment with no knowledge graph to reach is likewise explicit: capability() reports available: false and never throws, and the queries raise KgUnavailableError rather than returning empty.
  • A key inside a slice the caller does not hold comes back present, with no matches. That is deliberate, and it pulls the opposite way from the bullet above: the caller-level refusal is loud, while the key-level absence is silent and indistinguishable from a key that matches nothing anywhere. The seeded key is a structural guarantee about the map, not a statement about the corpus — capability() is the sanctioned way to ask what this caller may reach, and it answers with entitled and sliceCount before any query is issued.
  • Rules and their threats are fetched in one pass, not one request per rule. Left as an ordinary field resolver, addresses would turn a single question about a class into one round trip per rule it has. Batching is what keeps the two modes comparable rather than merely correct: a locally-served schema resolves the same selection in a single traversal.
  • kgRules requires where.classId.eq. A call naming no class is refused rather than answered empty — no named query resolves rules by rule id alone, and an empty list would be a silent wrong answer rather than a small one.
  • The GraphQL surface is deliberately narrower than a locally-generated one. Fields the access contract does not carry are absent rather than declared-and-null, because a consumer selecting an absent field fails at query validation with the field named, while a declared field with nothing behind it returns a null that reads as missing information. KG_REMOTE_SDL is exported for exactly this reason — so a consumer can prove its document validates against the surface a remote-mode deployment serves before shipping it. KG_CAPABILITY_SDL is exported separately, and included by both modules, so a kgCapability selection validates in either mode.

DtRemoteKnowledgeGraphModule implements the knowledge-graph surface of the wire protocol; the catalog, content, and evaluation surfaces are DtRemoteModule.


DocumentDescription
BASE_CLASSES.mdImplementation patterns for DTModule
UTILITY_CLASSES.mdHelper classes (DbOps, LangGraph ops)
DEVELOPMENT_GUIDE.mdStep-by-step development guide
MODULE_CUSTOM_RESOLVERS.mdCustom resolver architecture (LLD)
MODULE_MANAGEMENT_SERVICE.mdModule upsert + afterInstall invocation mechanism (LLD)