@dna-codes/dna-core
v0.6.0
Published
DNA — a JSON description language for business systems. Ships typed schema bindings, a JSON-schema manifest, and the cross-layer validator. Schemas themselves live in @dna-codes/dna-schemas.
Downloads
76
Maintainers
Readme
@dna-codes/dna-core
The TypeScript home for DNA — typed bindings for the JSON schemas, a typed builder API for constructing Operational DNA programmatically, a DnaValidator for validating any DNA against the canonical schemas (with cross-layer checks), and a pure merge() utility for composing multiple DNA chunks into one with conflict reporting.
The raw JSON schemas live in @dna-codes/dna-schemas and are pulled in as a dependency.
| Layer | What it captures | Analogous to | |---|---|---| | Operational DNA | What the business does | Domain-Driven Design | | Product DNA | What gets built | OpenAPI + Atomic Design | | Technical DNA | How it gets built | Terraform / AWS SAM |
DNA is a contract, not a runtime. Producers (authoring agents, humans) emit JSON that conforms to these schemas; consumers (validators, viewers, code-generation cells) read the JSON and do something useful with it.
docs/operational.md— authoring contract for operational DNAdocs/product.md— authoring contract for product DNA (core + api + ui)docs/technical.md— authoring contract for technical DNAdocs/builders.md— full reference for the builder APIdocs/merge.md—merge()reference for composing DNA from multiple chunksAGENTS.md— agent contract for working with DNA at large
Contents
- Installation
- Quick start — build, then validate
- Constructing Operational DNA
- Validating Operational DNA
- Recipes
- API reference (other exports)
- Using schemas from non-JS languages
- Primitive vocabulary
- What this package does not include
- Versioning
- License
Installation
npm install @dna-codes/dna-coreQuick start — build, then validate
End-to-end: construct a small Operational DNA from scratch via the builders, validate it, get back a green light.
import {
createOperationalDna,
addResource,
addPerson,
addRole,
addOperation,
addTrigger,
addRule,
addTask,
DnaValidator,
} from '@dna-codes/dna-core'
// 1. Start from an empty domain.
let dna = createOperationalDna({ domain: { name: 'lending', path: 'acme.lending' } })
// 2. Compose primitives via the builders. Each call returns
// { dna, conflicts }; same-name composes via the merge engine.
;({ dna } = addResource(dna, {
name: 'Loan',
attributes: [
{ name: 'amount', type: 'number', required: true },
{ name: 'status', type: 'enum', values: ['pending', 'active', 'repaid'] },
],
actions: [
{ name: 'Apply', type: 'write' },
{ name: 'Approve', type: 'write' },
],
}))
;({ dna } = addPerson(dna, { name: 'Borrower' }))
;({ dna } = addRole(dna, { name: 'Underwriter' }))
;({ dna } = addOperation(dna, { name: 'Loan.Apply', target: 'Loan', action: 'Apply' }))
;({ dna } = addOperation(dna, { name: 'Loan.Approve', target: 'Loan', action: 'Approve' }))
;({ dna } = addTrigger(dna, { operation: 'Loan.Apply', source: 'user' }))
;({ dna } = addRule(dna, { operation: 'Loan.Approve', type: 'access', allow: [{ role: 'Underwriter' }] }))
;({ dna } = addTask(dna, { name: 'approve-loan', actor: 'Underwriter', operation: 'Loan.Approve' }))
// 3. Validate.
const validator = new DnaValidator()
const schemaResult = validator.validate(dna, 'operational')
const crossResult = validator.validateCrossLayer({ operational: dna })
if (!schemaResult.valid) console.error(schemaResult.errors)
if (!crossResult.valid) console.error(crossResult.errors)
// Both green for the DNA above.That covers the two responsibilities — builders give you a typed, schema-aware way to construct DNA, and DnaValidator confirms the result holds together at the JSON-Schema level and across primitive references.
Constructing Operational DNA
Builders — the canonical way to construct DNA
createOperationalDna(opts) returns an empty DNA shell. Eleven add* builders compose primitives into it: every Operational primitive (Resource, Person, Role, Group, Membership, Operation, Trigger, Rule, Task, Process) plus addRelationship. Every add* has the same shape:
function addResource(
dna: OperationalDNA,
resource: Resource,
opts?: { validate?: boolean }
): { dna: OperationalDNA; conflicts: Conflict[] }Properties guaranteed by every builder:
- Pure & immutable — never mutates the input DNA. Same input, same output, every time.
- Compose-on-duplicate — same-name composes via the merge engine. List-shaped children union by name; scalar disagreements emit
Conflictentries. Builders never throw on a duplicate name. - Default-on schema validation — catches malformations TypeScript can't express (regex patterns like
^[A-Z][a-zA-Z0-9]*$onResource.name, conditional requirements likevalueswhenAttribute.type === 'enum', the source-of-truth enum lists). Pass{ validate: false }for hot paths where input is already known valid. - Compile-time typed — input parameters are typed against each primitive's schema. Misuse fails at build time, not just at runtime.
// Compile-time error: name must be a string
addResource(dna, { name: 123 })
// Compile-time error: action is required on Operation
addOperation(dna, { target: 'Loan' })
// Runtime error: enum requires `values`
addResource(dna, { name: 'Loan', attributes: [{ name: 'status', type: 'enum' }] })
// throws: "operational/resource input failed validation: /attributes/0 must have required property 'values'"Auditing compose-on-add disagreements:
let r = addResource(dna, { name: 'Loan', description: 'Consumer loan' })
const r2 = addResource(r.dna, { name: 'Loan', description: 'Mortgage product' })
// r2.conflicts contains:
// { path: 'resources.Loan.description',
// values: [{ value: 'Consumer loan', source: ... }, { value: 'Mortgage product', source: ... }],
// recommendation: { value: 'Mortgage product', reason: 'stable-order: tied on all heuristics; first-observed wins' },
// kind: 'scalar' }Full reference and patterns: docs/builders.md.
merge() — compose multiple chunks at once
Compose multiple Operational DNA chunks into one. Pure, deterministic, no I/O — designed for the multi-source ingest case (@dna-codes/dna-ingest calls this as its fan-in step) but usable on its own.
import { merge } from '@dna-codes/dna-core'
const result = merge([
{ dna: chunkA, source: { uri: 'file:///sop.md', loadedAt: '2025-01-01T00:00:00.000Z' } },
{ dna: chunkB, source: { uri: 'gdrive://policy', loadedAt: '2025-06-01T00:00:00.000Z' } },
])
result.dna // merged OperationalDNA — validates against the operational schema
result.conflicts // Conflict[] — scalar disagreements + unresolved-ref warnings
result.provenance // dotted-path → contributing sources, e.g. 'resources.Loan' → [...]Same-named primitives unify across chunks. List-shaped children union by name. Scalar disagreements use the v1 recommendation policy:
- Value backed by the most distinct sources wins.
- Tie-break by most recent
loadedAt. - Tie-break by longest non-empty string representation.
- Final tie-break: stable input order.
The recommendation is written into the merged DNA so the result is always schema-valid; the full list of competing values + sources rides along in Conflict.values for review. Cross-references (Operation.target, Membership.{person,role,group}, Trigger.operation, Task.{actor,operation}, Step.task, Rule.operation) resolve against the merged noun set; unresolved references surface as Conflict entries with kind: 'unresolved-reference' (the referencing primitive is still emitted so a reviewer can fix the source documents).
Full reference: docs/merge.md.
Builders vs. merge() — when to reach for which
| Use case | Reach for |
|---|---|
| Building DNA from code (fixtures, tests, a custom adapter, programmatic composition) | Builders |
| Composing N chunks produced by separate sources (input adapters, multi-document ingest) | merge() |
| Adding one primitive to an existing DNA, audit-able with conflict reporting | Builders |
| Resolving cross-references after composition | Both — merge() does it as its final step; builders rely on the same engine |
Both share the same composition engine internally; pick the surface that matches your call shape.
Validating Operational DNA
DnaValidator — the validator
AJV-backed. All DNA schemas (per-primitive + per-layer aggregates) pre-registered. No setup beyond new DnaValidator().
import { DnaValidator } from '@dna-codes/dna-core'
const validator = new DnaValidator()Per-document validation
validate(doc, schemaId) validates a document against any registered schema, by short or full ID:
import operational from './dna/lending/operational.json'
validator.validate(operational, 'operational') // full document
validator.validate(operational.domain.resources[0], 'operational/resource') // single primitiveThe result is { valid: boolean; errors: ErrorObject[] } — AJV's standard error shape. Common interpretation pattern:
const result = validator.validate(operational, 'operational')
if (!result.valid) {
for (const err of result.errors) {
console.error(` ${err.instancePath || '/'} ${err.message}`)
if (err.params) console.error(` params:`, err.params)
}
}Common error classes you'll see (and why each exists):
| Error | What triggered it |
|---|---|
| must have required property 'X' | A required field is missing. Check the primitive's schema in @dna-codes/dna-schemas for the required list. |
| must match pattern "^[A-Z][a-zA-Z0-9]*$" | A name doesn't match its naming convention. PascalCase for nouns; PascalCase verbs for actions; kebab-case for tasks/processes. |
| must be equal to one of the allowed values | An enum field got an unknown value (e.g. Attribute.type: 'invalid' or Trigger.source: 'manual'). |
| must NOT have additional properties | An unknown field on a primitive (typo, or a field that belongs on a different primitive). |
| must have required property 'values' (on an Attribute) | Attribute.type === 'enum' requires a values array — schema's conditional rule. |
| must have required property 'schedule' (on a Trigger) | Trigger.source === 'schedule' requires a schedule cron expression. |
Cross-layer validation
Schema validation alone confirms shape, not consistency. validateCrossLayer() walks references between primitives and across layers:
const result = validator.validateCrossLayer({
operational,
productCore, // optional
productApi, // optional
productUi, // optional
technical, // optional
})Examples of what cross-layer catches:
- Operational:
Operation.targetreferences a noun that doesn't exist;Task.actorreferences a Role/Person not declared;Process.steps[].taskreferences an undeclared Task;Membership.person/.role/.groupreferences a primitive not declared; Role.scope/.parent cycles; Role exclusion pairs whose effective scopes don't intersect; etc. - Operational → Product Core: every Product Core Resource/Operation must be present in Operational (Product Core never invents).
- Product API → upstream: every API Resource/Operation must trace back to Product Core (preferred) or Operational (fallback).
- Product UI → Product API: every Page Resource and Block Operation must be declared in Product API.
- Technical: every Construct's provider must be declared; every Cell's constructs must be declared.
The result is { valid: boolean; errors: { layer, path, message }[] }. The errors are designed to be human-readable directly:
operational operations/Loan.Approve/target Operation "Loan.Approve" target "Loanz" is not a declared
Resource, Person, Role, Group, or Process; available
targets: "Loan", "Borrower", "Underwriter"availableSchemas()
validator.availableSchemas()
// → ['https://dna.codes/schemas/operational/resource', 'operational/resource', ...]Useful for diagnostics; both the full $id and the short form (without the https://dna.codes/schemas/ prefix) are registered for every schema.
Validating untrusted input
function validateOperationalDna(json: unknown):
| { ok: true; dna: OperationalDNA }
| { ok: false; errors: { schema?: ErrorObject[]; cross?: CrossLayerError[] } } {
const validator = new DnaValidator()
const schema = validator.validate(json, 'operational')
if (!schema.valid) return { ok: false, errors: { schema: schema.errors } }
const cross = validator.validateCrossLayer({ operational: json })
if (!cross.valid) return { ok: false, errors: { cross: cross.errors } }
return { ok: true, dna: json as OperationalDNA }
}Schema validation tells you the shape is right. Cross-layer validation tells you the references hold together. Both run together when you need full confidence.
Recipes
Build then validate
Already shown in the quick start. The pattern:
let dna = createOperationalDna({ domain: { name: 'd' } })
;({ dna } = addX(dna, ...))
const validator = new DnaValidator()
const r = validator.validate(dna, 'operational')The builders' default-on schema validation already ensures every individual primitive validates; the post-hoc DnaValidator.validate(dna, 'operational') confirms the aggregate document is well-formed and validateCrossLayer({ operational: dna }) confirms references resolve.
Audit conflicts produced during construction
let dna = createOperationalDna({ domain: { name: 'd' } })
const accumulated: Conflict[] = []
for (const r of incomingResources) {
const result = addResource(dna, r)
dna = result.dna
accumulated.push(...result.conflicts)
}
if (accumulated.length > 0) {
console.warn(`composed ${accumulated.length} disagreements:`)
for (const c of accumulated) {
console.warn(` ${c.path}: ${c.values.length} values, picked: ${JSON.stringify(c.recommendation.value)} (${c.recommendation.reason})`)
}
}Merge multiple sources, then validate
import { merge, DnaValidator } from '@dna-codes/dna-core'
const result = merge([
{ dna: chunkFromDrive, source: { uri: 'gdrive://abc', loadedAt: '...' } },
{ dna: chunkFromSopFile, source: { uri: 'file:///sop.md', loadedAt: '...' } },
])
const validator = new DnaValidator()
const schemaResult = validator.validate(result.dna, 'operational')
const crossResult = validator.validateCrossLayer({ operational: result.dna })
if (!schemaResult.valid || !crossResult.valid || result.conflicts.length > 0) {
// Decide whether to ship the partial DNA, raise an alert, or fail the run.
}Skip validation in hot paths
Builders default to validate: true. For inner loops processing many primitives that are already known to validate (e.g., re-emitting a previously-validated DNA), opt out:
;({ dna } = addResource(dna, knownGoodResource, { validate: false }))merge() is also a hot path: it calls into the same composition engine via builders without re-validating per primitive — it's designed for chunks that are already schema-valid by construction.
Inspect what the validator covers
const validator = new DnaValidator()
console.log(validator.availableSchemas().filter(id => !id.startsWith('https://')))
// → ['operational/action', 'operational/resource', 'operational',
// 'product/core/resource', 'product/api/endpoint', ...]API reference (other exports)
schemas — typed access to every per-primitive JSON Schema
import { schemas } from '@dna-codes/dna-core'
schemas.operational.resource // Resource schema (Draft 2020-12)
schemas.product.core.role // Product Core Role schema
schemas.product.api.endpoint // Product API Endpoint schema
schemas.product.web.page // Product UI Page schema
schemas.technical.cell // Technical Cell schema
schemas.operational.resource.$id
// → 'https://dna.codes/schemas/operational/resource'documents — per-layer aggregate schemas
import { documents } from '@dna-codes/dna-core'
documents.operational // shape of operational.json
documents.productCore // shape of product.core.json
documents.productApi // shape of product.api.json
documents.productUi // shape of product.ui.json
documents.technical // shape of technical.jsonallSchemas() — flat array
Convenient for bulk-registering with a JSON Schema validator:
import Ajv from 'ajv'
import { allSchemas } from '@dna-codes/dna-core'
const ajv = new Ajv({ strict: false, allErrors: true })
for (const s of allSchemas()) ajv.addSchema(s)resolveSchemaFile(family, name)
Returns the on-disk path of a schema file, or null if it doesn't exist:
resolveSchemaFile('operational', 'resource') // → '/abs/.../resource.json'
resolveSchemaFile('product', 'api/endpoint') // → '/abs/.../api/endpoint.json'
resolveSchemaFile('operational', 'ghost') // → nullSCHEMA_ROOT, layerDirs
Filesystem roots for consumers that walk the tree themselves:
SCHEMA_ROOT // .../node_modules/@dna-codes/dna-schemas
layerDirs.operational // .../schemas/operational
layerDirs.product // .../schemas/product
layerDirs.technical // .../schemas/technicalRaw JSON schemas
To import an individual schema directly, depend on @dna-codes/dna-schemas:
import resourceSchema from '@dna-codes/dna-schemas/operational/resource.json'TypeScript types for every primitive
The package exports per-primitive types alongside the runtime API. Useful for typing function signatures, fixtures, or your own builders:
import type {
OperationalDNA,
Resource, Attribute, Action,
Person, Group, Role, RoleScope,
Membership,
Operation, OperationChange,
Trigger, TriggerSource,
Rule, RuleAllowEntry, RuleCondition, RuleConditionOperator, RuleType,
Task,
Process, ProcessStep,
Relationship, RelationshipCardinality,
Domain,
Conflict, ConflictRecommendation, ConflictValue, Source, Provenance,
MergeChunk, MergeResult,
} from '@dna-codes/dna-core'A contract test in this package re-validates each schema's examples[] through the corresponding TypeScript type at build time — if a JSON Schema gains a field the TypeScript type is missing, the build fails.
Using schemas from non-JS languages
Install @dna-codes/dna-schemas directly — zero dependencies, ships only the JSON files:
node_modules/@dna-codes/dna-schemas/
operational/*.json # 15 primitive + 1 aggregate
product/core/*.json # 5 primitives
product/api/*.json # 4 primitives
product/web/*.json # 4 primitives
product/product.{core,api,ui}.json # 3 aggregates
technical/*.json # 11 primitives + 1 aggregateSchemas cross-reference each other by absolute URI (e.g. https://dna.codes/schemas/operational/attribute), so your validator must register all schemas before validating any one of them. The allSchemas() helper does this for you in JS; reproduce the same load-everything-then-validate pattern in your target language's validator.
Primitive vocabulary
| Layer | Primitives |
|---|---|
| Operational | Resource, Action, Operation, Attribute, Domain, Relationship, Trigger, Rule, Person, Role, Group, Membership, Task, Process |
| Product | Resource, Action, Operation, Layout, Page, Route, Block, Field, Namespace, Endpoint, Schema, Param |
| Technical | Environment, Cell, Construct, Provider, Variable, Output, Script, View, Node, Connection, Zone |
Operational is modeled around the Actor > Action > Subject triad. Resource and Action appear at both the Operational and Product layers by design — a Product Resource is the surface projection of an Operational Resource, likewise for Action. The Actor is a Role (or Person) referenced by Rule (access), Task (assignment), Membership (eligibility), and Process (operator), rather than declared on the Operation itself. State mutations live on Operation.changes; there is no separate Outcome primitive. See each layer's doc in docs/ for full semantics.
What this package does not include
- The raw JSON schemas. Those live in
@dna-codes/dna-schemas(core depends on it). - A CLI. See
@cell/cba(command:cba) for the full authoring lifecycle. - Cell runtimes. Cells are separate consumers of DNA — see
technical/cells/*. - Multi-source orchestration.
@dna-codes/dna-ingestfans many sources into one DNA via themerge()from this package.
Versioning
DNA schemas are the contract; breaking changes require a major version bump. $id URIs (https://dna.codes/schemas/<layer>/<primitive>) are stable identifiers and will not change without a deprecation path.
License
MIT.
