DataShield Ontology · documentation

Development & Operations

Audience: developers and operators of the Library (v0.22.188, package.json). Method: every claim below was measured from the code or the governing document in this tree on 2026-09-22, cited as file:line. Where something is a plan rather than shipped code it is marked PLANNED or DESIGN. Normative keywords follow RFC 2119.


On this page

1. The async job system

1.1 Two queues, two workers

Queue tableConsumerClaim styleEvidence
registry.async_jobspdl-job-worker (scripts/job-worker.ts)poll every JOB_WORKER_POLL_MS (default 1000 ms), FOR UPDATE SKIP LOCKED (STANDARD-006 P2)scripts/job-worker.ts:13-14, :71, :700
conduit.analysis_jobspdl-worker (scripts/worker.ts) and pdl-worker-large (scripts/worker-large.ts)analysis loop: claim → fork child → donescripts/worker.ts:290, :588, :1148; scripts/worker-large.ts:1-8

The generic queue lifecycle is queued → running → (completed | failed | cancelled) (scripts/job-worker.ts:16). The worker is stateless and restartable: on restart, running rows older than 5 minutes with no recent updated_at are reaped to failed (:24-26, reapStaleRunning, :617 exempts legitimately long kinds such as crawler.project_run).

1.2 Job kinds and handlers

Handlers live in one map, const HANDLERS: Record<string, JobHandler> at scripts/job-worker.ts:105 (the map closes at :430). Kind names are imported from config, never typed in the worker (headline rule) — with five string-literal exceptions (three crawler kinds and two dataset_analysis.* kinds). Measured: 16 config-keyed entries plus 5 literals = 21 kinds (grep -nE '^\s{2}(\[|")' scripts/job-worker.ts, lines 105-430; portal-to-ontology-development-writeup.md:102 counts 21 independently).

Kind (resolved string)Config sourceHandler line
workspace_schema_maintenanceconfig/rag.config.ts:1141:109
mdm.resolution_runconfig/mdm-resolution.config.ts:65:121
mdm.config_trainconfig/mdm-resolution.config.ts:251:140
mdm.rule_run / mdm.rule_backfillconfig/mdm-stewardship.config.ts:338-339:163 / :170
mdm.proposal_prepareconfig/mdm-preparer.config.ts:77:183
mdm.recommendation_refreshconfig/recommendation-generation.config.ts:70:195
mdm.recommendation_attributionconfig/recommendation-attribution.config.ts:47:211
mdm.quality_axis_refreshconfig/mdm-quality.config.ts:473:226
rag.usage_rollupconfig/rag-telemetry.config.ts:99:246
corpus_ingest / corpus_reindex / corpus_resplitconfig/rag.config.ts:2099,2112,2151:257 / :277 / :296
corpus_fetchconfig/rag.config.ts:2467:305
catalog.terms_materializeconfig/data-catalog.config.ts:3693:310
crawler.project_run / crawler.source_run / crawler.portal_discoverliterals (config/crawler.config.ts:228 for the first):316 / :334 / :362
dataset_analysis.preflightliteral (the dataset_ingest.* handler twins were deleted at LIB-BUG-287, :373-377):378
dataset_ingest.physical_loadconfig/physical-masking.config.ts:78:389
dataset_analysis.submitliteral (thin delegation to the conduit analysis queue — the async_jobs row shadows the conduit job and bridges status back):404

A handler receives (params, isCancelled, job). Cancellation is cooperative: the worker checks cancel_requested each poll and a handler halts at its next checkpoint (:18-22); crawler handlers refuse with cancelled_before_start if cancelled while queued (:324, :335, :363).

1.3 Lanes

config/job-lanes.config.ts declares named lanes; each runs at most one job at a time on its own DB connection, and the implicit default lane runs every kind not claimed (:10-17). Measured lanes (:36-49):

LaneKindsWhy
bulkcorpus_ingest, corpus_reindex, corpus_resplit, workspace_schema_maintenanceLIB-BUG-698 head-of-line; LIB-BUG-791/766 single-slot mutual exclusion
CORPUS_UPLOAD_CONFIG.fetchLane.laneNamecorpus_fetchmulti-GB pulls must not starve small ingests (LIB-RAG-1 D3 F13)
defaulteverything else (MDM, analysis, masking, catalog)pre-lane semantics preserved

Enrolling a heavy kind is one config row, no worker edit (:16-17). The same file declares SYSTEM_ENQUEUED_JOB_KINDS (:80; rationale :54-79): kinds the platform enqueues with created_by IS NULL, which the job umbrella MUST NOT expose to a non-admin keyless caller (LIB-BUG-946 V-9).

1.4 Durable ticks

Time-gated schedulers (quality-axis refresh, recommendation refresh/attribution, RAG usage report) are gated on registry.scheduled_tick_state (scripts/job-worker.ts:43, :819-825) via armDurableTick(...) (defined :828; armed at :841, :960, :979, :991 — one per named tick). Policy lives in config/scheduled-ticks.config.ts: pollCadence: "PT5M", claimGrace: "PT10S", maxCatchUpWindows: 1, durabilityRequiredAtOrAbove: "PT30M" (ISO 8601 durations, :51-60); tick names at :88-92. Operators: a healthy boot logs durable-tick gate healthy (no degraded ticks); durable-tick DEGRADED means the schema is behind the code — migrate first (LIB-BUG-1029; CLAUDE.md release step 5).

1.5 The job umbrella (retry, cancel, correlation ids)

scripts/mcp-server/umbrellas/job.ts commands (measured case labels): get (:172), list_mine (:287), cancel (:334), retry (:396), history (:446), list_all (:504), stats (:533), reset_circuit_breaker (:552). Read-only get is open to any caller; listing/mutation of the analysis queue is admin-only (:168). retry is owner-scoped (asyncOwnerScope, LIB-BUG-428) and honours the failed/cancelled-only contract (:396-410); the analysis-queue status vocabulary is pending|running|completed|failed|retry|cancelled (:130).

Failures carry a correlation id. On handler failure the worker mints randomUUID() and stores {...err, correlation_id} in the row while logging failure correlation <id> for <short_id> (<code>) (scripts/job-worker.ts:588-596); reaped rows each get their own id (:656, :664). Operators: grep pm2 logs pdl-job-worker for the id a caller reports.


2. pm2 processes (ecosystem.config.cjs)

All processes run from the PRIMARY tree /home/datashield/library; worktree builds never deploy. DATABASE_URL is declared once (:13) and PDL_EXPECTED_DATABASE is derived from it so every process fails closed on a foreign current_database() (:14, LIB-BUG-719). Never pm2 reload --update-env (:12, :339, LIB-BUG-719). The three auxiliary workers are sourced from lib/workers/*.ts, not scripts/ (scripts/build-scripts.sh:84-86).

ProcessEntry (args)RoleLines
pdl-webnpm run start, PORT 3002Next.js UI + REST bridge (/api/*, /api/health):28-30
pdl-mcpdist-scripts/mcp-server.mjs --http, MCP_PORT 3100MCP server (all umbrellas); UNIVERSAL_MIDDLEWARE_ENABLED="true":64-66
pdl-workerdist-scripts/worker.mjs (16 GB heap)analysis loop over conduit.analysis_jobs, crons, crawler queue, child ingest:105-108
pdl-worker-largedist-scripts/worker-large.mjs (18 GB heap, 4 h drain)large/high-priority analysis only:146-151, :182-183
pdl-anomaly-detectordist-scripts/anomaly-detector.mjs (lib/workers/anomaly-detector.ts)polls mcp_call_log every 60 s; last-5-min rate > 5× the rolling 60-min baseline → registry.alerts_queue INSERT (anomaly-detector.ts:4-8):191-193
pdl-job-workerdist-scripts/job-worker.mjs (2 GB heap)§1 generic queue + durable ticks; MUST restart every release (LIB-BUG-565):218-221
pdl-alerts-dispatcherdist-scripts/alerts-dispatcher.mjs (lib/workers/alerts-dispatcher.ts)drains alerts_queue; only the internal channel (structured log line) delivers — slack/email are STUBS that record channel_not_implemented and dispose the row (alerts-dispatcher.ts:9-13, :179):258-260
pdl-billing-reconciledist-scripts/billing-reconcile.mjs (lib/workers/billing-reconcile.ts), cron_restart "0 6 * * *"one-shot daily reconciliation of mcp_call_log against api_usage_daily / api_usage_mcp_daily; each drift row → alerts_queue (billing-reconcile.ts:5-17):290-305

Which bundle each consumer needs, and the reload verb/order, is declared in config/bundle-consumers.config.ts (CONSUMER_RELOAD_POLICY) and consumed by npm run release:reloads (§3).


3. The release process as it actually runs

Authoritative text: CLAUDE.md § "canonical release-event sequence". TAG LAST. Summary of the sequence with the commands a runner types:

StepCommandNotes
0npm run release-lock:acquire -- --holder <s> --purpose "release vX.Y.Z"STANDARD-007; release-lock:check from the PRIMARY tree with RELEASE_LOCK_HOLDER set (package.json:328-331)
0npm run release:build-cycle -- open --version vX.Y.Z --input '{...}'writes registry.build_cycles (scripts/release-pipeline/13-build-cycle.ts, package.json:347)
2a0ledger reconciliationonly if DDL was applied out of band under another filename
2a0.1npm run release:must-declare -- --sprint <branch>generates the must-declare list into docs/audit/vX.Y.Z-merge-ledger.md (package.json:342)
2a1RELEASE_LOCK_HOLDER=<s> npm run release:migrateapplies pending migrations INSIDE the lock, before gates (LIB-BUG-1057; package.json:339)
2a2npm run release:gatesthe HARD gate chain; 205 links at v0.22.188 (measured: node -e "…split('&&').length"). Never hand-edit; use npm run gates:splice (package.json:341). Then release:build-cycle -- gates (state-less; per-link exits + red attribution; LIB-BUG-1166) and -- merge
2bRELEASE_VERSION=vX.Y.Z npm run release:genrelease:snapshotrelease:diff → release noteswrites docs/versions/vX.Y.Z/ (write-once)
2b2/2b3npm run audit:snapshot-version-agreement, RELEASE_VERSION=… npm run audit:test-prompt-shape-validityHARD, run after snapshot and before publish
2cnpm run release:publish -- --snapshot=docs/versions/vX.Y.Z --version=vX.Y.Zauto-commit
2d/2eversion-surface grep audit → package-bumpbump commit
3-4PAUSE "ready to tag at SHA"; operator git tag -a vX.Y.Z <sha>build-cycle tag REFUSES without gates_final_exit
5npm run release:migrate againexpected PLAN: nothing pending — no-op
6npm run release:reloads (scripts/release-reload-chain.ts, package.json:306)derived from CONSUMER_RELOAD_POLICY; terminates in strict audit:bundle-freshness. NO hand restarts
6brelease:build-cycle -- deploy / verify / reportthen audit:build-cycle-completeness --strict, audit:bug-provenance-coverage --strict
7npm run release:report (T-8)LAST; --snapshot required (scripts/release-pipeline/07c-gen-release-report.ts:16,113); saved via the report umbrella at slug release-vX.Y.Z (:173-219); checked by audit:release-report-currency

Builds: scripts/build-scripts.sh produces every dist-scripts/*.mjs (esbuild, type-stripped) and is REQUIRED before any bundle-consumer reload; env NODE_ENV=production npm run build produces .next/ for pdl-web (LIB-BUG-201 disaggregation, LIB-BUG-266).

3.1 Dev-gov rules a contributor MUST know

RuleWhere it is writtenWhat it means for you
Independent close vet is non-waivableCLAUDE.md META-CALIBRATION 7; docs/CC-DIRECTIVES.md:263 (Directive 9)every substantive change ships only after an adversarial vet by a session other than its author; live verification exercises the real queue/door path
Findings define classes, not instancesCLAUDE.md META-CALIBRATION 8grep every sibling site of a finding before closing it; re-vets hunt unswept siblings first
Must-declareconfig/release-ledger-contract.config.ts; audit:must-declare-completenessevery merged bundle ledger declares its release-notes section or a written waiver; the gate blocks otherwise
.describe() coverageCLAUDE.md rules 7/7b/7c; scripts/release-pipeline/_lib/enforced-describe-umbrellas.tsenrolled umbrellas HARD-BLOCK the release on any undescribed required param; new umbrellas enroll at creation
No new MCP toolsdocs/audit/portal-migration-1-ratification.md:55 (cited by writeup §6 iv-a); npm run audit:umbrella-inventorynew capability is a command on an existing umbrella; the tool count is reconciled against config/mcp-tiers.config.ts
Worktree preflightnpm run agent:preflight -- --branch <b> --base <ref> [--detach <sha>] --role builder|vetter|runner (package.json:340)creates /home/datashield/library-wt-<name>; refuses under an ambient DATABASE_URL; check ls -ld node_modules before any install (symlink hazard)
Hooks.husky/pre-commit (anti-sprawl: audit:duplicate-detector --staged, audit:justification-required --staged; every run logs to .husky/.audit-log); .husky/pre-merge-commit (LIB-BUG-1273: clean merges fire this hook)--no-verify is PROHIBITED by policy; a commit-count vs log-count mismatch is the bypass signal
Path-scoped commits, --no-ff merges, never delete sprint branchesCLAUDE.md "Sprint forensic ledger discipline"the sprint branch is the forensic ledger
Standardsdocs/standards/STANDARD-001…011001 report scroll, 004 server-side authz, 006 concurrency (row-scoped patterns P1-P5, never table locks), 007 release lock, 008 master-detail surfaces, 009 privacy/masking, 010 connection paths; 011 (catalog north star) is still -DRAFT

4. The bug registry (artifact umbrella)

Bugs are registry-backed rows in registry.bugs with a generated file; ids come only from the writer. Measured from the tool description in scripts/mcp-server/umbrellas/artifact.ts:

CommandLineBehaviour
bug_submit({title, description, severity?, umbrella?, sprint?, discovered_by?, reproducer?, affected_versions?, related_bugs?, metadata?, auto_commit?}):129canonical filer: DB-sequence id LIB-BUG-NNN, tmp+rename atomic write, INSERT, optional auto-commit. Replaces client-picked ids (LIB-BUG-129..138 collisions)
bug_get({id}):130body + provenance (introduced_in/by/how, detected_by, resolved_in/by, regression_evidence, bisect_owed) + final_resolution
bug_list({umbrella?, sprint?, status?, severity?, limit?, cursor?}):131paginated, id-DESC, cursor = last id
bug_update({id, status?, severity?, metadata?, body_append?}):132a flip to resolved writes the provenance record and renders ## Final resolution; pass metadata.resolved_in at release events (CLAUDE.md 6b)

artifact.create({type:'bug'}) FORWARDS to bug_submit (:119); artifact.update/bulk_update/convert REJECT bug ids (:123-126). Directive 1 (docs/CC-DIRECTIVES.md:10) governs the bug file lifecycle.


5. The Development section (being built)

Sources: docs/audit/dev-section-charter.md §3-§4 and docs/audit/portal-to-ontology-development-writeup.md §1, §6. Status at v0.22.188: step 0 ratified in documents; no step-i code has been measured as shipped in this tree. Treat everything below as the plan of record.

5.1 Architecture (charter §3, dev-section-charter.md:28-46)

Development (nav section, config row)
 ├─ Monitor   — read models: Releases (build_cycles) · Bugs · Estate · Jobs        [step i]
 ├─ Pipelines — DEFINITIONS (registry.pipelines + append-only revisions)
 │             + RUNS (lineage.runs, one per node execution)                       [ii, iii-a, iv]
 └─ Lineage   — in-house canvas, third VIEW_SOURCE `openlineage_runs`, one pipeline run  [iii-b]

Rules (RFC 2119, charter §3): definitions reference catalog assets by id and MUST NOT carry a value (scanForValueLeak); the run ledger MUST be the OpenLineage store — no second run table; the executor MUST be one job kind pipeline.run on the existing queue (§1), cadence via the durable tick; every surface follows STANDARD-008.

5.2 Step i — the slice being built (PLANNED; writeup §6 rows i-a…i-f)

Sub-stepWhatWireMigrations
i-aNav section config/development-nav.config.ts (mirror data-catalog-nav.config.ts:180-213) + app-nav.config.ts edits + nav testnonenone
i-bDevelopment app shell app/app/development/ + sections/index.ts totality assert + lib/contracts.tsnonenone
i-cBridge door app/api/app/development/[verb]/route.ts derived from a wire contract + COMMAND_AUTH; RFC 9457 refusalsREST over EXISTING MCP commandsnone
i-dMonitor lanes Bugs (artifact.bug_list), Jobs (job.list_all/stats), Estate (link to catalog bridge) via useSectionFeed; STANDARD-009 counts-onlyREST onlynone
i-eReleases lane: ONE read command over registry.build_cycles on an existing umbrella (the single wire addition; 7b enrolment + must-declare)1 additionnone
i-fBlank-lineage defect: set anchor on app-nav.config.ts:464-473 (anchor: null at :470) (page requires start, app/data-catalog/lineage/page.tsx:100-104)nonenone

Step i total ≈ 455k naive tokens (≈ 680k at ×1.5) + vet ≈ 200k + runner ≈ 300k; zero migrations (charter §4).

5.3 Roadmap ii-iv (PLANNED — NOT SHIPPED)

StepWhatGate
ii-aregistry.pipelines + pipeline_revisions + config/pipeline-model.config.ts; CRUD on data_catalog (1 migration)D-3 ownership + append-only; D-8 retire docs/specs/pipeline-monitor-ui-spec.md first
ii-bOpenLineage store M-1 + M-2 + config/openlineage.config.ts (2 migrations, +1 contingent)O-1..O-6 ruled; D-5 heal-first
ii-creal lib/server/lineage/emit.ts (does not exist today; lib/data_catalog/extract/lineage.ts:109-116 is a self-declared stub); gate → blockD-5
iii-aPort the Portal editor UI (real 9,020-line React-Flow front end, backend absent) onto ii-a and @xyflow/react@12D-5 design session; gated on ii-a
iii-bthird VIEW_SOURCES entry openlineage_runs (config/lineage-view.config.ts:401) + facet rendererO-6, LIB-BUG-1209
iv-aexecutor: job kind pipeline.run in HANDLERS + lane row; status via existing job.get/list_all/historyW1 catalogWrite() shipped; D-6
iv-bONE enqueue seam over the 17 non-test files carrying INSERT INTO registry.async_jobs (58 raw occurrences; writeup §6 E-5)D-6
iv-cW1 sinksout of scope

Sequencing constraints (writeup §6): ii-a before iii-a; D-8 before ii-a; heal O-1..O-6 in code before ii-b; ii-b before iii-b; W1 before iv-a; iv-b before or with iv-a.


6. Lineage today and the OpenLineage roadmap

6.1 Today: the edges umbrella over asset_edges

scripts/mcp-server/umbrellas/edges.ts is read-only by contract — there is no edge-write command (:4, :51); relationships are written by the pipelines that own them (the corpus splitter writes part_of/cites_section). Commands (:55-92): predicates, neighbors (depth-1), traverse (bounded BFS, every hop gated), view (layered graph for a canvas), impact (reverse closure). Storage: migrations/registry/0190_asset_edges.sql; config/edges.config.ts:17 states the one rule — asset_edges stores ONLY otherwise-discarded relationships; everything else is read_through from its owning table (:42, :64). Measured state (writeup §1): lineage is real for RAG chunks (17,888 document→document edges) and absent for data — the lineage.* schema has 0 tables.

6.2 DESIGN: OpenLineage-native lineage (docs/specs/lineage-openlineage-design.md)

D1 canonical model Dataset / Job / Run (:62-101); D2 ONE run plane lineage.datasets / runs / io, relationship plane stays in asset_edges (:111); D3 emitter enrolment contract (:174); D4 wire: one read, one new command, no new tool (:228); D5 asset detail = three lists, no new tables (:270). Operator rulings 2026-09-16 (:346-367):

DecisionRuling
O-1 namespacesopaque (datashield://provider/<id>) by default; per-provider interop opt-in
O-2 Marqueznot hosted this sprint; edges.export targets its ingest format
O-3 retentionkeep all runs 90 days, then fold to latest run per (job, dataset, io) + monthly counts
O-4 golden recordsDatasets by type, never per record (STANDARD-009)
O-5 backfillnone in L1a
O-6 facetsstandard dataQualityMetrics carries counts only; richer analysis travels as custom value-free datashield_* facets (:369+)

None of D1-D7 is implemented at v0.22.188 (lib/server/lineage/emit.ts absent; writeup §1).


7. Observability surfaces

UmbrellaFileCommands (measured)Reads
timelinescripts/mcp-server/umbrellas/timeline.tsmake :131, get :136, render :150, publish :155, list :238, lineage :248, analysis :282, super :316registry.timeline_spec, registry.storyline (:143, :161), PHI determinations gate (:173)
mcp_logscripts/mcp-server/umbrellas/mcp_log.tsquery :270, stats :381registry.mcp_call_log (:348), registry.mcp_sessions (:173), account scoping via registry.api_keys (:309)
health_checkscripts/mcp-server/umbrellas/health_check.tsno command discriminator — a flat object (:206) taking mode (:222) = quick (default, any caller) | full (admin-tier: doctor report, config, hardware, queue, table sizes, egress) | worker (admin-tier: heartbeat analytics) and format (:223) = json | text. STANDARD-003 extracted umbrella; v0.22.14 R2 sweep gave it the canonical compact MCP error envelope (:1-6); REST twin GET /api/health on pdl-web (CLAUDE.md preflight)DB connectivity + latency, migration status (pending is always a COUNT), worker-heartbeat freshness, dataset + active-key counts, rollout_mode
alertsno alerts umbrella exists; the surface is stewardship.alerts_queue (scripts/mcp-server/umbrellas/stewardship.ts:835)queue rows produced by pdl-anomaly-detector and pdl-billing-reconcile, drained by pdl-alerts-dispatcher (§2)registry.alerts_queue

The MCP surface uses the compact canonical MCP error envelope; problem+json (RFC 9457) is REST-only (DEC-1 / LIB-BUG-318; lib/server/problem.ts).


8. Where to read release notes and reports

ArtifactLocationProduced by
Release notes + capability manifest + per-tool docs + diffsdocs/versions/vX.Y.Z/ (RELEASE-NOTES.md, capability-manifest.json, diff-*.json, tools/) — write-once, 1 directory per tagrelease:gen / release:snapshot / release:diff (§3 step 2b)
Release event ledgerdocs/audit/vX.Y.Z-release-event.md (default --event-ledger, 07c-gen-release-report.ts:20)the release runner, during the event
Merge ledger with must-declare blockdocs/audit/vX.Y.Z-merge-ledger.mdrelease:must-declare
T-8 release reportreport umbrella, slug release-vX.Y.Z (07c-gen-release-report.ts:173-219); also the local file the script writesnpm run release:report, generated LAST (§3 step 7)
Build-cycle recordregistry.build_cycles (read via release:build-cycle; the PLANNED i-e Releases lane)13-build-cycle.ts
Fresh-client test prompt per releasedocs/audit/vX.Y.Z-fresh-client-test-prompt.md (e.g. v0.22.188)operator requirement, every cycle close
Live discovery_helloux.help / ux.docs_get / ux.versions_list with versionpdl-mcp

Operator quick checks: pm2 list && curl -s localhost:3002/api/health; git status && git log --oneline -3; npm run release-lock:status; npm run release:reloads -- --plan; npm run release:migrate -- --plan.

Measured against the release named in the text. Raw Markdown: /ontology/documentation/development-operations.md. This page is also served as raw Markdown at the same URL with a .md suffix, for agents and search tools that prefer plain text. Every capability statement cites the source file and line it was measured from.

You've seen the proof

Ready for a number? Scope your deployment and we'll price it against your own economics.

Get your quote →