Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Signals

Signals is the hub of a federated compute and data plane. Independent engines — Gaius, Ægir, Atelier, Hermes Agent — keep their own products, native gRPC, and models. They attach here: one protocol, one scheduler, one warehouse, one governance record.

If you arrived from weathership.org or from the GitHub README, this book is the operator and architecture reference for that hub. Clone this repository, bring the stack up, and every peer you start is already on the same wire.

What you get

PlaneWhat it is
Wiresignals-protocol — zndx.engine.v1 and zndx.scheduler.v1. Each engine registers the shared Engine service beside its native service, so one stub reaches any peer.
ScheduleSignals engine on :50551 (capability=scheduler). YuniKorn admits Applications; the queue path is the resource class.
WorkflowsAirflow holds coordination Activities (including agent-rtc). Metaflow is the production flow path: one metadata service, artifacts on RustFS, events through Knative Eventing.
StorageTransparent hierarchical storage: Apache Kudu (hot), Apache Iceberg on RustFS (warm), Impala SQL across both, impala_fdw from PostgreSQL. Cluster DCGM through that warehouse, with agent exchange, is one diagram.
GovernanceApache Atlas (AGE on PostgreSQL 16) with an OpenLineage REST API. Marquez-web is the lineage UI. Ranger consumes Atlas tags.
Controlsignals-ui on :9889.

Nautilus sits beside each engine as a deterministic supervisor (zndx.supervision.v1). Its phase gates and Brier ledger are drawn in one diagram. Classification (sigint) still lives in this tree: it writes SIGDG tags into the same Atlas record the rest of the federation already uses.

The lattice

  Gaius :50051          Ægir :50151         Atelier :50251
  cognition             instruct            referee
         \                   |                   /
          \                  |                  /
           ┌─────────────────┴─────────────────┐
           │   signals-protocol  (gRPC)        │
           │   zndx.engine.v1 · scheduler.v1   │
           └─────────────────┬─────────────────┘
                             │
                    Signals engine :50551
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
   YuniKorn :30080     Airflow :30800      Metaflow :30180
        │                    │                    │
        └────────────────────┼────────────────────┘
                             │
              Kudu + Iceberg + Impala + PostgreSQL
              Atlas / Ranger / OpenLineage
              RustFS · Polaris · signals-ui :9889

  Hermes :50651 (agent)     Metabase :50451 (dashboard)
ProjectCapabilityPortPrimary UI
Signalsscheduler:50551signals-ui :9889
Gaiuscognition:50051board :9890
Ægirinstruct:50151gateway / Vite
Atelierreferee:50251workbench :3000
Hermes Agentagent:50651dashboard :9119
Metabasedashboard:50451Metabase :3200

A federated engine is independently operable and a full participant. Capabilities travel on the wire (cognition, instruct, referee, agent, scheduler, dashboard); the serving engine chooses the model and reports what it ran. Launchers seed the hub, read Engine/Status.surfaces, and walk ServerQuery PEERS. A peer Announces itself; the Federated menu fills in.

Hermes interactive sessions keep WebRTC media on the engine and declare a coordination Activity that claims root.internal.inference.agent-rtc — one GPU, only while the session runs. Airflow observes that claim; YuniKorn admits it.

Bring it up

Linux workstation, devenv + Nix, recurse submodules. Impala, Kudu, and the RKE2 critical plane are Linux.

git clone --recurse-submodules git@github.com:weathership/signals.git
cd signals
devenv shell

# One-time native builds on a fresh machine
devenv tasks run kudu:build-cpp
devenv tasks run impala:build

cp .env.example .env
just up                 # PostgreSQL 16+AGE, Kerberos, RustFS, Polaris,
                        # Atlas, Ranger, Kudu, Impala, signals-ui, …
just signals-ready      # PASS / WARN / FAIL

Open http://127.0.0.1:9889. Then:

just kinit
just lattice-ci         # Engine/Status + reflection
just test               # hermetic pytest
just docs-serve         # this book, live

Group lifecycle on a lab host:

just install-systemd --peers gaius,aegir,atelier --enable
sudo systemctl start signals.target

Details: Quick Start, devenv, Peer integration.

How this book is organized

  1. Architecture — protocol, queues, Activities, Metaflow, the warehouse, Atlas OpenLineage, the control UI.
  2. Components — the Apache and sibling trees this repo pins.
  3. Operations — devenv, Kerberos, secrets, backup, joining as a peer.
  4. Classification — sigint, SIGDG, Dempster–Shafer fusion, SAGE. Useful if you tag columns; optional if you are attaching an engine.
  5. Reference — configuration, ontology, roadmaps.

Start with System Overview if you want the diagram, or signals-protocol if you are implementing a peer.

Quick Start

Prerequisites

  • Linux workstation (Impala, Kudu, and the RKE2 critical plane)
  • devenv + Nix; direnv recommended
  • just, kubectl, grpcurl on PATH
  • Recurse submodules on clone

Bring-up

git clone --recurse-submodules git@github.com:weathership/signals.git
cd signals
devenv shell

# One-time native builds if this machine has never built them
devenv tasks run kudu:build-cpp
devenv tasks run impala:build

cp .env.example .env
just up
just signals-ready

just up starts PostgreSQL 16 + AGE (:5455), Kerberos, RustFS, Polaris, Atlas, Ranger, Kudu, Impala, signals-ui, and preflights YuniKorn, Knative, Metaflow, and Airflow on RKE2.

Open http://127.0.0.1:9889 — that is the control plane.

just kinit
just kerberos-status    # Impala HS2 GSSAPI OK
just lattice-ci
just test               # pytest tests/
just behave             # tier-0 BDD; SIGNALS_BDD_TIER1=1 for the live stack
just docs-serve

Attach a peer

just install-systemd --peers gaius,aegir,atelier --enable
sudo systemctl start signals.target
just lattice-ci --require gaius,aegir,atelier

A new engine implements zndx.engine.v1 on its lattice port, waits on signals-ready.service, Announces, and consumes Metaflow, Atlas, and the warehouse. Contract: config/platform/peer-contract.json. Walkthrough: Peer integration.

Classify columns (optional)

The in-tree sigint pipeline samples Impala, fuses evidence, and writes Atlas tags:

just tag-dry-run default.my_table
just tag default.my_table

See Metadata Tagging.

Next

System Overview

Signals is the federation hub: protocol pin, scheduler, warehouse, and governance record. Sibling engines attach over signals-protocol and consume this plane.

Layers

Planes

PlaneRole
ProtocolShared Engine and Scheduler RPCs. Peers keep native gRPC.
ScheduleYuniKorn admits Applications. Resource class is the queue leaf. Activities are Airflow DAG runs owned by Signals.
FlowsOne Metaflow metadata service; tasks as Kubernetes pods; events via Knative Eventing.
StorageKudu hot, Iceberg on RustFS, Impala views, PostgreSQL via impala_fdw.
GovernanceAtlas is lineage and classification. OpenLineage REST on the same process. Marquez-web on :21011 is the UI.
Controlsignals-ui :9889. Federated surfaces come from Engine/Status.

Critical-plane services (Postgres, RustFS, Atlas, Ranger, Kudu, Impala, YuniKorn, Knative, Metaflow, Airflow) come up together. just up is the entry point; just signals-ready is the check. See Critical plane.

Workloads

YuniKorn Applications use leaves such as root.internal.inference.{reasoning,coding,instruct,heavy,extract,agent-rtc}. Project is identity (federation.project). Interactive Hermes sessions claim agent-rtc for one GPU while they run.

Coordination Activities are two hops: local process → peer engine → Signals :50551 → Airflow. Only Signals talks to the DAG.

Classification

sigint classifies columns into the SIGDG taxonomy (Dempster–Shafer fusion, SAGE-measured features) and writes Atlas tags. It is one consumer of the governance record. See Metadata Tagging and Evidence Fusion.

Signals protocol core

Signals holds lineage, governance metadata, and scheduler state for the federation. Peer engines — Gaius, Ægir, Atelier, Hermes, Metabase — discover and call those services over a shared gRPC contract.

Hermes Agent (Nous Research) joins as capability agent. Weathership plugins attach memory and context to that runtime; the official product remains Hermes. Memory providers are the documented extension point.

Wire contracts live in components/signals-protocol (trunk). Changes land on that trunk first (additive within a version). Every peer that pins the submodule then bumps to that same SHA, regenerates its bindings, and pushes in the same slice. A side branch is not a pin.

How the federation contract evolves

signals-protocol is the shared federation contract: engine-to-engine wire, capability semantics, co-tenancy, OIP mapping. It grows as peers need new shapes. The first cross-engine call showed why zndx.engine.v1.Engine sits beside each project’s native service. Complete, Remediate, GPU leases, and OIP tensors followed the same path: ship in a peer, then promote a stable shape into the protocol.

In-family engines (Gaius, Ægir, Atelier, planned siblings) share multi-service gRPC, capability-on-the-wire, private serving, lattice ports, and co-tenancy leases.

AGPL peers (Metabase) live in an isolated tree and speak the same wire. mbengine is a lattice participant (Status, unit under signals.target). See Core vs license-external engines.

Signals hosts the platform (governance SoR, critical plane, process group control) and pins the protocol submodule. When attachment work (peer units, lattice-ci, Metaflow/CE) meets a gap, prefer extending signals-protocol (or documenting an explicit interim) over forking per-project wire dialects.

LayerMaturity (honest)Where it lives
Native product gRPCMature in each core peerGaius / Ægir / Atelier / … trees
Multi-service + capability enginesIn production use; still convergingCore peer engine/ packages
zndx.engine.v1 shared servicev1 landed; incomplete relative to real peer needssignals-protocol + per-peer bindings
OIP dual-registration / mappingPartial (stronger in some engines than others)Protocol spec + peer OIP servicers
License-external enginesIsolated adapters (e.g. mbengine)Separate trees (e.g. AGPL Metabase)
Platform process attachmentLab-usable (ready gate, units, lattice-ci)This repo (peer-contract, systemd)
Cross-engine product flowsEarly (CE map, Remediate, federated Complete)Protocol + peers together

Working stance: treat protocol growth as co-evolution with peer engines — especially core peers, where shared engine practice is densest — requirements materialize in real projects; the contract absorbs what should be shared. Expect substantial further foundation work on signals-protocol itself before “federation complete” is a fair claim.

Signals as hub

Gaius, Ægir, and Atelier carry product engines (multi-service gRPC, serving, co-tenancy, Remediate). Signals holds the scheduler engine on :50551 and the control plane those peers attach to:

What the hub centralizes:

Hub control (today)Role
Critical plane + signals-readyOne foundation readiness gate
signals.target + peer unitsOne group lifecycle for co-tenant engines
peer-contract + lattice-ciOne accept surface (codegen Status + reflection)
Platform Metaflow / Airflow / Eventing / YK / AtlasShared production path without re-hosting
signals-protocol pinOne wire contract to promote peer-proven shapes into

Working mode while early: drive from hub control and protocol gaps; land proven depth in peers that already have engines (and promote stable shapes into signals-protocol). Peer sessions stay product-local; convergence is measured here (lattice-ci, unit graph, protocol bumps). Engine functionality in Signals itself can grow when the federated architecture is solid enough to host it honestly — maturity, not a permanent “thin product” identity.

Doctrine: total commitment as a federation peer

Adopting signals-protocol as a federation peer is total commitment, not a decorative Status endpoint on a half-running stack. That applies whether the peer is a mature product engine (Gaius today) or a younger tree: peerhood means full capacity as that project defines it, honest ops, and remediation — not a lattice-only shell built only to pass accept.

ObligationMeaning
Full project stackUnder signals.target, unit start brings the peer’s full devenv stack as that project defines production-local operation (just up / devenv up): capability engine and product UI/gateway/DB/supporting processes — not an engine-only or Status-only subset.
Full engine capacityThe peer runs its real capability engine (product multi-service gRPC, models, co-tenancy, native surfaces as designed for that project) — not a quasi-engine or Status-only process.
Honest operational statestart / stop / restart (unit and group) must take the full stack through real transitions: processes up with intended capacity, ports freed and workers down on stop, restart = full stop then full start. Partial “already READY — skip” or orphan listeners that survive stop are error conditions, not success modes.
Remediation on failureWhen capacity or a transition fails, that is a fault requiring remediation (peer-local health/FMEA/fix paths — e.g. Gaius /health fix engine and related recovery). Optimistic Status that advertises healthy capabilities while backends are down is not accept.
Hub vs peer (while architecture is early)Peers own full-stack depth and lifecycle honesty for their lattice + product ports. Signals currently owns group control, contract, and lattice-ci; it does not yet re-home peer engines into this tree. That is sequencing, not a ban on future Signals engine work.

Anti-patterns (not federation):

  • Engine-only unit start that leaves product UI/gateway dark while claiming peer ready
  • Synthetic always-healthy Status rows so lattice-ci passes without resident capacity
  • Unit start that no-ops because an orphan still answers Status
  • Unit stop that leaves lattice ports, UIs, or engine process groups alive
  • Interactive devenv up stacks fighting the systemd unit for the same lattice port
  • Treating “Signals is thin/early” as doctrine that excuses half-committed peers or freezes engine growth here forever

Accept implication: lattice-ci / Engine/Status are necessary gates, not a substitute for full capacity. Peer-unit accept should include clean start/stop/restart of this peer’s engine under signals.target (single listener ownership, honest health). Detail: Peer unit acceptance spec and Peer integration.

Platform capability: Scheduler (zndx.scheduler.v1)

Signals’ first thick engine capability is federated scheduling (capability=scheduler; lab model=yunikorn, later PBS/SLURM). Thin clients call zndx.scheduler.v1.Scheduler on the Signals lattice (:50551); the engine privately talks to the backend and owns archive/current/scratch projection. See YuniKorn queue management (lab adapter).

Spec vs tooling surfaces

LayerRole
.proto + gRPC servicesThe specification (OIP-aligned). Codegen is how we implement clients and servers.
Generated stubsFirst-party path for lattice-ci, unit probes, peer libraries (scripts/zndx_engine_status.py, peer engine/generated/, …).
gRPC server reflectionInstall requirement so external tools can use bare grpcurl host:port Method without cloning our tree.

Reflection is not a substitute for the proto. First-party scripts should not prefer “raw grpcurl -proto” as a substitute for codegen — they should call generated clients from signals-protocol.

RequirementWhy
Codegen from proto/Proper protocol implementation for engines and our automation
Server reflection on lattice portsExternal/operator grpcurl without extra steps

Python engines: grpcio-reflection + enable at server start. Lattice-ci runs both a generated Status client and a reflection check.

Language: brass-tacks naming

Name platform, wire, and foundational theory by the terms a practitioner (or inventor) already uses — not by product metaphor, softened ops slang, or in-house glosses that drift from the definition.

Two tests (fail either → rename until both pass, then document once):

  1. Systems: Can someone who only knows the underlying tech (gRPC, systemd, CI) map the phrase to a concrete artifact — port, unit, service registration, gate — without a project glossary?
  2. Theory: Would the people who invented (or standardly formalized) the concept recognize our names, types, and invariants as their object — not a vague cousin? Example: persistent homology should read cleanly to Gunnar Carlsson (or any TDA practitioner): Vietoris–Rips filtration, homology dimension, birth/death pairs, essential classes (death = +∞), not rebranded “shape scores.” Same bar for Dempster–Shafer (Bel/Pl/K), SAGE, TreeSHAP, Ollivier–Ricci, and future topology / belief / geometry work.
PreferAvoidWhy
multi-service gRPCmulti-face gRPCOne listen port, several services on one server — gRPC’s own model
lattice-ci / *-cismoke (for elevated gates)Repeatable accept gates are CI; smoke is one-off first-run feedback
protocol / codegen pathproto fallback.proto is the specification, not a backup
peer-scoped stopsoft stopFull unit stop, scoped to one peer — not a partial or vague halt
Domain terms + faithful APIsMetaphor wrappers for mathImplementations stay inspectable against papers and reference libraries (e.g. Ripser dgms)

Product metaphors are fine in UI copy when they map 1:1 to a brass-tacks term documented here. Do not invent a second vocabulary for the same process shape or the same mathematical object.

Layers

  UIs / external engines          Federated engines (OIP + zndx.engine.v1)
  ┌──────────────┐                ┌─────────┬──────────┬─────────┐
  │ Marquez-web  │                │  Aegir  │ Atelier  │  Gaius  │
  │ Metabase*    │                │ :50151  │ :50251   │ :50051  │
  │ Atlas UI     │                └────┬────┴────┬─────┴────┬────┘
  └──────┬───────┘                     │         │          │
         │  HTTP                       │  multi-service gRPC
         │  /api/v1  /api/atlas        │  + OIP ModelInfer (horizon → now)
         ▼                             ▼
  ┌────────────────────────────────────────────────────────────┐
  │  SIGNALS (this product)                                     │
  │  · Atlas :21010  governance SoR + OpenLineage /api/v1       │
  │  · Ranger        tag/resource authz from Atlas tags         │
  │  · Marquez-web   OL UI (proxy only; no Marquez DB)          │
  │  · components/signals-protocol  shared protos + specs       │
  │  · components/hermes-agent      agent runtime (plugins)     │
  │  · PG/AGE + Kudu/FDW scale path                             │
  └────────────────────────────────────────────────────────────┘
         ▲
         │  MemoryProvider + context engine plugins
         │  (Weathership → official Hermes memory service)
  ┌──────┴───────┐
  │ Hermes Agent │  tools / hooks / multi-agent ops
  └──────────────┘

  * AGPL Metabase is a *license-external* peer (isolated engine, not core family):
    process + wire only; consumes catalog/lineage/authz; never SoR or vendored.

Core vs license-external engines

Two kinds of “peer” attach to Signals. Conflating them muddies protocol work and peer-unit sessions.

Core federated enginesLicense-external peers
ExamplesGaius, Ægir, Atelier; future synth, vigil, …Metabase (AGPL) today
Why separate treesProduct modularity; shared ASL2 (or compatible) lineLicense incompatibility (AGPL ↛ ASL2 combine)
Engine architectureShared family (capability engines, multi-service gRPC, co-tenancy)Isolated product engine (e.g. mbengine); not Gaius-lineage
Protocol rolePrimary source of organic signals-protocol requirementsSpeaks enough wire to join; not the design center for core engine law
Platform attachsignals.target, lattice ports, Metaflow/CE/YK as applicableSame process pattern optional; no source/jar into Signals or core peers
SoRNever — Atlas/lineage/authz stay on SignalsNever — same

Metabase is external by requirement: introducing an engine there is inherently isolated so the product can federate without licensing contamination. Architecturally it is distinct from Gaius / Ægir / Atelier and from any future core Signals projects. Operator install and dual-map still matter; they do not make Metabase “another Atelier.”

ConcernAuthorityClient surface
Governance types & classificationsAtlas on signals PG + AGE/api/atlas/*
Runtime lineage (Job/Run/Dataset)Same Atlas process + signals_ol/api/v1/* (OpenLineage + Marquez-compat)
Authorization decisionsRanger (Atlas tags as input)Ranger REST / plugins; policy evaluate via discovery
Human OL UIMarquez-web:21011 (Atlas HTTP + 1) → proxies /api/v1 only
Agent memory + compactionWeathership plugins on HermesMemoryProvider + context engine (plugins)
Engine-to-engine inferencezndx.engine.v1Complete / Status / Remediate / Yield / ServerQuery
Heterogeneous model servingKServe OIP + signals-protocol mappingModelInfer / readiness; authz + provenance on the wire
GPU co-tenancyYK Application claim (federation.zndx.org/gpu) + Status.gpu_idsQueue occupancy SoR; /tmp/zndx-gpu-leases is intra-node refuse only

Hard rules (unchanged):

  • No Marquez database and no stock Marquez API as a second catalog.
  • No Python OL facade beside Atlas.
  • DST method stays in Atelier (and related labs); Signals stores outcomes (classifications, optional belief facets on runs) for governance and audit.
  • Protocol evolution is additive within a version; breaking changes → v2.

Submodule

# already pinned
git submodule update --init components/signals-protocol

# bump when protocol trunk advances
git -C components/signals-protocol fetch origin
git -C components/signals-protocol checkout origin/trunk
# commit pin in signals
PathRole
components/signals-protocol/proto/Source of truth for protos
components/signals-protocol/specification/Human specs per package
Generated stubsVendored per adopter (engines); Signals may generate for discovery services

Discovery (Atlas, Ranger, lineage)

Federated and external engines must find centralized services without hardcoding lab topology. Today Status describes engine endpoints only. The protocol core extends discovery so a peer learns:

Service kindWhat is advertisedTypical base
LINEAGE_OLOpenLineage ingest + Marquez-compat readhttp(s)://…:21010/api/v1
ATLAS_GOVERNANCEAtlas REST v2 (entities, types, classifications)http(s)://…:21010/api/atlas
RANGER_AUTHZRanger admin / policy evaluatehttp(s)://…:6080 (lab)
MARQUEZ_UIOptional human UI (not SoR)http(s)://…:21011 (Atlas HTTP + 1)
ENGINEPeer zndx.engine.v1 gRPChost:port
OIPOpen Inference Protocol HTTP/gRPCKServe-compatible base

Discovery is informative + capability-scoped: advertising a URL does not grant access. Edge (CF Zero Trust / IdP), Kerberos (data plane), and Ranger policies still authorize. See Identity and access.

StatusResponse.surfaces[] (kind, url, healthy) is the v1 step. kind=primary is the product UI the waffle lists. Signals advertises http://$SIGNALS_ADVERTISE_HOST:9889 (host from SIGNALS_KRB_HOST / LAN hostname — never loopback). Do not invent peer UI URLs — list only what that engine’s Status returns.

Engine/ServerQuery is pairwise S2S (not gossip): REMOTES (named git remotes + head), PEERS (lattice host:port from peer-contract), SURFACES (same list as Status), QUEUES (occupancy leaf shape), WORKLOADS (models, capabilities, TP/PP requirements — not the queue). Empty is honest.

A later zndx.discovery.v1 package can grow auth hints if the surface expands.

Engines converge by:

  1. Calling discovery (or reading a Signals-published service map).
  2. Emitting OpenLineage to LINEAGE_OL (not a local Marquez).
  3. Registering / classifying assets via ATLAS_GOVERNANCE (or Signals hooks).
  4. Evaluating access via RANGER_AUTHZ (or plugins that pull Atlas tags).

External engines (Metabase and peers)

Metabase (AGPL) and similar BI/agent tools are license-external participants — architecturally isolated from the core engine family (see above). They are first-class on the wire and process group, not second SoRs and not core engine projects:

They mayThey must not
Read catalog/lineage via discovered APIs / FDWOwn a parallel lineage DB as product truth
Honor Ranger / edge authn for queriesBypass ZT + Kerberos/data-plane identity
Appear as OL Jobs/Runs when they materialize datasetsReplace Atlas entity identity

Signals may ship thin adapters (e.g. Metabase → dataset namespace mapping) under ops docs; the contract remains discovery + OL + Atlas/Ranger.

Optional install (separate AGPL checkout + metabase.service under signals.target, never vendored into this tree): Peer integration — Metabase.

KServe Open Inference Protocol (OIP)

signals-protocol already patterns after OIP and maps Complete/Status as a horizon peer. Multi-agent model ops make that horizon active:

NeedWhy OIP + signals-protocol together
Heterogeneous servingTriton/KServe/external clusters speak OIP; zndx engines speak Complete
AuthorizationModel invoke is a privileged action — identity + Ranger/tag context on the request path
ProvenanceEvery multi-agent inference should leave an OpenLineage Run (and optional reasoning retention)

Direction:

  1. Map zndx.engine.v1.Complete ↔ OIP ModelInfer (text/tensor payloads) in the protocol spec (expand existing OIP mapping table).
  2. Carry authz context on federation/OIP calls (caller principal, optional Atlas entity / classification scope) — additive proto fields; enforcement at the serving edge and Ranger.
  3. Emit provenance — engines (or a Signals sidecar) POST RunEvents to /api/v1/lineage for model ops: inputs (prompt hashes / dataset refs), outputs, model id from CompleteResponse.model, capability name.
  4. Optional OIP gateway in Signals or Gaius that dual-registers zndx.engine.v1 and OIP for the same capability set.

OIP is not a second governance plane. Atlas/Ranger remain law; OIP is the model invoke lingua franca; OL is the audit thread.

Relationship to Marquez

PieceRole
Marquez-webDefault-stack UI for OL; proxy to Atlas /api/v1
Marquez API contract testsFreeze the subset Signals implements
Stock Marquez server + FlywayRejected

See OpenLineage + Atlas.

Phased delivery (protocol core)

PhaseDeliverable
P0components/signals-protocol submodule + this doctrine
P0bGovernance scale plane: Atlas/Ranger Kudu projections + RustFS on RAID — required so multi-engine devenv does not crush Postgres/AGE (governance-scale-plane)
P1Discovery: advertise Atlas + OL + Ranger + OBJECT_STORE on federation Status (or discovery.v1)
P2Identity map: OL dataset ↔ rdbms_* qualifiedName; dual-path smoke
P3Producer contract suite (polyglot, Gaius, Flink) → single /api/v1
P4OIP mapping + authz/provenance fields in protocol; model-op RunEvents
P5External peer pack: Metabase (and similar) discovery + lineage participation
P6Kudu/FDW scale for OL/governance projections; Gaius/Aegir drop private facades
P7Hermes: Weathership memory provider + context engine plugins; path to official listing

Hermes / Weathership memory

Hermes discovers memory providers under plugins/memory/<name>/ and context engines under plugins/context_engine/<name>/ (plugin system). Only one external memory provider is active at a time (memory.provider in config), additive to built-in MEMORY.md / USER.md (memory providers).

Weathership (Signals-backed) should:

  1. Implement MemoryProvider with prefetch, turn sync, session extract, and agent tools (search/store) — backed by Signals AGE/OL (and optional belief facets), not a greenfield DB.
  2. Implement a context engine that compacts with lineage-aware retention (pre-compression extract into Weathership memory, similar spirit to ByteRover’s pre-compression extraction).
  3. Use federation discovery for Atlas/Ranger/OL bases; emit OpenLineage for memory writes and model ops (authz + provenance).
  4. Ship as installable Hermes plugins; eventually qualify as an official reasoning-enabled memory service provider in the Hermes ecosystem.
  5. Keep superset capabilities fleet-side (and exposable as tools/skills): mechanistic interpretability (SAE/CLT), topological structure (PH, Ollivier–Ricci), multi-engine Complete/Remediate—without waiting for Hermes core to absorb them. Deeper core integration remains open later; plugins-first is the current strategy.

Submodule: components/hermes-agent.

  • Submodule: components/signals-protocol
  • Submodule: components/hermes-agent
  • OpenLineage + Atlas
  • Identity and access
  • Atlas → Kudu outbox
  • Protocol README (federation face, OIP horizon, co-tenancy)
  • Gaius: CO-TENANCY draft; hx.lineage; FEDERATION.md
  • Aegir: polyglot OpenLineage; former gateway/marquez.py (shape reference only)
  • Atelier: DST / classification method lab

Signals engine

The Signals engine is the scheduler-capability process on :50551. It implements zndx.engine.v1.Engine and zndx.scheduler.v1.Scheduler from signals-protocol. Product UIs and peer engines call this process; they do not speak YuniKorn REST or Airflow HTTP as a product path.

Responsibilities

  • Engine/Status, ServerQuery, Announce, WatchWorkload, RecordLineage, Yield
  • Queue policy, projection, and RequestQueueShare
  • Declare / Renew / Release / List / WatchActivity — leases that Airflow sensors hold
  • Discovery: Status.surfaces and pairwise PEERS

Where it runs

ModeLocation
devenvLocal process, :50551
Lab hostsignals.service under signals.target
ClusterDeployment in the Signals namespace; ClusterIP on 50551

Health is gRPC Engine/Status plus server reflection. just lattice-ci is the accept probe.

grpcurl -plaintext 127.0.0.1:50551 zndx.engine.v1.Engine/Status
just lattice-ci

Primary UI: signals-ui :9889. Peer engines (Gaius :50051, Ægir :50151, Atelier :50251, Hermes :50651, Metabase :50451) register the same Engine face beside their native service.

YuniKorn queue management (engine-first)

Signals owns queue configuration and operational control as a platform capability. Thin clients never call YuniKorn REST as the product path.

Architecture

signals-ui · MCP · CLI
        │  gRPC only
        ▼
Signals engine :50551
  zndx.engine.v1.Engine          (Status — capability=scheduler, model=yunikorn)
  zndx.scheduler.v1.Scheduler    (ops + projection; vendor-agnostic)
        │ private HTTP / kube
        ▼
YK REST :30080  ·  ConfigMap apply (promote)   ← lab backend

Protocol: components/signals-protocol → zndx.scheduler.v1. YuniKorn is the lab backend, not the service name.

Official YK references:

Projection roots (Aegir-shaped)

Under build/dev/ (or SIGNALS_YK_PROJECTION_ROOT):

RootRole
currentActive policy view + live overlay; regenerable via SyncProjection
scratchEdit/review pending promote
archiveFreezes of past applied configs

API-symmetric clients

ClientEntry
CLIsignals-yk / uv run python -m signals.cli.yk
MCPsignals-yk-mcp / uv run python -m signals.mcp.yk (stdio JSON-RPC)
Web/ Scheduler band + /queues lineup + /applications → engine gRPC (SIGNALS_ENGINE_TARGET) via /api/engine/v1/*

Queues lineup (Aegir geometry)

Full-height shell under chrome:

PieceBehavior
Side-nav SECTIONarchive | current | scratch (same roots as projection)
QUEUES / POLICY / OPSSeeds: queue notes, config/queues, ops/diff, ops/nodes, ops/health, Sync
CanvasHorizontal panels (~33rem), openFrom(i) branch, × closes to parent
TrailsessionStorage per layer; URL holds ?root=&open=&partition= focus only
DataEngine projection notes + virtual notes (declared YAML, nodes, health, diff)

Legacy /nodes and ?panel=nodes redirect to /queues?open=ops/nodes.

export SIGNALS_ENGINE_TARGET=127.0.0.1:50551
export KUBECONFIG=~/.kube/config   # for promote ConfigMap apply

uv run python -m signals.engine          # server
uv run python -m signals.cli.yk health
uv run python -m signals.cli.yk queues
uv run python -m signals.cli.yk config
uv run python -m signals.cli.yk sync

# policy lifecycle
uv run python -m signals.cli.yk write-scratch path/to/queues.yaml
uv run python -m signals.cli.yk diff --live
uv run python -m signals.cli.yk promote --dry-run
uv run python -m signals.cli.yk promote
uv run python -m signals.cli.yk archives
uv run python -m signals.cli.yk index --root current

MCP (engine gRPC only; tools mirror CLI):

export SIGNALS_ENGINE_TARGET=127.0.0.1:50551
uv run python -m signals.mcp.yk
# tools: yk_health, yk_queues, yk_config, yk_validate, yk_sync,
#        yk_write_scratch, yk_diff, yk_promote, yk_archives, yk_restore, yk_index

Promote

PromoteScratch order:

  1. Read scratch queues.yaml
  2. Validate via YK POST /ws/v1/validate-conf (engine-private)
  3. dry_run: optional kubectl apply --dry-run=server of ConfigMap patch; no projection write
  4. live: apply ConfigMap → archive current → write scratch → current → best-effort SyncProjection

ConfigMap adapter (engine-private)

EnvDefaultMeaning
SIGNALS_YK_APPLY_ENABLED1Set 0 for local-only promote (no kubectl)
SIGNALS_YK_CM_NAMESPACEyunikornNamespace
SIGNALS_YK_CM_NAMEyunikorn-configsPrimary CM (runtime override)
SIGNALS_YK_CM_FALLBACKyunikorn-defaultsIf primary missing
SIGNALS_YK_CM_KEYqueues.yamlData key
KUBECONFIG / SIGNALS_YK_KUBECONFIG—kubeconfig path
SIGNALS_YK_KUBE_CONTEXT—optional context

Lab/RKE2: yunikorn-configs overrides yunikorn-defaults at runtime (zarf values). Apply preserves other CM keys (e.g. admission controller flags).

On apply failure, projection is not mutated (applied=false).

Resource-class tree

Queue path is scarcity, not project. Project stays identity (federation.project, C2, Engine/Yield). Aegir / Atelier / Gaius / Signals share the same internal.inference.* leaves.

Canonical policy: config/scheduler/federation-queues.yaml. Catalog prose: config/scheduler/resource-classes.md.

root.default                         leftover / unannotated (no GPU)
root.platform                        Metaflow, Airflow, Eventing
root.internal.compute                CPU burst / idle sentinels
root.internal.inference.{reasoning,coding,orchestration,instruct,embedding,extract,agent-rtc}
root.external.token-metered          pay-per-token (Cerebras thinking, OpenAI-class)
root.external.rate-metered           peer meters RPM
root.external.subscription.rate-limited   Grok ACP / Bytez / xAI subscription only

Placement (without provided, annotations are ignored and everything lands on root.default):

placementrules:
  - name: provided          # yunikorn.apache.org/queue
    create: false
  - name: fixed
    value: root.default
    create: false

Queue share (WRK occupancy). Peers send Scheduler/RequestQueueShare (uuidv7 request_id, workloads[], shares[] guaranteed/max, validity window). Signals persists the time series, merges overlapping leftover floors against parent GPU max 6 (extract=1 vs light/CLT or medium/SAE), and PromoteScratch applies yunikorn-configs. Peers never write queues.yaml. Persist failure is #YK.00000007.SHAREFAIL.

Idle occupancy. Extended inactivity on a model (e.g. Qwen3.8) is Knative KPA scale-to-zero of the proxy sentinel Application, then MiNiFi C2 last-gasp → Engine/Yield on the federated engine that owns that vLLM. Tokens (federation.zndx.org/gpu) free only when the Application completes. Knative does not stop host CUDA. Serving / loading keeps min-scale: 1. See MiNiFi sentinels.

GPU occupancy is the Application claim key federation.zndx.org/gpu. Advertise node capacity with scripts/advertise_federation_gpu.sh. Host-engine sentinels request that token only — never nvidia.com/gpu on a CPU-only pod (that binds the card into the empty container). In-cluster CUDA pods request both.

Each leaf carries properties.federation.* (class, yk_enforces, peer_meters, examples) so GetQueueTree is the discovery SoR. MCP catalog tools come after this tree is live; protocol SuggestQueue later still.

An Application is placed once, when its app-id is first submitted. Re-annotating a later pod with the same id does not move it; use a new id or let the old Application complete. root.default holds unannotated leftovers (including earlier autogen).

Turnkey refresh: just redeploy walks the k8s.product-redeploy FSM (IT-ops FSM). Probes are Brier-scored forecasts. Completing still owns the app-id; timeout fails the procedure. YK and Knative substrate apply only when missing or those manifests changed. Not signals.target (host engines). Not Metabase (host peer). Not a scorched-earth rebuild.

uv run python -m signals.cli.yk write-scratch config/scheduler/federation-queues.yaml
uv run python -m signals.cli.yk promote --dry-run
uv run python -m signals.cli.yk promote

Sentinel Applications and Yield

A MiNiFi C++ sentinel is the YuniKorn Application: 1:1 with a schedulable process. signals-ui Applications lists only what YK admitted.

Closed loop

YK ends Application  →  last-gasp / heartbeat (C2 HTTP)
                     →  Signals C2
                     →  gRPC zndx.engine.v1.Engine/Yield
                     →  owning engine ends the local process

Two wires on the C2 process: HTTP toward sentinels; gRPC Yield toward the lattice engine from peer-contract.json. Sentinels never dial gRPC.

ReasonWhen
PREEMPTEDYK / kubelet ended the sentinel
COMPLETEDwork finished
ORPHANmissed heartbeat (SIGKILL / node loss fallback)
UNIT_STOPpeer-scoped unit stop

Unknown workload_id is idempotent (ok=true, process_ended=false).

Resource claim

The Application’s resource vector is the claim. GPU work asks for federation.zndx.org/gpu on the sentinel (or on an in-cluster pod). That is how YK sees occupancy and how preemption frees a card. CUDA still runs on the host engine unless the work is an in-cluster GPU pod (nvidia.com/gpu and the federation token). Do not request nvidia.com/gpu on a CPU-only MiNiFi container.

Queue path is the resource class (root.internal.inference.extract, root.internal.compute, …), not root.{project}. Project stays on federation.project for C2 and Yield.

Platform warehouse upkeep (data-product.tier-upkeep) uses a flow-level proxy sentinel on root.platform (signals-dataproduct-tier-upkeep) when the Metaflow run is not itself a YK Application. A @kubernetes step is an Application — same app-id / workload_id.

What this cut is not

  • NiFi proper / canvas (optional later K8s fielding for OTel wiring; not signals-ui)
  • Per-engine C2 servers
  • Gaius/Aegir/Atelier Yield bodies (they implement the same RPC; Gaius maps onto CompleteWorkload / restore)
  • Moving host vLLM into the sentinel container

Unit

signals-c2.service is foundation (with the engine): signals.target Wants it; it After= signals-engine.service. Start/stop wrappers: scripts/systemd_c2_{start,stop}.sh.

sudo systemctl start signals-c2.service   # or: start signals.target
curl -sf http://127.0.0.1:50561/healthz

just signals-ready records C2 as WARN until the unit is up (C2 starts after ready).

Lab

# engine on :50551, control :50552, C2 :50561
just sentinel-yield-lab

Protocol: components/signals-protocol Engine/Yield. Ops design: specification/operations/minifi_sentinels.md.

IT-ops FSM and Brier ledger

IT operations are methods: a named FSM, not a pile of kubectl lines. Each probe is a forecast (proposition + implied p) tagged with the FSM state it was made from. When the oracle (kubectl / YuniKorn GET) resolves the claim, we score Brier per (observer, method fsm_state, implicit object FSM + state, procedure, epoch). The method cell is where we were; the implicit cell is the K8s/YK object machine (yk.application=running vs completing, k8s.deploy=desired_positive, k8s.job=active). Mixing those hid the Helm-Job miss (method said apps_completing, object was still Running).

This is the same discipline as synth’s Deluge calibration ledger and cyberphy converge (fail loud, do not treat a holding state as done). Signals holds the K8s-first catalog so ACP agents can run a method with a risk number, not a vibe.

Law

  1. Holding is not terminal. YK Completing still owns the app-id. Deployment spec.replicas still 1 still owns the ReplicaSet.
  2. Ill-posed claims resolve false. Asserting yk-apps-completed from start or scaling is a demerit even if a GET happens to look empty.
  3. Uncalibrated is 0.5 risk, not 0. No track record → honest ignorance.
  4. Timeout fails the procedure. It does not WARN-and-apply.

First method: k8s.product-redeploy

just redeploy / uv run python -m signals.ops redeploy

start → substrate_ready → scaling → desired_zero
      → pods_draining? → pods_quiet
      → apps_completing? → apps_completed
      → applying → verifying → placed
                 ↘ failed (from any non-terminal)

Holding: scaling, desired_zero, pods_draining, apps_completing.

data-product.tier-upkeep

Weekly ADD next 168-hour Kudu range, then settle weeks ≥ 4 weeks old (copy → verify → DROP RANGE PARTITION). Holding through dropping. Do not DROP from verifying.

Metaflow flow: signals.flows.tier_upkeep.DataProductTierUpkeep. Airflow is the clock. YK: flow-level proxy sentinel signals-dataproduct-tier-upkeep on root.platform (config/platform/tier-upkeep-sentinel.yaml). A @kubernetes step is an Application — reuse that app-id.

just tier-upkeep
uv run python -m signals.ops method data-product.tier-upkeep

Agent / ACP surface

No extra flags. The method is the command.

CommandRole
python -m signals.ops methodsList registered methods
python -m signals.ops method [name]JSON method (states, holding, well-posed probes)
python -m signals.ops risk --state apps_completingForecast risk for well-posed probes here
python -m signals.ops report [--axis method|implicit|both]Ledger Brier table
python -m signals.ops redeployWalk k8s.product-redeploy against the live cluster
python -m signals.ops review-product <id>Record History event + ACP brief (quality, lineage, delta)
python -m signals.ops record-snapshot --flow F --run-id RFirst product: map a Metaflow run + ACP upkeep assessment onto details

An ACP agent should: list methods, load a method, refuse transitions the graph does not allow, consult risk before acting, and never treat a holding state as placed.

Ledger file: build/state/ops-observations.jsonl (gitignored).

Nautilus is a different machine. It stamps positions and scores Brier per (observer, call site, momentum bucket) inside spec_version + engine_build. That diagram is Nautilus FSM and the Brier ledger.

Lineage

PieceWhere
Brier + α shrinksynth engine/calibration.py (Signals DST discount is the parent)
Converge / fail loudcyberphy zarf/converge CLOSURE
This walksrc/signals/ops/

Platform Metaflow (YK · Knative · Airflow)

Status: Architecture direction (platform-grade)
Date: 2026-08-11
Scope: Signals-owned ground-up Metaflow capability — not Gaius, not Marquez. Any federated engine that integrates via signals-protocol can consume it as a platform service.
Submodule: components/metaflow → weathership/oss-metaflow branch rch/devenv (custom line, same convention as other ASF/oss components).
References:

Thesis

Signals provides a platform Metaflow so current and future engines (Gaius, Aegir, Atelier, …) do not each stand up thrashing metadata services on shared RKE2. Engine repos stay free to keep isolated Metaflow for solo devenv; this tree does not modify Gaius or Marquez. Platform work lands here: submodule patches, federation packaging, Airflow + Knative Eventing, YuniKorn admission, signals-ui visibility.

With YuniKorn + Knative Serving already locked as the federation control plane, Metaflow is a first-class platform service:

LayerChoiceWhy
Metadata serviceOuterbounds metadata service + shared PG (platform DB)Central run tracking for all engines
Artifact storeRustFS (S3 API) under SIGNALS_DATA_ROOTAlready platform object plane
Task compute@kubernetes pods on RKE2Only compute layer Airflow integration supports
Pod schedulerYuniKorn queues root.{aegir,atelier,gaius,signals,hermes}Scarce GPU/CPU governance
Production DAG scheduleApache Airflow (airflow create)Already in Signals components; no Argo Workflows SoR
Event / reactive triggerKnative Eventing → Airflow DAG triggerPlatform event fabric; matches Gaius need for trigger without Argo
UIsignals-ui Applications (+ Sentinels panel)Workloads YK admits, including Metaflow task pods

Stock Outerbounds prefers Argo Workflows when in doubt (full event triggering). We deliberately take the eks_airflow shape and close the event gap with Knative Eventing, not Argo Events / Argo Workflows.

Submodule and customization line

FieldValue
Pathcomponents/metaflow
Remotegit@github.com:weathership/oss-metaflow.git
Branchrch/devenv (created from upstream master pin; platform patches land here)
Initgit submodule update --init components/metaflow

Platform-specific changes to Metaflow itself (Airflow event-bridge hooks, Knative-friendly defaults, YK labels, devenv packaging) go on rch/devenv in that repo — not as one-off patches only in Signals, and not by forking work into Gaius.

Isolation elsewhere (out of scope here)

Other products may still run engine-local Metaflow (Tilt metadata, private PG/S3) for isolated devenv. That pattern is fine for those repos; Signals does not implement or maintain it. Platform Metaflow is the shared SoR when an engine opts into federation via signals-protocol.

Why platform upgrade

Isolation painPlatform remedy
Competing Tilt/Helm Metaflow on shared nodeOne Zarf/federation-aligned package + YK queues
ImagePull / registry thrashPlatform images in Zarf registry
No fair-share with sentinels / other enginesYK multi-tenant queues
Cron-only if we only use Airflow stock Metaflow triggersKnative Eventing for external + inter-flow events
Opaque ops UIsignals-ui Applications · Queues · Status

Official stack vs Signals adaptation

Outerbounds eks_argo (reference default)
  Metaflow service + S3 + EKS
  Argo Workflows  ←── steps
  Argo Events     ←── event triggering

Outerbounds eks_airflow (our base)
  Metaflow service + S3 + EKS
  Airflow         ←── steps as KubernetesPodOperator DAGs
  @schedule only  ←── stock Metaflow→Airflow (no native event trigger)

Signals platform (target)
  Metaflow service + RustFS + RKE2
  Airflow         ←── production DAGs (schedule + API-triggered runs)
  Knative Eventing←── event fabric (replace Argo Events role)
  YuniKorn        ←── schedule all Metaflow + sentinel pods
  Knative Serving ←── sentinels / scale-to-zero (already federation)

Critical product fact: Metaflow’s first-class event triggering is documented as Argo Workflows only today. Airflow docs state event-based triggering is not supported via Metaflow’s Airflow integration (only @schedule / cron). Gaius already showed that application-level and platform-level triggering is enough for real product capability. We institutionalize that as Knative Eventing → Airflow, rather than forking Metaflow’s Argo-only path.

Target architecture

                    ┌─────────────────────────────────────┐
                    │  Producers                          │
                    │  CLI · MCP · Atlas OL · Sentinels · │
                    │  Engine gRPC · webhooks · cron      │
                    └──────────────┬──────────────────────┘
                                   │ CloudEvents
                                   ▼
                    ┌─────────────────────────────────────┐
                    │  Knative Eventing                   │
                    │  Broker / Channel / Triggers        │
                    │  (types: metaflow.run.v1, …)        │
                    └──────────────┬──────────────────────┘
                                   │ Sink: Airflow REST / API
                                   ▼
                    ┌─────────────────────────────────────┐
                    │  Apache Airflow (on RKE2, YK queue) │
                    │  DAGs from: python flow.py          │
                    │             airflow create dag.py   │
                    │  @schedule + event-triggered runs    │
                    └──────────────┬──────────────────────┘
                                   │ KubernetesPodOperator
                                   ▼
                    ┌─────────────────────────────────────┐
                    │  RKE2 + YuniKorn                    │
                    │  Metaflow @kubernetes step pods     │
                    │  Queues root.{engine,signals,…}     │
                    └──────────────┬──────────────────────┘
                          ┌────────┴────────┐
                          ▼                 ▼
              ┌───────────────────┐  ┌──────────────────┐
              │ Metaflow metadata │  │ RustFS (S3)      │
              │ service + PG      │  │ artifacts/cards  │
              └───────────────────┘  └──────────────────┘
                          │
                          ▼
              ┌───────────────────┐
              │ signals-ui        │
              │ Applications ·    │
              │ Queues · Status   │
              └───────────────────┘

Component notes

Artifact store — RustFS only

Metaflow’s product property is that every run retains an immutable snapshot of code, data, and dependencies. That snapshot is the first Signals data product (signals.metaflow.snapshots). Mapping and ACP brief: Data Products History.

SettingLab valueLaw
METAFLOW_DEFAULT_DATASTOREs3local is refused
METAFLOW_DATASTORE_SYSROOT_S3s3://metaflow/metaflowSignals RustFS bucket only
METAFLOW_S3_ENDPOINT_URLhttp://127.0.0.1:9010RustFS; in-cluster signals-rustfs:9010

signals.ops.metaflow_store.require_rustfs fails closed if any of those drift. Do not point SYSROOT at /tmp, ~/.metaflow, or a foreign S3.

Metaflow metadata service

  • Deploy as platform Deployment/Service (not engine Tilt thrash).
  • Postgres: platform instance (signals or dedicated metaflow DB on lab PG 5455 / production SoR) — not a second unmanaged store per engine.
  • Expose: ClusterIP + NodePort or signals-ui/proxy for lab (METAFLOW_SERVICE_URL).
  • Config profile (shared ~/.metaflowconfig shape):
{
  "METAFLOW_DEFAULT_METADATA": "service",
  "METAFLOW_SERVICE_URL": "http://metaflow-service.platform.svc:8080",
  "METAFLOW_DEFAULT_DATASTORE": "s3",
  "METAFLOW_DATASTORE_SYSROOT_S3": "s3://metaflow/metaflow",
  "METAFLOW_S3_ENDPOINT_URL": "http://rustfs.platform.svc:9010",
  "METAFLOW_KUBERNETES_NAMESPACE": "metaflow",
  "METAFLOW_AIRFLOW_KUBERNETES_CONN_ID": "kubernetes_default"
}

Airflow

  • Run on RKE2 under YK (queue root.platform for control-plane services; inference steps use root.internal.inference.*, not a project parent).
  • Lab/platform pin: Airflow 3.1.7 (components/airflow chart); cncf.kubernetes provider for KPO.
  • Metaflow steps → KubernetesPodOperator only (@kubernetes, not @batch).
  • Deploy path: python flow.py --with retry airflow create flow_dag.py then ship DAG into Airflow dags/ (Git-sync or ConfigMap pipeline).
  • Time schedules: Metaflow @schedule as today.
  • Event runs: Airflow REST POST /api/v1/dags/{dag_id}/dagRuns (or stable equivalent) invoked by Knative SinkBinding / Service — this is the platform “bring Airflow up to event-triggering spec.”

Knative Eventing (not Argo Events)

ConcernStock OuterboundsSignals
Event busArgo Events + JetstreamKnative Broker (e.g. MT channel / Kafka later)
IngestArgo webhookKnative Broker HTTP + domain sources
FilterArgo sensorsTrigger filters on CloudEvent type / attributes
ActionSubmit Argo WorkflowTrigger Airflow DAG run (+ optional ksvc)

Example event types (illustrative):

  • dev.metaflow.flow.finished — inter-flow chaining (@trigger_on_finish analogue)
  • dev.signals.sentinel.activated — sentinel woke; start inspection flow
  • dev.gaius.article.curate.requested — product CLI/MCP publish
  • dev.atlas.openlineage.runEvent — optional OL-driven reactions

Emitter libraries: small platform helper (replace ArgoEvent.publish) that POSTs CloudEvents to the Broker ingress. Gaius/CLI keep product APIs; they publish platform events instead of talking Argo.

YuniKorn

  • All Metaflow task pods and Airflow workers labeled into federation queues.
  • signals-ui Queues flow already shows root.* capacity; Applications shows admitted apps (including Metaflow steps once scheduled).
  • Avoid default-scheduler side channels for production steps.

Sentinels

  • Remain Knative Serving (scale-to-zero).
  • Eventing can wake work and start Metaflow DAGs when a sentinel signals readiness or C2 heartbeat thresholds — without conflating Serving and Workflow engines.

Isolation vs platform (policy)

ModeWhenMetaflow serviceSchedulerTrigger
Engine isolationSingle-repo devenv, offlineLocal Tilt/PG (Gaius-style)Local / optional K8sCLI/MCP
PlatformShared RKE2, multi-enginePlatform metadata + RustFSAirflow + YK podsKnative Eventing + @schedule

Engines opt into platform by setting Metaflow config profile to platform URLs and deploying DAGs to platform Airflow. Isolation remains for unit hermeticity.

Delivery phases

M0 — Document + guardrails (this page)

  • Architecture + non-goals; signals-ui Applications already frames YK workloads.

M1 — Platform Metaflow service (landed)

ArtifactPath
Submodulecomponents/metaflow (rch/devenv)
K8s manifestsconfig/k8s/metaflow/
Bootstrapscripts/metaflow_platform_bootstrap.sh · just metaflow-platform
Client profileconfig/metaflow/platform.json
RustFS bucketmetaflow (devenv rustfsBuckets)
Lab URLhttp://127.0.0.1:30180/ping (NodePort 30180)

Host PG (:5455) and RustFS (:9010) are bridged into ns/metaflow via Service + Endpoints (signals-postgres, signals-rustfs).

M2 — Airflow 3 on RKE2 (YK) (landed)

ArtifactPath
Submodule chartcomponents/airflow/chart (appVersion 3.1.7)
Helm valuesconfig/k8s/airflow/values-signals.yaml
Host PG bridgeconfig/k8s/airflow/host-bridge.yaml
Cross-ns RBACconfig/k8s/airflow/rbac-metaflow.yaml (KPO → metaflow)
CI DAGconfig/k8s/airflow/dags/signals_ci_dag.py
Bootstrapscripts/airflow_platform_bootstrap.sh · just airflow-platform
CI gatejust airflow-platform-ci
Lab URLhttp://127.0.0.1:30800 (NodePort 30800, admin/admin)

Shape: LocalExecutor, external PG (airflow DB on host :5455 via hostNetwork socat proxy — devenv PG is loopback-only), no in-cluster Bitnami Postgres/Redis. Fernet/JWT/API secrets persist under build/config/airflow-secrets.env. CI DAG signals_ci is live; Metaflow airflow create DAGs ship next (same ConfigMap / dags path). REST: AF3 /api/v2 with JWT from POST /auth/token.

M3 — Knative Eventing bridge (landed)

ArtifactPath
Eventing manifests (v1.23.0)zarf/federation/manifests/knative/eventing-*.yaml (+ IMC + MT broker)
Platform nssignals-events
Brokersignals-events/default (MTChannelBasedBroker / in-memory)
Sinkairflow-dag-trigger (CE HTTP → AF3 JWT + /api/v2/dags/{id}/dagRuns)
Triggerssmoke, metaflow.finished, gaius.curate, …
Bootstrapscripts/knative_eventing_bootstrap.sh · just knative-eventing
Publish helperscripts/signals_events_publish.sh · just events-publish
CI gatejust airflow-eventing-ci (CE → signals_eventing_ci DAG)

No Argo. Engines (Gaius/Aegir/…) and Metaflow finish hooks publish CloudEvents to the Broker; Triggers filter by type and invoke Airflow. Zarf agent ignore until Eventing images are packaged into signals-federation.

M4 — Multi-engine consumers

  • Gaius/Aegir/Atelier profiles point at platform Metaflow
  • signals-ui Applications: show Metaflow run correlation (run id ↔ YK app id)
  • OL: keep Atlas SoR; Metaflow cards remain UX, not second lineage SoR

Critical plane membership

Metaflow (with YK, Knative, Airflow, RustFS, and host data services) is part of the uniform critical plane for Signals — enforced by just stack-ready / just signals-ready / signals:stack-ready. See Critical plane.

Non-goals

  • Touching Gaius, Marquez, or engine repos from this workstream — engines adopt platform Metaflow on their schedule via config + protocol
  • Argo Workflows / Argo Events as SoR — rejected for this platform path
  • Nested Metaflow foreaches on Airflow (Airflow limitation) — avoid in platform DAGs or gate on Airflow version + design
  • Conditional/recursive steps (Metaflow 2.18+) until Airflow support exists
  • Marquez DB as Metaflow metadata (Atlas OL remains lineage SoR)

Open design questions

  1. Airflow executor: start LocalExecutor / CeleryExecutor vs full KubernetesExecutor (DAG volume sync cost on single-node lab).
  2. Event exactly-once: Broker + Airflow dagRun dag_run_id idempotency keys from CloudEvent id.
  3. Namespace layout: one metaflow ns vs per-engine namespaces for pods with YK queue labels only.
  4. UI: Metaflow UI (Outerbounds) optional lab validation vs signals-ui only.

Signals critical plane

Status: Policy (enforced via preflight)
Date: 2026-08-11

A Signals deployment is not “Postgres + a few daemons.” devenv up / devenv up -d is the uniform entry point for a critical plane that spans host processes and RKE2 platform services. Engines integrate via signals-protocol; they do not each re-host thrashing control-plane copies of these services.

Critical plane (uniform)

DomainServiceRoleLab surface
IdentityKerberos KDCData-plane authnjust bootstrap
Data SoRPostgreSQL 16Catalog, AGE, Ranger, Metaflow DB:5455
Object storeRustFSS3 for artifacts, Metaflow datastore, Iceberg warehouse:9010
Iceberg catalogPolarisREST catalog; table data on RustFS:8181 / :8182
GovernanceAtlas (+ Marquez-web validator)Metadata + OL SoR:21010 / :21011
AuthzRangerTag/resource policies:6080
Storage / SQLKudu + ImpalaAnalytic tablesKudu / HS2 :21050
Federation scheduleYuniKornMulti-tenant pod admissionREST NodePort :30080
Scale-to-zero / event fabricKnative Serving + EventingSentinels + CE→AirflowServing + signals-events Broker
Workflow productionAirflow 3Metaflow DAG orchestrationNodePort :30800
Flow metadataMetaflow servicePlatform runs for any engineNodePort :30180
Control UIsignals-uiYK ⊇ backplane:9889
GPU telemetryDCGM exporter + engine OTel yieldLive watts. Pull only. No TSDB.host :9400 / OTLP :9410/v1/metrics

These are uniformly critical: missing any of them is a degraded Signals deployment, not a supported “minimal profile” for product work. Hermetic unit tests may still mock subsystems; the live stack does not.

Not critical (by design)

ItemWhy out of critical plane
Gaius / Aegir engine processesFederated consumers, not platform SoR
Marquez DB / stock Marquez APIRejected — Atlas OL is lineage SoR
Argo Workflows / Argo EventsRejected — Airflow + Knative Eventing
Engine-local Metaflow TiltIsolation mode outside this tree

Enforcement

ToolBehavior
devenv up -dTurn-key entry: full process graph + stack-ready before signals-ui
scripts/signals_stack_preflight.shHost (PG/RustFS/Atlas/Kudu/Impala) + RKE2; auto Metaflow/Airflow
scripts/signals_ready.shCheck-only Gaius-style PASS/WARN/FAIL oneshot (peers / systemd)
scripts/data_plane_preflight.shBinary gate before Kudu/Impala (no compile)
scripts/devenv_process_assert.shFail if process graph is a partial up
just stack-readyPreflight + optional auto-bootstrap (also automatic on up -d)
just signals-readyCheck-only readiness; critical includes Kudu + Metaflow
signals:kerberos-bootstrapWait for KDC → keytabs + kinit before Kudu/Impala
signals:federation-readyYK + Knative (subset)
signals:metaflow-platform / airflow-platformPlatform RKE2 services
just redeployRefresh those product Deployments (complete YK apps, re-place)
devenv up -d                        # preferred — full validate + bootstrap
just stack-ready                    # preflight (may bootstrap missing pieces)
just signals-ready                  # check-only oneshot for peers / systemd
just data-plane-ci                  # Kudu/Impala ports after up (elevated CI gate)

Naming: ready vs CI vs smoke

SurfaceRole
signals-readyFoundation readiness (PASS/WARN/FAIL). Exit 0 iff critical set is ready.
*-ciElevated continuous-integration gates (just airflow-platform-ci, DAG ids signals_ci, CE dev.signals.eventing.ci)
smokeOne-off scripts under ./scripts/ (and some ./tests/); thin wrappers may still exist for habit

Do not elevate “smoke” into system recipe / package / CloudEvent API names — that role is CI.

Topology

devenv up [-d]
  ├── host critical: KDC, PG, RustFS, Atlas, Ranger, Kudu, Impala, signals-ui, …
  └── RKE2 critical: YuniKorn, Knative, Metaflow service, Airflow 3
            │
            ▼
     Federated engines (signals-protocol) — optional consumers

Query Engine & Catalog Stack

Transparent hierarchical storage in Percy’s sense: Kudu for hot mutable rows, Impala for SQL, cooler data as Iceberg files on RustFS. PostgreSQL is the application front door; impala_fdw carries one Kerberos principal through to Impala and Kudu.

Table metadata lives in a PostgreSQL catalog registry (KuduMetaProvider, SignalsDdlExecutor) and is loaded from the Kudu master. Impala talks to Kudu (STORED AS KUDU) and to Iceberg via the Polaris REST catalog. Applications use Postgres foreign tables: kudu_scan for closed hot-tier shapes, impala_sql for Iceberg and UNION ALL views of hot ∪ warm. The cluster path from DCGM into that union, and the agent-exchange join back to those samples, is the appendix diagram.

Stack

HMS-Free Mode

When -Dsignals.hms_free_mode=true is set, the catalog server bypasses HMS entirely:

OperationStandard PathHMS-Free Path
CREATE TABLE (Kudu)HMS → KuduKuduCatalogOpExecutor → Kudu + PG registry
CREATE TABLE (Iceberg)HMS → PolarisIcebergCatalogOpExecutor → Polaris — PENDING (see below)
DROP TABLEHMS → storageSignalsDdlExecutor → storage + PG registry
Load metadataHMS getTable()TableLoader.loadHmsFree() → Kudu master
CREATE/DROP DATABASEHMSKuduMetaProvider → PG registry

Iceberg DDL/DML through Impala is PENDING — in-fork, gradual, with purpose. The Phase-2 IcebergRESTCatalog.createTable() path exists but its created metadata cannot yet be loaded back by the MultiMetaProvider (2026-08-30 phantom-table incident, #SL.00000027.SCHEMA2), and upstream’s REST-catalog support marks tables ACCESSTYPE_READ (“read-only … not supported yet”, IMPALA-13586) so DML is gated at analysis. Doctrine: this fork will implement the Transparent Hierarchical Storage write path over Kudu and Iceberg through our own efforts. Until then the bridges are signals.ops.iceberg_register (PyIceberg → Polaris) for tier1 DDL and out-of-band settle (HDF5 + IcebergHdf5Register) for tier1 writes — bridges, not the architecture.

Modified Components

Eight Java files implement HMS-free mode in the Impala fork (rch/asf-impala on the rch/devenv line):

FileRole
SignalsDdlExecutorRoutes DDL to Kudu or Iceberg without HMS
KuduMetaProviderPostgreSQL-backed catalog registry (databases + tables)
CatalogOpExecutorHMS-free DDL block before standard switch statement
TableLoaderloadHmsFree() constructs metadata from Kudu master
KuduTableHandles null msClient in load()
FrontendSkips MetaStoreClientPool when HMS-free
JniCatalogZero-size pool in HMS-free mode
CatalogServiceCatalogSeeds default database without HMS

Catalog Registry Schema

The PostgreSQL signals_catalog database stores table registrations:

-- config/impala/catalog_schema.sql
CREATE TABLE catalog_databases (
    db_name TEXT PRIMARY KEY,
    comment TEXT,
    location TEXT,
    created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE catalog_tables (
    db_name TEXT NOT NULL,
    table_name TEXT NOT NULL,
    kudu_table_name TEXT,
    created_at TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (db_name, table_name)
);

Current Status

Working (validated end-to-end):

  • CREATE/DROP DATABASE
  • CREATE/DROP TABLE (Kudu, STORED AS KUDU)
  • INSERT INTO (single and multi-row)
  • SELECT with WHERE, ORDER BY, JOINs, GROUP BY, HAVING, subqueries
  • Multiple data types: INT, STRING, DOUBLE, BOOLEAN, BIGINT

Near-term:

  • Iceberg tables via Polaris REST catalog
  • Atlas integration for metadata visibility and tagging

Running the Stack

All services start via devenv or manual scripts:

Statestore  → port 24000  (pure C++, no JVM)
Catalogd    → port 26000  (JVM, HMS-free mode)
Impalad     → port 21050  (JVM, HS2 protocol)
Kudu Master → port 7051
Kudu TServer→ port 7050
PostgreSQL  → port 5455   (signals_catalog DB)

Connect via JDBC:

jdbc:hive2://localhost:21050/default;auth=noSasl

Data Products History

Top-line History is the federation inventory of autonomous data products, viewed as agents understand them. OpenLineage remains the provenance facet (Atlas /api/v1); it is not the nav label.

Sole SoR: Iceberg on RustFS

Logical names stay details / tx / hx. Physical storage is tiered:

NameStoreRole
*_tier0Kudu on $SIGNALS_DATA_ROOT/kuduHot land; expire by DROP RANGE PARTITION
*_tier1Iceberg on RustFS s3a://signals-dataproducts/Settled; add _tier2 when local disks fill
details, tx, hx_*Impala views (UNION ALL)Readers; Impala merges tiers

Writers insert tier0. Time grain is UTC hour; each Kudu range is one week (168 hours). After 4 weeks, copy that week to tier1, verify, then DROP RANGE PARTITION — never DELETE rows (Kudu schema design). Hour is the range unit, not a second hash/range level. Iceberg is not hour-partitioned (file explosion); details_tier1 / tx / hx use product.

Postgres :5455 (devenv PostgreSQL 16 + AGE) is administrative only (Atlas graph, Ranger). It is not pglite. No product copies there. impala_fdw stays Kudu-only and is not the warehouse reader.

Schema:

Aspects are SHACL Core NodeShapes, not products. Instances claim a spec (signals.spec.warehouse_product) and carry aspect.<id> bindings. The shapes graph is signals.aspects.catalog (a product that describes aspects). Wire: ServerQuery kind=ASPECTS. Contract: data_products.md.

details is a fact log (e, a, v, t, op) plus epoch_hour (range unit). t / tx_id is RFC 9562 UUIDv7 (time-ordered). Non-v7 ids are refused and surfaced to the source via Engine/Remediate (TX_ID_NOT_UUIDV7). Current inventory = latest assert per (e, a) over the view.

erDiagram
    PRODUCT ||--o{ TX : "emits"
    PRODUCT ||--o{ DETAILS : "described by"
    TX ||--o{ DETAILS : "asserts at t"
    TX ||--o{ HX_EXCHANGE : "keyed by tx_id"
    TX ||--o{ HX_REASONING : "keyed by tx_id"

    PRODUCT {
        string product_id PK "logical — no Iceberg table"
    }
    TX {
        string tx_id PK
        string product_id FK
        long ts_ns
        string kind
        string summary
        string source
        string ce_type
    }
    DETAILS {
        int epoch_hour "Kudu RANGE unit; week-wide tablets"
        string e FK "product_id"
        string a "peer title kind leaf agent_focus spec aspect.* + snapshot facts"
        string v
        string t FK "tx_id"
        boolean op
    }
    HX_EXCHANGE {
        string tx_id FK
        string product_id FK
        string agent
        string actor
        string message
    }
    HX_REASONING {
        string tx_id FK
        string product_id FK
        string quality
        string lineage
        string delta
        string trace
    }

config/platform/data-products.json is a bootstrap seed for a first tier0 tx + details assert, then retired as SoR. Local JSONL under build/state/ is lab scratch only — never a copy of the warehouse.

Object IO is RustFS (/raid/signals/rustfs, S3 :9010). Impala core-site.xml uses s3a://signals-dataproducts/ — not /tmp.

First product: Metaflow run snapshots

signals.metaflow.snapshots is the first Signals-owned data product. Metaflow already retains an immutable code + data + deps triple per run; we do not re-snapshot. We map that triple into details and refuse any datastore that is not RustFS.

Metaflowdetails fact (e = product)Meaning
current.flow_name / run_idflow_name, run_id, pathspecRun identity
code package URL / shacode_package, code_package_shaImmutable source tarball
{sysroot}/{flow}/data (CAS)data_uriContent-addressed artifacts
{sysroot}/{flow}/{run}snapshot_uriRun prefix (task metadata + pointers)
@conda / @pypi / defaultdepsEnvironment lock (empty = default)
YK proxy or @kubernetesyk_app_id, yk_queueAdmission claim
platform profiledatastore, datastore_root, rustfs_endpointMust be s3://metaflow/ @ :9010

Each run is a tx (kind=snapshot, source=metaflow). Latest-wins on latest_* shows the current run; run-qualified keys run.{flow}/{run_id}.* keep every retained snapshot in the projection so ACP can see the net set, not only the last write.

Fail closed: METAFLOW_DEFAULT_DATASTORE=local (or unset, or a non-RustFS endpoint / bucket) is a warehouse error. Profile: config/metaflow/platform.json (s3://metaflow/metaflow @ RustFS :9010).

just record-snapshot DataProductTierUpkeep 42 \
  --code-package s3://metaflow/metaflow/DataProductTierUpkeep/data/abc \
  --code-sha abc
uv run python -m signals.ops review-product signals.metaflow.snapshots

The product includes the ACP assessment, not only the object URIs. An observer (acp-observer) walks data-product.history-review and writes quality / lineage / delta on hx_reasoning plus details facts assessment, upkeep_nominal, upkeep_fsm. The job is narrow: observe data-product.tier-upkeep and confirm it is proceeding nominally (legal FSM — holding is neither failed nor done — ADD next week, settle only weeks ≥ 4 weeks old via DROP RANGE PARTITION, snapshot on RustFS). Off-nominal is recorded, then fail-closed.

DataProductTierUpkeep.end passes the walk doc into that assessment.

Nascent products (seed)

ProductPeerKind
signals.metaflow.snapshotsSignalsPer-run snapshot (code, data, deps)
gaius.cognition.outputsGaiusCognition
gaius.prospects.corpusGaiusProspects SEC/FMP corpus (compact + summary on RustFS)
aegir.usd-corporaÆgirCorpus
aegir.models.bespokeÆgirFine-tuned / bespoke models
atelier.classification.embeddingsAtelierClassification parquet / embeddings

Events → agent

CloudEvent type: dev.signals.dataproduct.updated.

On create / update / maintain, walk method data-product.history-review:

  1. event_received → reviewing → understood
  2. Activate ACP: Grok-Subscription (root.external.subscription.rate-limited) or Grok-Local with Qwen3.8 capability=[thinking,vision]
  3. Brief must cover quality, lineage, and delta (and nominal for signals.metaflow.snapshots)
  4. Persist a tx + details asserts; persist agent work on hx_* keyed to tx_id
uv run python -m signals.ops review-product gaius.cognition.outputs \
  --kind updated --summary 'article-curate run 18'

review-product writes tier0 (Kudu). JSON catalog is seed only. ACP spawn is the next hop; hx is how History stays agent-facing.

Partition upkeep is method data-product.tier-upkeep (Metaflow flow, Airflow schedule). It surfaces on YK root.platform as proxy sentinel signals-dataproduct-tier-upkeep, or as a real Application if a step is a K8s pod.

just rebuild              # Polarisfork assemble + restart + wait :8182
just polaris-install      # assemble only

UI: /history (nav History). /lineage redirects here. The page is a reader of the warehouse, not a catalog editor.

Peers that maintain their own product on this warehouse: Peer data products. Device samples for a tx join these hx_* rows at read time: DCGM, the warehouse, and agent exchange.

Data Flow

Data flow through the system connects user interaction, the gRPC engine, compute infrastructure, and data sources.

Instruction Path

Data Sources

Streaming Ingest

High-velocity data (click streams, telemetry) arrives via NiFi and is routed to appropriate storage layers. The engine can direct the agent to observe stream accumulation and discover ontology-grounded feature patterns.

Analytical Storage

LayerEngineAccess Pattern
GraphPostgreSQL + AGERelationship queries, ontology traversal
ColumnarKuduLow-latency random access, time-series (hot tier)
SQLImpalaDistributed analytical queries (HMS-free)
Table FormatIcebergLarge-scale batch analytics, schema evolution (warm tier)
MetadataAtlasData catalog, AI-driven classification

Object Storage

S3 buckets store raw data, intermediate results, and model artifacts. In air-gap deployments, an S3-compatible endpoint (RustFS or similar) replaces AWS S3.

Response Path

Computed visualizations flow back through the engine to the web client:

  1. Dask workers complete computation
  2. Datashader rasterizes the result
  3. HoloViews composes the view specification
  4. Engine streams the update over gRPC
  5. Web client renders the visualization in the top panel

OpenLineage + Atlas

Governance and runtime lineage live in one Atlas process: Apache AGE on PostgreSQL 16. Existing Atlas clients keep /api/atlas/*. An OpenLineage extension on the same host serves the Marquez REST surface. Heavy analytics scale through impala_fdw onto Kudu projections of those same entities.

What lives where

ConcernAuthorityNotes
Governance (types, classifications, policies)Atlas on signals PG + AGE/api/atlas/*
Runtime lineage (Job / Run / Dataset, facets)Same Atlas/AGE storeOL types + edges
OL HTTP ingestAtlas OL extensionPOST /api/v1/lineage
Marquez-compatible APIAtlas OL extension/api/v1/* and /api/v2beta/*
OpenLineage UIMarquez-web:21011 (Atlas HTTP + 1); proxies /api/v1 and /api/v2beta
Heavy lineage analyticsFDW → Impala → KuduAtlas projections

components/marquez is the UI and the contract reference. Producers (Airflow, Metaflow, Flink, Gaius, sigint) post RunEvents to Signals.

Completeness

Atlas implements the Marquez OpenLineage API so that:

  1. Marquez-web renders jobs, datasets, events, search, stats, tags, and lineage graphs.
  2. Marquez API contract tests can point at Atlas.
  3. OpenLineage clients talk to one host.

Empty collections return 200. Every path Marquez-web calls returns Marquez-shaped JSON.

Deployment model (one SoR process; UI on adjacent port)

Producers (Flink OL, Airflow, polyglot, sigint, …)
        │
        ▼
 Atlas :21010  (single SoR process)
   ├─ /api/atlas/*     # governance — existing clients, zero changes
   ├─ /api/v1/*        # complete Marquez-compat OL API + ingest
   └─ /api/v2beta/*    # search jobs/datasets (OpenSearch-shaped; AGE-backed)
        │
        ▼
 signals Postgres :5455
   ├─ Atlas AGE graph (entities, classifications, OL Job/Run/Dataset)
   ├─ lineage_events / lineage_tags / lineage_namespaces / lineage_sources
   └─ optional Kudu projections (FDW scale path)

 marquez-web :21011  ── default stack; port = Atlas HTTP + 1; proxies /api/v1 + /api/v2beta → Atlas

Marquez-web forwards /api/v1 and /api/v2beta to Atlas. It is part of every just up. Port rule: MARQUEZ_WEB_PORT = SIGNALS_ATLAS_HTTP_PORT + 1 (defaults 21010 / 21011).

Aegir gateway/marquez.py and Gaius hx.lineage inform graph shape and contract tests.

Atlas client compatibility

ClientConstraint
Atlas REST v2 (/api/atlas/*)No breaking changes
rdbms_* / classificationsAdditive only
AGE labelsNew OL labels additive
Native Atlas lineage GUIContinues on existing lineage APIs
Marquez-web / OL HTTP clientsTalk only to Atlas /api/v1/* (+ v2beta search)

Marquez API surface on Atlas

Implemented in components/atlas (OpenLineageServlet + OpenLineageStore):

AreaPaths (representative)
HealthGET /api/v1/health
NamespacesGET/PUT/DELETE /api/v1/namespaces[/{ns}]
JobsGET /api/v1/jobs, GET/PUT/DELETE .../namespaces/{ns}/jobs[/{job}]
RunsGET .../jobs/{job}/runs, GET /api/v1/jobs/runs/{id}, facets, start/complete/fail/abort
DatasetsGET/PUT/DELETE .../namespaces/{ns}/datasets[/{ds}], versions
TagsGET/PUT /api/v1/tags[/{name}], entity/field tag attach/detach
EventsGET /api/v1/events/lineage
LineageGET/POST /api/v1/lineage, GET /api/v1/column-lineage, GET /api/v1/runlineage/upstream
SearchGET /api/v1/search, GET /api/v2beta/search/jobs, .../datasets
StatsGET /api/v1/stats/{lineage-events,jobs,datasets,sources}
SourcesGET/PUT /api/v1/sources[/{name}]
IngestPOST /api/v1/lineage (OpenLineage RunEvent)

Store: AGE graph signals_ol (Job / Run / Dataset) plus relational lineage_events, lineage_namespaces, lineage_tags, lineage_entity_tags, lineage_sources on the same Postgres database as Atlas.

Marquez in devenv

PieceRole
components/marquezSource submodule (UI + API contract reference)
languages.javascriptdirectory = components/marquez/web; npm.install.enable (enterShell)
tasks.marquez:build-webBefore devenv:processes:marquez-web — npm + webpack; turn-key devenv up [-d]
processes.marquez-webDefault stack — always starts with devenv up; :21011 (Atlas + 1), depends on Atlas
SIGNALS_OL_API_HOST / PORTAtlas (:21010); UI proxies /api/v1 + /api/v2beta there via setupProxy.js
MARQUEZ_WEB_PORTOverride UI bind (default SIGNALS_ATLAS_HTTP_PORT + 1)

There is no marquez:db-init, no marquez PG database, no processes.marquez-api, no Python OL side service.

Validation loop

# After atlas:build + restart atlas process:
curl -sS http://127.0.0.1:21010/api/v1/health
curl -sS http://127.0.0.1:21010/api/v1/tags
curl -sS http://127.0.0.1:21010/api/v1/jobs
curl -sS 'http://127.0.0.1:21010/api/v1/events/lineage?limit=10'
curl -sS 'http://127.0.0.1:21010/api/v1/stats/lineage-events?period=DAY'
# UI: http://<node>:21011  (proxies to Atlas)

Seed via POST /api/v1/lineage with a standard OpenLineage RunEvent; jobs, datasets, events, and lineage should appear in Marquez-web without a Marquez DB.

Upstream Apache Flink is a first-class OpenLineage integration; Cloudera’s stack already proves Flink → Atlas. Dual-path lab check:

PathExpectation
OpenLineageFlink OL → Atlas OL ingest → visible in Marquez-web (via Atlas API)
AtlasEntities / processes remain queryable via stock Atlas clients

BDD @tier-1 (planned): features/platform/lineage_flink_ol.feature.

Phased delivery

PhaseDeliverable
M0Submodule + this doctrine
M1marquez-web default stack process + build-web (UI → Atlas /api/v1)
M2Atlas webapp complete Marquez-compat /api/v1/* + /api/v2beta/* into signals PG/AGE
M3Flink dual-path tests green (OL → Atlas /api/v1; Atlas clients still healthy)
M4Kudu/FDW projections for scale; logical backup via DataFusion Parquet; Gaius/Aegir → Signals

M2 lives in components/atlas (OpenLineageServlet on /api/v1/* and /api/v2beta/*), not in Python. Broader federation (discovery of Atlas/Ranger, OIP, external peers such as Metabase) is documented under Signals protocol core.

  • Signals protocol core — submodule, discovery, OIP
  • Marquez component — UI-only deploy, port rule
  • Scratch: docs/scratch/2026-08-08/145328_marquez-composite-schema-direction.md
  • Scratch: docs/scratch/2026-08-08/151502_no-marquez-db-composite-sor.md
  • Aegir: gateway/marquez.py (contract/shape reference only — not deployed here)
  • Gaius: gaius.hx.lineage (event + AGE labels reference)
  • FDW: components/impala_fdw (scale path for projection tables)

Atlas → Kudu projection outbox contract

Status: Binding for sync implementers (freeze 2026-08-07).
SoR: Postgres + AGE (atlas_graph).
Derived store: Impala/Kudu tables in database atlas (see config/atlas/kudu_projections.sql).

Goals

  • Idempotent, at-least-once materialization of typed projections.
  • Per-key ordered delivery so mutable secondary keys (entity_by_qn) stay correct.
  • Full edge identity on delete so dual adjacency tables do not orphan rows.

Topics / streams

StreamPartition keyEvents
atlas.entityguid (16-byte UUID or hex string of same)create, update, soft-delete, QN/type rename
atlas.edgestable edge id or sha256(src‖elabel‖dst)create, delete
atlas.classificationguidassign, remove tag

Prefer Atlas notification stream (full payloads). Logical decoding is allowed only with the delete rules below.

Per-key order (hard invariant)

Consumers must process events for a given partition key in order (single consumer per key, or Kafka partition = key).

At-least-once + Impala UPSERT is safe for latest-state entity_flat / classifications only under this order.

Mutable qualifiedName (entity_by_qn)

Atlas unique attributes are type-scoped. Digest:

qn_digest = first_16_bytes(SHA-256( type_name || 0x1F || qualified_name ))

(UTF-8; separator 0x1F unit separator.)

Rename / type change on entity G:

1. DELETE FROM atlas.entity_by_qn WHERE qn_digest = digest(old_type, old_qn)
2. UPSERT atlas.entity_by_qn (digest(new_type, new_qn), G, new_type)
3. UPSERT atlas.entity_flat (... new qualified_name, type_name ...)

A retried (1) after (2) without order → missing mapping. Ordering prevents this; Kudu has no conditional version compare on UPSERT.

Entity mutations → Kudu

AGE / Atlas changeKudu ops
Create/update entityUPSERT entity_flat; UPSERT entity_by_qn for current digest
QN or type_name changeOrdered DELETE old digest + UPSERT new digest + UPSERT flat
Soft delete / purgeDELETE/UPSERT tombstone policy (freeze: hard DELETE from projections or state=DELETED on flat only — pick one; default state=DELETED on flat + DELETE from by_qn and edges)

Hot columns on entity_flat only: guid, type_name, qualified_name, name, state, created_ts, updated_ts. All other attributes → props_json (size-capped). Never put search-critical fields only in props_json.

Timestamps: store as BIGINT microseconds since epoch UTC (UNIXTIME_MICROS-compatible).

Edge mutations → Kudu

ChangeKudu ops
Create edge src -[:L]-> dstUPSERT edge_out (src,L,dst,dst_type); UPSERT edge_in (dst,L,src,src_type)
Delete edgeDELETE both sides with full (src, elabel, dst)

Delete decoding prerequisite

If using PostgreSQL logical decoding on AGE label tables:

  • Default REPLICA IDENTITY DEFAULT emits only pkey (graphid) on DELETE — insufficient for Kudu.
  • Required before wiring: either
    • ALTER TABLE … REPLICA IDENTITY FULL on edge label tables feeding the outbox, or
    • source deletes from Atlas notifications with full edge payload.

Do not ship sync without one of these.

Classification mutations

ChangeKudu ops
Assign tag T to guid GUPSERT entity_classifications (T, G, …)
Remove tagDELETE where tag_name=T AND guid=G

No guid-leading mirror table by default (single-entity tag UI stays on AGE).

Audit

Append-only: UPSERT/INSERT into entity_audit with (guid, event_ts, seq).
event_ts immutable monotonic; do not update. Retention via future RANGE partitions + DROP RANGE PARTITION (prod).

Writers

AllowedNot allowed (v1)
Impala HS2 UPSERT/DELETE from outbox workerFDW INSERT/UPDATE/DELETE as SoR
Native Kudu client from workerMulti-table 2PC with AGE

FDW consumers

Foreign tables: config/atlas/kudu_projections_fdw.sql.
Traversal uses frontier loop + src = ANY($1::bytea[]) pushdown — not recursive CTE over foreign tables.

Governance scale plane (Atlas / Ranger → Postgres AGE → FDW → Kudu)

Why this exists: Federated engine services (Ægir, Atelier, Gaius, Hermes) must be able to run simultaneously without overwhelming the lab governance database. This lab uses devenv PostgreSQL 16 on :5455 (services.postgres in devenv.nix) with Apache AGE. That is not pglite. pglite is Atelier’s AMP embedded Postgres and is only intended for AMP-style deployments, not this Signals core.

Scale is achieved by engineering Atlas and Ranger to keep connecting to Postgres, while Postgres uses impala_fdw to reach Kudu for bulk governance tables. RustFS holds durable objects on $SIGNALS_DATA_ROOT/rustfs.

This is a prerequisite for multi-engine operation and for signals-protocol.

Primary path (remember this)

  Atlas JVM  ── JDBC ──►  Postgres :5455  (DB signals + AGE atlas_graph / signals_ol)
  Ranger JVM ── JDBC ──►  Postgres :5455  (DB ranger — admin policies)
                              │
                              │  foreign tables (impala_fdw, Kerberos HS2 / kudu_scan)
                              │  atlas_entity_flat, ranger_tag_resource, …
                              ▼
                         Kudu  (atlas.* / ranger.* projection tables)
                              ▲
                              │ outbox / notification UPSERT (writers)
                         Impala HS2
LayerRole
Atlas / Ranger processesAlways talk to Postgres (JDBC) — topology, admin, policy UI, Cypher
Postgres 16 + AGEThin SoR for AGE graph + Ranger admin; FDW client for scale reads (:5455)
impala_fdwHow this Postgres leverages Kudu without relocating Cypher or policy admin
Kudu projectionsHigh-volume derived entity/tag/edge/audit tables
RustFSObject/blob store and Iceberg warehouse for data products + hx (sole SoR)

Hard rule: Do not host AGE openCypher topology on Kudu. Do not make engines bypass Postgres for Atlas/Ranger identity — they use Atlas/Ranger APIs (or SQL against Postgres :5455, which FDWs to Kudu). Historical audit: docs/scratch/2026-08-07/204458_fdw-atlas-ranger-scale-audit.md.

Federated engines / Hermes
        │ discovery + REST / gRPC
        ▼
  Atlas :21010  /  Ranger :6080  /  S3 :9010
        │                │              │
        │ JDBC           │ JDBC         │ objects
        ▼                ▼              ▼
     Postgres :5455 (AGE)            RustFS
        │  AGE SoR + Ranger admin    ($SIGNALS_DATA_ROOT/rustfs)
        │  + foreign tables
        ▼
     Kudu (scale projections via FDW)

Bulk governance SQL from tools that sit on this Postgres (Aegir, analytics, Weathership helpers) should hit foreign tables → Kudu, not sequential openCypher over the whole estate.

Split of authority

PlaneStoreHoldsNot for
Topology SoRPostgres + AGEVertices/edges, Cypher, hot single-entity pathsBulk multi-engine scans
Policy admin SoRPostgres rangerx_policy*, defs, admin UIHigh-QPS tag membership heap scans
Scale projectionsKudu, exposed as PG foreign tablesentity_flat, by_qn, edges, classifications; Ranger tag denormopenCypher graph
Object / blobRustFSartifacts, lineage packages, Weathership memory, backup objectsRelational SoR
Data products + hxKudu tier0 + RustFS Iceberg tier1 (details/tx/hx views)fact log, tx, hxPostgres AGE / any extra PG copy
Logical packagesDataFusion (signals-df)portable Parquet backup/verifyLive multi-writer SoR

Atlas → Kudu (via Postgres FDW)

ArtifactPath
DDL (Kudu)config/atlas/kudu_projections.sql
FDW (on Postgres :5455)config/atlas/kudu_projections_fdw.sql
Seedjust atlas-kudu-projections-seed
Outbox contractAtlas → Kudu outbox

Atlas continues to use AGE on Postgres as SoR. Projections are derived; consumers on Postgres :5455 query foreign tables. Continuous fill needs the outbox worker (notifications → Impala UPSERT).

Still required for “fully leverage”:

  1. Outbox worker (per-key order, at-least-once)
  2. True kudu_scan / libkudu_client for pk_lookup latency
  3. Atlas/AGE and app SQL prefer foreign tables for bulk entity/tag joins

Ranger → Kudu (via Postgres FDW)

ArtifactPath
DDL (Kudu)config/ranger/kudu_projections.sql
FDW (on Postgres :5455)config/ranger/kudu_projections_fdw.sql
Seedjust ranger-kudu-projections-seed

Ranger admin remains on Postgres. Tag/resource denorm lives on Kudu and is read through FDW so multi-engine load does not thrash x_tag* heaps. TagSync / outbox → UPSERT is near-term wiring.

RustFS (object store)

ItemValue
Data dir$SIGNALS_DATA_ROOT/rustfs (default /raid/signals/rustfs)
S3 APIhttp://127.0.0.1:9010 path-style
Consolehttp://127.0.0.1:9011
Lab credsRUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY (secretspec in shared envs)
Clientmc → alias local
Bucketssignals-artifacts, signals-lineage, weathership-memory, signals-backup, signals-dataproducts
Processprocesses.rustfs (default stack)

Implications for signals-protocol

Discovery should advertise Atlas, Ranger, OL, OBJECT_STORE, and that governance bulk SQL is available via Postgres :5455 + FDW (not “raw Kudu only”).

Engines leave SoR writes to Atlas/Ranger APIs; bulk analytics and tag joins go through the scale plane above.

Ops checklist

just bootstrap
devenv up -d
just atlas-kudu-projections-seed   # Kudu tables + foreign tables on Postgres :5455
just ranger-kudu-projections-seed
just gov-kudu-projections-seed     # both
mc ls local/

Signals Control Plane UI (signals-ui)

Status: Architecture plan (repo + stack decisions locked)
Date: 2026-08-10
Repo: git@github.com:weathership/signals-ui.git
Signals path: components/signals-ui (git submodule)
Related: OpenLineage + Atlas, YuniKorn, MiNiFi sentinels, signals-federation Zarf

Thesis

signals-ui is the primary backplane UI for Signals. Admission, queues, and process visibility go through YuniKorn; operators open :9889.

  1. Superset of stock yunikorn-web — YK REST used in rch-yunikorn-web, plus Signals views (lineage, sentinels, federated surfaces).
  2. Isolated submodule — developed in weathership/signals-ui, vendored into Signals as components/signals-ui (same pattern as other components).
  3. Idiomatic Rust for the service — no Node.js runtime in production. Axum (tokio-rs/axum); Askama HTML. Visual law: Keiretsu (Atelier) + Cloudera logo for brand continuity with Atelier/Aegir; dark + light.
  4. Federation overwatch — MiNiFi sentinels, OTel, Atlas OL lineage, and engine discovery sit beside YK views in one process.
  5. Engine required — /readyz is zndx.engine.v1.Engine/Status with capability=scheduler healthy. SIGNALS_UI_ALLOW_NO_YK=1 is chrome-only lab (skip Status). YuniKorn REST is private to the engine.
                    ┌──────────────────────────────────────────────┐
                    │  signals-ui  (Rust service · Keiretsu UI)    │
                    │  = yk-web capabilities  ∪  Signals value-add │
                    └────────┬──────────┬──────────┬───────────────┘
           YK /ws/v1/*       │  C2/OTel │  Atlas   │  discovery
                             ▼          ▼          ▼
                      YuniKorn     MiNiFi     Atlas :21010
                      (admission)  sentinels  /api/v1 + /api/atlas

0. Repository and process

ItemDecision
Upstream repogit@github.com:weathership/signals-ui.git
Signals submodulecomponents/signals-ui
Language (service)Rust (edition 2021+, idiomatic: axum / tower / tokio / reqwest / serde)
PresentationRust-native UI preferred (Leptos or Dioxus + Trunk, or server-rendered HTML via Askama/Maud + progressive enhancement). No Node service. Asset pipeline must not require a long-lived Node runtime in devenv/prod.
ThemeKeiretsu CSS + Kumo token ramp (vendored from Atelier / cldr-design-template); data-theme="keiretsu" + data-mode="dark|light"
Port policy9889 default (SIGNALS_UI_BIND); yk-web muscle memory.
YuniKornRequired when stack is up (SIGNALS_YK_API_URL). Primary backplane.
Brand logosPluggable packs (SIGNALS_UI_BRAND): cloudera (default) · weathership · custom · SIGNALS_UI_BRAND_DIR
ThemeKeiretsu dark/light (fixed; not brand-switched)
Product identitySignals (brand-agnostic copy/vars)
AuthLab: open or simple; prod: Cloudflare ZT in front (identity-and-access)

Submodule lifecycle (Signals monorepo)

# once repo exists and has an initial commit
git submodule add git@github.com:weathership/signals-ui.git components/signals-ui
# devenv: process + task to build release binary
# cargo build --release -p signals-ui

Do not embed a large Node monorepo under signals root for this product.


1. Strict yunikorn-web superset

1.1 Capability catalog (must implement — parity bar)

Reference: ~/local/src/asf/rch-yunikorn-web/ (Angular; port 9889; hash routes).

Stock routeCapabilitysignals-ui requirement
/dashboardCluster overview, history chartsRequired parity
/applicationsApps by partition/queue; allocations; state logRequired parity
/queues, /queues-v2Queue tree, resources, properties/templatesRequired (both layouts or unified equivalent covering all data)
/nodesCapacity, allocated/occupied/available, foreign allocs, searchRequired parity
/statusScheduler healthchecksRequired parity
ChartsDonut/area/bar as used by dashboard/nodesRequired functional equivalent (not pixel clone)
Error surfaceAPI failure UXRequired

Parity means: an operator who today uses yk-web can complete the same tasks against the same YuniKorn without opening Angular. Layout may differ (Keiretsu); data and actions may not.

1.2 YK REST surface (must cover — contract suite)

From yk-web SchedulerService + health/utilization:

MethodPathUse
GET/ws/v1/clustersCluster list
GET/ws/v1/partitionsPartitions
GET/ws/v1/partition/{p}/queuesQueue tree + resources/templates
GET/ws/v1/partition/{p}/queue/{q}/applicationsApps, allocations, stateLog, times
GET/ws/v1/partition/{p}/nodesNodes + allocations + foreignAllocations
GET/ws/v1/history/appsApp count time series
GET/ws/v1/history/containersContainer count time series
GET/ws/v1/scheduler/node-utilizationsUtilization buckets
GET/ws/v1/scheduler/healthcheckHealth checks

Contract tests live in signals-ui: golden JSON fixtures captured from yk-web’s json-server (json-db.json) and/or live YK; Rust client deserializes every field the Angular models used (see src/app/models/* in yk-web).

1.3 Superset (Signals value-add — after / with parity)

ExtensionSourceNotes
App ↔ sentinel ↔ engineMiNiFi C2, Knative, tagsOverwatch
App ↔ OTel duration/stagesCollector / Tempo-compatible APIUniform process proxy
App ↔ OpenLineage runAtlas /api/v1Semantic I/O lineage
History (data products)Catalog + dev.signals.dataproduct.updatedAgent-facing quality / lineage / delta
Lineage browse (table/column)Atlas OLFacet inside a product, not the top-line menu
Sources / schema / versionsAtlas-enriched OLSame
Federated engine boardDiscovery + zndx.engine.v1Peers
OTel routing viewsCollector config + tracesS04 RCA entry

Rule: YK parity is not optional “phase debt.” Value-add routes may ship behind flags, but YK surface area ships complete before declaring yk-web replaced.

1.4 Deliberately discarded (from stock yk-web)

Stocksignals-ui
Angular 13 + Node toolchain as runtimeRust service; no Node runtime
Material / ad-hoc SCSSKeiretsu + Kumo
json-server as product defaultLive YK (fixtures only for tests)
Scheduler-only productControl-plane product (superset)

2. Rust service architecture

2.1 Crate layout (proposed)

signals-ui/                     # weathership/signals-ui
  Cargo.toml                    # workspace
  crates/
    signals-ui/                 # binary: axum server
    signals-ui-yk/              # YK REST client + models (parity with yk-web models)
    signals-ui-atlas/           # Atlas /api/v1 + /api/atlas clients
    signals-ui-otel/            # OTel query helpers
    signals-ui-core/            # config, error, correlation ids
  assets/                       # keiretsu.css, kumo tokens, static
  ui/                           # Leptos/Dioxus app or templates (if split)
  tests/
    yk_parity/                  # contract tests vs fixtures + live YK
  README.md

2.2 Runtime responsibilities

ConcernImplementation
HTTP serveraxum + tower-http (trace, cors, compression, static)
YK proxy/clientTyped client; optional reverse-proxy path for raw /ws/v1/*
BFF merge APIe.g. GET /api/signals/v1/processes/{id} → YK + OTel + OL
Configenv + optional HOCON/YAML; SIGNALS_YK_API_URL, SIGNALS_ATLAS_HTTP_*, OTel, C2
Health/healthz (liveness), /readyz (Engine/Status scheduler healthy)
MetricsPrometheus /metrics (idiomatic for control plane)

2.3 Presentation options (choose in Phase 0 spike)

OptionProsCons
A. Leptos (CSR/SSR) + TrunkFull Rust, reactive, one languageWASM size; learning curve
B. DioxusFullstack Rust storyEcosystem younger
C. Axum + Askama/Maud + HTMXSimple, tiny, very idiomatic ops UILess “app-like” interactivity
D. Axum serves prebuilt WASM/staticClear splitTwo-step build

Recommendation: spike A or C in week one; pick on binary size, charting story, and team velocity. Charts: prefer Rust-friendly (e.g. plotters → SVG, or lightweight canvas) over pulling a Node chart stack.

Keiretsu/Kumo remain CSS tokens, not a JS theme runtime.


3. UI standards (Keiretsu + Kumo)

Canonical references (do not invent a third palette):

AssetLocation
Keiretsu CSS rampAtelier ui/src/styles/theme-keiretsu.css (upstream: cldr-design-template)
Kumo ramp valuesAtelier ui/src/theme/kumo.ts (restated as CSS vars in signals-ui)
Color modedata-mode="dark" | "light" independent of data-theme="keiretsu"

Laws

  1. Elevation is lightness — canvas < base < elevated
  2. Accents on accent duty only — brand for primary/link/focus
  3. Contrast band — text 10–13:1 on surfaces

Default dark; full light parity required.


4. Atlas backplane (feeds superset lineage)

Closing Marquez-native gaps remains Atlas enrichment; signals-ui is a client. See openlineage-atlas.md.

GapAtlas worksignals-ui view
Column lineageFacet → /api/v1/column-lineageLineage module
Schema / fieldsFacets + rdbms_columnDataset detail
VersionsEvent fingerprint / entity versionHistory tabs
SourcesUpsert lineage_sourcesSources catalog
Duration / last-NOL pairs and YK/OTel merge (§5)Process / run bars

Native Atlas /api/atlas/v2/lineage remains available for governance graphs.


5. Sentinels + YK as uniform process proxy

SignalSourceUI
Admission / queuesYuniKornYK-superset views
Claim / scale-to-zeroKnative + YK app stateSentinel + app detail
C2 healthMiNiFiSentinels module
Duration / stagesOTelMerged process timeline
Semantic I/OOpenLineage → AtlasLineage module
duration_display = coalesce(duration_ol, duration_otel, duration_yk)
correlation: openlineage.runId · yunikorn.applicationId · otel.trace_id · signals.workload_id

Standardize keys in signals-protocol; signals-ui BFF implements merge.


6. Phased delivery

Phase 0 — Repo + Rust skeleton

  • Create weathership/signals-ui (Apache-2.0 or project license as decided)
  • Workspace: axum binary, signals-ui-yk client, health, config
  • Vendor Keiretsu CSS; dark/light shell page
  • Add submodule components/signals-ui in Signals
  • devenv process + cargo task; document port
  • Spike: Leptos vs Askama+HTMX decision recorded in signals-ui README

Phase 1 — YK-web complete parity (superset base)

  • All §1.1 routes/capabilities
  • All §1.2 REST clients + deserialization of yk-web model fields
  • Contract tests vs json-db.json fixtures from rch-yunikorn-web
  • Live YK e2e in lab when scheduler is up
  • Acceptance: side-by-side checklist vs Angular yk-web; no missing operator task

Phase 2 — Superset: process proxy + Atlas lineage

  • Correlation + merged duration API
  • Sentinel + OTel modules
  • Lineage (table; column when Atlas ready)
  • Jobs/datasets/events/sources views (Atlas /api/v1)

Phase 3 — Atlas depth (backplane; may be parallel in components/atlas)

  • Column lineage, fields, versions, sources enrichment
  • OL duration last-N from event pairs
  • signals-ui consumes enriched APIs

Phase 4 — Production federation

  • ZT auth, namespace tenancy
  • OL ↔ Atlas entity projection
  • Zarf/package packaging for air-gap control plane

7. Non-goals

Non-goalWhy
Node.js service or Node-based devenv dependency for signals-uiExplicit departure
Investing in or owning Angular yunikorn-webApache YK focuses on core features; stock SPA may ship disabled. Not Keiretsu. Optional lab exposure is OK while our pin still includes it enabled
Growing Marquez-web into control planeWrong ownership; temporary validator only
Reimplementing YuniKorn schedulingClient of /ws/v1 only
Subset “dashboard-only” replacement of yk-webMust be strict superset

8. Acceptance criteria

YK-web replacement (hard gate)

  • Every yk-web operator task works in signals-ui against live YK
  • Contract suite green for all §1.2 endpoints
  • Documented mapping: yk-web screen → signals-ui route

Superset

  • At least one Signals-only module (sentinels or lineage) shippable
  • Merged duration for a process with YK+OTel and optional OL

Platform

  • Single Rust binary (or small set of crates) in release builds
  • Keiretsu dark + light
  • Submodule builds under Signals devenv/CI

9. Immediate next actions

  1. Create weathership/signals-ui empty repo + Apache-2.0 (or project) license
  2. Scaffold Axum hello + YK client crate + Keiretsu static shell
  3. Import yk-web json-db.json as fixtures; first contract test (/ws/v1/partitions)
  4. Submodule add into Signals when skeleton builds
  5. Presentation spike (Leptos vs HTMX) ≤ 1 week, then freeze
  6. Parallel in Atlas: column-lineage facet spike (feeds Phase 2–3)

  • OpenLineage SoR: openlineage-atlas.md
  • Federation Zarf: signals-federation-zarf.md
  • Sentinels: components/signals-protocol/specification/operations/minifi_sentinels.md
  • YK web reference (capability only): ~/local/src/asf/rch-yunikorn-web/
  • Theme: Atelier ui/src/styles/theme-keiretsu.css, ui/src/theme/kumo.ts

Identity and access (authn → authz)

Signals (and the broader ZNDX constellation) separates who you are from what you may touch. There is no product path that skips either layer.

Layers

LayerMechanismPurpose
Edge / human & agent accessCloudflare Zero Trust + IdPs (Okta OIDC, GitHub OAuth)Reach apps and APIs without “open network + hope”
Cluster / data planeKerberos (lab/devenv required; FQDN SPNs)Impala, Kudu, and related clients — no NOSASL
Governance metadataApache AtlasClassifications, types, SoR for “what is this data”
AuthorizationApache Ranger (tag + resource policies)Enforce “who may use this table/column/tag” using Atlas tags

Zero Trust is the outer door. Kerberos (or equivalent workload identity in cloud) is the inner door for engines. Atlas + Ranger turn “authenticated” into “authorized.”

Precedent: Gaius

Gaius already puts Cloudflare at the edge:

  • Workers on gaius.zndx.org — OAuth callbacks, A2UI surfaces, KV sessions (gaius/infra, gaius-ui/apps/worker).
  • IdP direction (gaius-kb-interop roadmap): GitHub OAuth (developers), Okta OIDC (enterprise), unified under Cloudflare Zero Trust Access policies (group membership, risk signals, SCIM).
  • WebRTC called out next to the SPA: access-controlled edge first, then media client.

That is the house pattern: edge authn (CF Access + IdP) → session/JWT → app and media planes — not browser → bare port on the lab network.

Precedent: synth (early HTTPS / secure context)

Synth’s multi-client observer work already hits the hard gate:

  • getUserMedia (mic/camera) requires a secure context (HTTPS) off localhost — WebKit/iPad has no exception.
  • Today: Caddy TLS in devenv for LAN secure context (internal CA).
  • Explicit alternative called out: cloudflared (atelier-style tunnel) — good for remote reach, weaker as a pure LAN story unless paired with Access.
  • WebRTC is the upgrade path for lower-latency audio/video once the observer chain is multi-client.

Early synth adoption is “TLS for observations.” Full product federation of audio + video agent observations (and later omni models) needs ZT + HTTPS as the default, not a late bolt-on.

WebRTC, omni models, federation

CapabilityWhy ZT / robust HTTPS matters
Agent observation of synthesizer chainsBrowser sensors + web UI must be secure-context; multi-device observers
WebRTC A/VDTLS/SRTP assumes proper HTTPS origin and stable identity for signaling
Nvidia-class omni modelsMulti-modal streams (A/V + text) across federated sites — cannot run on ad-hoc open ports
Signals governance UIsAtlas/Ranger/Marquez-web and future OL surfaces behind the same edge discipline
Federated engines + OIPDiscovery of Atlas/Ranger/OL; model invoke carries authz context and leaves OL provenance — see Signals protocol core
External peers (e.g. Metabase)Same edge + Ranger discipline; consume discovery, never become a second SoR

Federation of multi-modal agent features should assume:

  1. Cloudflare Zero Trust (or equivalent) in front of every human/agent web surface.
  2. Okta / GitHub as IdPs into Access policies.
  3. Kerberos (or cloud IAM) for service-to-service data plane.
  4. Atlas classifications → Ranger policies for data authorization.

Signals today (lab)

ConcernState
Bootstrapjust bootstrap — Kerberos KDC, keytabs, FQDN, kinit (required)
Impala / KuduKerberos only; GSSAPI clients via signals.impala
Atlas / RangerAuthz spine; portable SoR via just backup / just restore
Edge ZTNot yet wired in this repo — adopt Gaius/Access patterns when product UIs leave localhost

Non-goals / rejected

  • NOSASL or “auth optional” for Impala/Kudu in devenv
  • Shipping multi-modal agent UIs over plain HTTP outside localhost
  • Treating Cloudflare only as CDN without Access policies for app routes
  • Lab Kerberos: docs/current/src/operations/storage-and-backup.md,
    scripts/signals_kerberos.sh, just bootstrap
  • Gaius edge: gaius/infra, gaius-ui/apps/worker
  • Gaius IdP roadmap: gaius-kb-interop/design (Okta + Zero Trust + WebRTC)
  • Synth secure context: synth docs on multi-client observers + Caddy TLS
  • Arclight GTM: frictionless zero-trust access as adoption requirement

Deployment Modes

Signals supports four deployment modes, from local development to full cloud production.

ModeComputeK8sServices
LaptopLocalk3d (optional)devenv
WorkstationLocalRKE2 (optional)devenv + GPU
HybridLocal + AWSRKE2 (local) + RKE2 (AWS)Split
Full AWSAWS EC2RKE2All remote

Laptop Development

Local-only with devenv services (PostgreSQL, KDC). Optional k3d cluster for Kubernetes workloads.

devenv up                              # Start PostgreSQL + KDC
devenv tasks run k8s:provision         # Optional: create k3d cluster
devenv tasks run k8s:deploy-dask       # Optional: deploy Dask on k3d

Best for: iterating on engine code, feature development, BDD scenarios at Tier 0-1.

Workstation

Local devenv with optional RKE2 for production-grade Kubernetes. Suitable for GPU-accelerated workloads.

devenv up                              # Start core services
devenv tasks run k8s:deploy-dask       # Deploy Dask on RKE2

Best for: integration testing, GPU workloads, running Tier 2 scenarios.

Hybrid (Workstation + AWS)

Local development environment with AWS RKE2 cluster for distributed compute. The engine and visualization run locally; heavy computation is offloaded to AWS.

devenv up                              # Local services
devenv tasks run aws:provision         # Provision AWS infrastructure
devenv tasks run aws:deploy            # Deploy RKE2 + Dask on AWS

Best for: distributed workloads that exceed local capacity, Tier 3 scenarios.

Full AWS

Everything runs on AWS EC2 with RKE2. Access via Cloudflare Zero Trust (WARP device posture).

devenv tasks run aws:provision         # VPC, EC2, S3
devenv tasks run aws:inventory         # Generate Ansible inventory
devenv tasks run aws:deploy            # Full stack deployment
devenv tasks run aws:verify            # Post-deployment checks

Best for: production deployments, air-gap environments, Tier 3 full-stack validation.

Air-Gap Variant

Any mode with Kubernetes can use Zarf for disconnected deployment. The air-gap variant removes the NAT gateway (airgap_mode = true in OpenTofu), relying on VPC endpoints for S3 access and the Zarf internal registry for container images.

devenv tasks run aws:provision         # With airgap_mode = true
ansible-playbook playbooks/airgap-deploy.yml

See Air-Gap (Zarf) for details.

Infrastructure Layers

Each deployment mode uses a subset of the infrastructure stack:

LayerToolPurpose
ProvisioningOpenTofuAWS VPC, EC2, S3, Cloudflare
ConfigurationAnsibleRKE2, Dask, services, tunnels
PackagingZarfAir-gap K8s deployment
Dev IterationTiltLive-reload on RKE2/k3d
PolicyOPA/ConftestGuardrails for IaC and K8s

See the Infrastructure section for detailed documentation of each layer.

Metadata Tagging

sigint classifies Impala columns against the SIGDG ontology and writes those tags to Atlas. Ranger consumes the tags. Discoverability and sensitivity labels come from that loop.

Architecture

How It Works

  1. Tables created in Impala become visible in Atlas as entities with columns, types, and ownership metadata.

  2. The tagging service watches Atlas for new or changed entities (tables, columns). When a new table appears, the service reads its name, column names, column types, and any existing metadata.

  3. The classification model maps each name to the SIGDG ontology, assigning both a semantic category and a sensitivity quality. For example:

    • Column email → SIGDG:0025 ContactInformation with SIGDG:1030 Confidential
    • Column ssn → SIGDG:0011 GovernmentIdentifier with SIGDG:1040 Restricted
    • Column total_amount → SIGDG:0021 FinancialInformation with SIGDG:1020 Internal
    • Table transactions → SIGDG:0050 TransactionInformation
  4. Classifications are written back to Atlas as tags on the entity, making them visible in the catalog UI and available for policy enforcement via Ranger.

The classification model uses a context engineering approach — each column is represented as a structured feature vector that can be measured and optimized using SAGE feature importance analysis. The pipeline outputs Dempster-Shafer belief intervals at every hierarchy level via the evidence fusion layer.

Atlas Integration

Atlas provides the metadata catalog that makes Impala-managed Kudu tables and columns visible as governed entities:

  • Entity types: rdbms_table, rdbms_column, rdbms_db, rdbms_instance — Atlas stock RDBMS model (2000-RDBMS), same family as Aegir (catalog bridge cut over from interim hive_*)
  • Classifications: Applied as Atlas tags using SIGDG CURIEs (e.g., SIGDG_0025_ContactInformation)
  • Lineage: Atlas tracks table-level lineage from INSERT…SELECT and CTAS operations
  • AGE graph queries: Atlas metadata stored in PostgreSQL with AGE enables graph traversal of classification relationships, lineage paths, and impact analysis

Atlas + Ranger Policy Flow

Once entities are tagged in Atlas:

  1. Ranger policies reference SIGDG classifications (e.g., “mask all columns tagged SIGDG:0022 PaymentInformation”)
  2. Impala enforces Ranger policies at query time
  3. The result: automated column masking, row filtering, and access control driven by ontology-grounded metadata tags

Validated Pipeline

The end-to-end tagging pipeline has been implemented and validated with 8 BDD scenarios (6 meta-tagging + 2 pipeline tagging):

  1. Atlas catalog bridge — Python function registers Impala-managed Kudu tables in Atlas via REST API. Validated by 4 catalog sync scenarios.
  2. Tagger service — Tagger class (src/sigint/tagger.py) orchestrates sample → classify → tag:
    • ImpalaSampler reads column names, types, and sample values from Impala
    • EmbeddingClassifier classifies against the SIGDG taxonomy using sentence-transformer embeddings
    • AtlasClient writes classifications back as SIGDG tags with confidence scores and evidence
  3. Air-gap isolation — Shared HF caches (HF_HOME / HF_HUB_CACHE / SENTENCE_TRANSFORMERS_HOME, lab RAID), HF_HUB_OFFLINE=1, zero external network calls. See Air-Gap (Zarf).
  4. HOCON config — Single source of truth (config/base.conf) drives both application code and BDD test helpers. All connection parameters (Impala host/port, Atlas URL/credentials, model cache) flow from config.

Running the Pipeline

# Pre-cache model (once)
just cache-models

# Dry-run: classify without writing to Atlas
just tag-dry-run default.my_table

# Live: classify and write SIGDG tags to Atlas
just tag default.my_table

# Or via Python module
uv run python -m sigint --tables default.my_table --dry-run

Next Steps

  1. Event-driven tagging: Watch Atlas for new entities and trigger classification automatically (replace manual just tag invocation)
  2. Evidence fusion calibration: Calibrate DST discount factors and mass constants against held-out data; resolve source independence assumption (see Research Roadmap)
  3. OWL formalization: Publish SIGDG as OWL ontology with BFO imports, verify via RASE 5-gate pipeline
  4. Ranger tag-based policies: Column masking and access control driven by Atlas SIGDG classifications
  5. Feedback loop: Analyst corrections to classifications improve the model over time
  6. Data lifecycle integration: Preserve classifications across Kudu→Iceberg CTAS migrations

Context Engineering

Context engineering is the discipline of deliberately constructing, measuring, and optimizing the input features fed to an embedding classifier. Rather than treating the text assembled from a column’s metadata as an ad-hoc string, we decompose it into 12 discrete, ablatable features and use SAGE (Shapley Additive Global importancE) to quantify each feature’s contribution to classification accuracy.

The goal: replace intuition about “what information helps the classifier” with measured Shapley values that show exactly which features drive predictions and which are noise.

The Problem

An embedding classifier that encodes column metadata as free-form text achieves high accuracy on well-named columns — but most correct predictions are assisted by string-matching heuristics (name boost). Columns with opaque or generic names (e.g., col0, field_42) expose the model’s dependence on column names: cosine similarity drops dramatically on these columns while maintaining high accuracy on semantically named columns.

To build a classifier that generalizes beyond known column names, we need to:

  1. Know which features actually contribute to correct predictions
  2. Measure how much accuracy degrades when each feature is removed
  3. Systematically reduce reliance on name matching
  4. Compare multiple classification methods on the same feature set

Feature Decomposition

Each column is represented by 12 named features extracted from its metadata:

#FeatureSourceExample
1column_nameHumanized column name"payment card number"
2column_typeData type (suppressed for string/varchar)"int"
3sample_valuesUp to 5 sample values, truncated to 80 chars"4111..., 5500..."
4cardinalityDistinct value count in sample"cardinality=42"
5null_rationull_count / total_count"null_ratio=0.15"
6value_entropyShannon entropy of value lengths (bits)"entropy=2.34"
7pattern_signalsDetected regex patterns"patterns: email_pattern, uuid_pattern"
8avg_value_lengthMean character length"avg_len=24.5"
9numeric_ratioFraction parseable as number"numeric=0.95"
10sibling_contextHumanized names of other columns in same table"siblings: first name, last name, email"
11source_tableTable name"table=identity_data"
12value_descriptionNL description of value patterns (substitutes for generic names)"column of date values in YYYY-MM-DD format"

Features are composed into pipe-separated embedding text via ColumnFeatures.to_embedding_text(mask), where the mask controls which features are included. When a column has a generic name (col0, field_1, Unnamed), the value_description feature substitutes for the uninformative name in the embedding text; otherwise it is appended as complementary information. See Heuristic Elucidation for the methodology behind this feature.

With all features enabled, a column might produce:

payment card number | int | 4111111111111111, 5500000000000004 | cardinality=42 |
  patterns: credit_card_pattern | avg_len=16.0 | numeric=1.00 |
  siblings: first name, last name, expiry date | table=personal_data

Pattern Detectors

Eight regex-based pattern detectors scan sample values for structural signals:

PatternWhat it matches
email_patternuser@domain.tld
phone_pattern+1 (555) 123-4567
ssn_pattern123-45-6789
ipv4_pattern192.168.1.1
uuid_pattern550e8400-e29b-41d4-a716-446655440000
date_iso_pattern2024-01-15
url_patternhttps://example.com
credit_card_pattern4111111111111111

A pattern is flagged when at least 1/3 of sample values match. This threshold prevents spurious matches from a single outlier value.

SAGE Feature Importance

SAGE (Shapley Additive Global importancE) measures the global importance of each feature by estimating how much each feature contributes to reducing classification loss. Unlike local explanation methods (LIME, SHAP per-instance), SAGE provides a single importance score per feature across the entire dataset.

How It Works

SAGE marginalizes each feature by substituting values from other samples in the dataset. For example, when marginalizing column_name for column email, SAGE asks: “what if this column had the name trade_secrets but the same sample values, cardinality, and patterns?”

The implementation:

  1. Feature index matrix: X has shape (N, 12) where X[i, j] = i — each sample initially uses its own value for every feature.

  2. Value lookup tables: For each feature j, a table maps sample index → feature text. When SAGE permutes X[i, j] to index k, the model uses sample k’s value for feature j instead.

  3. Model wrapper (FeatureMaskModel): Looks up text from the per-feature tables according to X, assembles embedding text, encodes via sentence-transformers, and returns softmax’d cosine similarities as class probabilities.

  4. SAGE estimation: MarginalImputer + PermutationEstimator with cross-entropy loss estimates each feature’s Shapley value across n_permutations (default 512) random orderings.

The result is a vector of 12 importance values with standard deviations — a quantitative answer to “which features matter?”

Pipeline

The classification pipeline runs in four stages, producing a parquet with three independent signals per column:

Three-Signal Comparison

When ground truth is provided (--ground-truth), the pipeline produces three independent classification signals per column:

SignalMethodSemantic NamesOpaque NamesOverall
CosineZero-shot embedding similarity + name boostHighLowModerate
CatBoost CVAugmented stratified k-fold CVModerateLowModerate
CatBoost train→evalSynthetic training + ordered boosting + column propagationHighHighHigh
LLM GTExpert column→code mapping (target)———

The CatBoost CV baseline uses category reference embedding augmentation to overcome extreme low-data regimes. Each fold’s training set includes all category reference embeddings (the same texts cosine uses as targets), giving at least 2 training points per class even in held-out folds.

The train→eval pipeline replaces k-fold CV with synthetic training data and several additional techniques. See Classification Training for the full methodology and accuracy progression.

The pipeline always produces Dempster-Shafer belief intervals at every hierarchy level. See Evidence Fusion for the full DST architecture.

Column Kinds

The pipeline classifies each column into one of three kinds based on naming patterns:

  • data: Semantically named columns (email, first_name, amount) — cosine excels here
  • opaque: Generic or positional names (col0, field_42) — CatBoost train→eval is needed
  • row_id: Row identifiers — excluded from GT evaluation

Ablation Runs

The pipeline supports controlled ablation experiments:

# Three-signal comparison with ground truth
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> \
    --taxonomy <taxonomy> --threshold 0.25 \
    --ground-truth <ground-truth.json> \
    --output build/sigint_embeddings.parquet

# Feature ablation (disable specific features)
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> \
    --taxonomy <taxonomy> --threshold 0.25 \
    --disable-features sample_values sibling_context \
    --output build/sigint_ablation.parquet

# With SAGE feature importance
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> \
    --taxonomy <taxonomy> --threshold 0.25 \
    --sage-permutations 512 \
    --output build/sigint_embeddings.parquet

# Visualize with embedding-atlas
embedding-atlas build/sigint_embeddings.parquet --text embedding_text

Output Schema

Each run produces a parquet (38 columns) and companion report JSON:

Column GroupColumnsDescription
Identityembedding_text, source_table, column_name, sample_values, column_kindColumn metadata and kind
Cosinetag_code, tag_label, tag_abbrev, confidence, boostZero-shot cosine predictions
Ground Truthgt_code, correctLLM GT evaluation (when provided)
CatBoost CVml_tag_code, ml_tag_label, ml_confidence, ml_correctCross-validated ML predictions
Featuresfeat_column_name … feat_value_description (×12)Transparency: input features as strings
SAGEsage_column_name … sage_value_description (×12)Global feature importance values

Current Status

The train→eval pipeline closes the opaque-name accuracy gap. Remaining errors are inherently confusable category pairs — types whose values are structurally identical (e.g., two address subtypes, two hex identifier formats). See Classification Training for the full methodology.

What remains:

  1. Confusable pair resolution — remaining errors cluster in category pairs that share identical value patterns. Resolving these requires either richer context (e.g., table-level schema hints) or category consolidation
  2. Feature refinement — richer pattern detectors (date formats, currency symbols, statistical distributions), deeper sample analysis (value distributions, min/max/mode)
  3. Cross-dataset validation — accuracy on datasets with different naming conventions and taxonomies (see GitTables CTA benchmark)
  4. Feedback integration — analyst corrections feed back as training signal, with SAGE tracking whether corrections improve non-name features

Classification Training

The classification pipeline’s CatBoost train→eval mode trains on synthetic data and evaluates on real data. This page documents the synthetic data generator, the training pipeline, and the techniques that bridge the domain shift between synthetic and real columns.

Note: CatBoost replaced XGBoost in March 2026. CatBoost’s ordered boosting (posterior_sampling=True) eliminates the self-training target leakage identified in the integrity audit. See the CatBoost migration section for details.

Why Synthetic Training

Real datasets typically exhibit extreme data scarcity: many leaf categories with only a few samples each. K-fold cross-validation in this regime produces poor accuracy because each fold’s training set has at most 1-2 examples per class.

A second challenge is the opaque name problem. Columns with generic or positional names (e.g., col0, field_42) carry no semantic signal. Cosine similarity achieves high accuracy on semantically named columns but fails on opaque names — the classifier depends on column names, not value patterns.

Synthetic training data addresses both problems: it generates thousands of columns per category with controlled name diversity, and half the synthetic columns use opaque names, forcing CatBoost to learn from value patterns rather than column names.

Synthetic Data Generator

scripts/generate_meta_tagging_train.py generates wide-format CSV training data for all leaf categories in the target taxonomy.

Name Generation

Each category produces two kinds of synthetic column names:

Semantic names use a synonym table (~60 terms with abbreviations and variants) to generate diverse human-readable names. Techniques include:

  • Case variants: snake_case, camelCase, UPPER_SNAKE_CASE
  • Synonym expansion: phone → tel, ph, phn
  • Word-dropping: payment_card_number → payment_number, card_number
  • Prefixed variants: user_, primary_, customer_, acct_, src_, raw_

Opaque names use 13 prefix templates (field_, col_, meta_, var_, etc.) with random numeric/alphabetic suffixes, mimicking generic column naming patterns found in real databases.

Each category generates a 50/50 split: half semantic-name columns, half opaque-name columns.

Value Generators

70+ generator functions cover all SIGDG leaf categories across the full taxonomy:

  • Payment card data — PANs (Visa/MC/Amex/Discover prefixes), CVVs, magstripe tracks, BINs, last-4, expiration dates, masked PANs, PINs
  • Identifiers — SSNs, passports, driver’s licenses, CPF (Brazil), PAN (India), VATIN, UUIDs, device IDs, serial numbers, MAC addresses
  • Contact — full names, first/middle/last/nickname, emails, phone numbers (full and split: country code, area code, subscriber, extension), social media handles
  • Addresses — full addresses, split components (street, city, state, postal, country), billing/shipping variants, coarse/precise geolocation
  • Demographics — age, birthdate, gender, race, education, employment, income, benefits, background checks
  • Health — mental health conditions, physical conditions, genetic markers, biometric IDs
  • Product usage — login events, crash types, security references, forum posts, photos, videos, user activity, device metrics, app bundles, user agents
  • Authentication — passwords (masked), security Q&A, e-signatures, key material, key digests
  • System — IP addresses, LDAP groups, permissions, session data, URLs, cluster nodes, file paths, executables, runtime data
  • Business — document references, SEC filings, trade secrets

Output Format

build/datasets/sigint_train/
  ├── synth_001.csv ... synth_NNN.csv   (wide-format, 100 rows each, 50 cols per file)
  ├── ground_truth.json                  (column → SIGDG code)
  └── taxonomy.csv                       (copy of controlled vocabulary)

CSV headers use table.column format (e.g., synth_001.credit_card_number) matching the eval data’s convention.

Train/Eval Pipeline

The --train-dir flag in build_sigint_embeddings.py activates train→eval mode. The pipeline applies four techniques that collectively raise accuracy from a 33.4% naive baseline to 83.1%.

Feature Vector

Each column is represented by a 992-dimensional feature vector:

[full_emb(384) | value_only_emb(384) | discrete(12) | cosine_sims(212)]
ComponentDimensionsDescription
Full embedding384Sentence embedding of all features (name + values + table + siblings)
Value-only embedding384Sentence embedding with name, table, and siblings stripped
Discrete features12Cardinality, null ratio, entropy, 8 pattern flags, value description — scaled by \(\sqrt{384/12}\)
Cosine similaritiesNSimilarity between value-only embedding and each category reference

Dual Embedding

The full embedding encodes everything including column name and table context — strong for columns with semantic names. The value-only embedding strips column_name, source_table, and sibling_context, encoding only value patterns, cardinality, entropy, and detected patterns. This bridges the domain shift for columns with opaque or generic names.

Category Reference Augmentation

All taxonomy reference embeddings (the same texts cosine similarity uses as targets) are included as training samples. These anchor embeddings exist in the same embedding space as the real eval data, teaching CatBoost the mapping from embedding regions to categories even when synthetic embeddings differ from real ones.

Cosine Similarity Features

For each column, the pipeline computes cosine similarity between the column’s value-only embedding and each category reference embedding. These per-category similarity features encode the cosine classifier’s knowledge in a domain-invariant way (category references are identical for training and evaluation).

Discrete Feature Scaling

The 12 discrete features (cardinality, null ratio, entropy, 8 pattern flags, value description) are multiplied by \(\sqrt{384/12} \approx 5.7\) so their magnitude competes with the 384-dimensional embedding vectors. Without scaling, gradient boosting ignores the discrete features because individual embedding dimensions dominate split gain.

Paired Column Propagation

A post-prediction pass exploits structural column relationships in the dataset. When CatBoost is uncertain about one column (confidence < 0.50) but confident about a structurally related column (confidence ≥ 0.35), the confident prediction propagates to the uncertain one. This is a dataset-specific heuristic — the particular structural relationship varies by dataset (see Heuristic Elucidation).

StandardScaler

The full feature vector is normalized with StandardScaler before CatBoost training for stable gradient boosting across features with different scales.

Results

Accuracy progression on a GT-labeled evaluation set, showing each technique’s marginal contribution:

TechniqueOverallData ColsAnn Cols
Baseline (k-fold CV)48.0%——
Cosine-only (5-source DST)84.6%——
Synthetic train→eval + dual embedding83.1%88.0%78.3%
SVM standalone (TF-IDF)84.6%——
Self-train (GT injection)99.4%99.4%99.4%

The naive k-fold CV (48.0%) cannot learn with ~2 examples per class. Synthetic training data + dual embedding (full + value-only) provides the largest gain. Column propagation corrects 43 annotation columns where CatBoost is uncertain but the preceding data column is confident. Self-training injects GT-labeled evaluation columns into the CatBoost training set, raising accuracy from 83.1% to 99.4% — legitimate for the LLM annotation reproduction workflow (see Self-Training Mode).

Validated numbers (2026-03-24, GPU-accelerated): CatBoost standalone (dual embedding + cosine sims + ordered boosting + propagation) achieves 83.1% (291/350, data=88.0%, ann=78.3%). CatBoost with self-training: 99.4% (348/350, data=99.4%, ann=99.4%) — only 2 residual errors, both in the Masked PAN category. SVM standalone: 84.6% (296/350). SAGE analysis: 489s on GPU vs 13,278s on CPU (27x speedup). CatBoost requires CPU for posterior_sampling=True and rsm=0.6 (not supported on CatBoost GPU for multiclass).

SVM text format fix (2026-03-24): The SVM text format mismatch (SVM trained on short name | type | values but evaluated on full 12-feature text) has been fixed. EmbeddingClassifier._build_svm_text() now ensures consistent short text format between training and inference. 5-source DST accuracy corrected from 60.6% to 84.6%.

Discovery Methodology

Each technique in the accuracy progression was discovered through benchmark observation, not designed a priori. Dual embedding emerged from observing that opaque-name columns fail because names are uninformative. Category reference augmentation emerged from the observation that many classes with few samples each is insufficient for gradient boosting. Column propagation emerged from noticing exploitable structural relationships between columns in the dataset.

This pattern — observe a phenomenon, hypothesize a mechanism, implement it as a feature, and measure its contribution with SAGE — is the heuristic elucidation methodology. SAGE Shapley values quantify each technique’s marginal contribution, and cross-benchmark validation (against GitTables CTA) prevents overfitting to a single dataset.

CatBoost Migration

CatBoost replaced XGBoost in March 2026 for two reasons:

1. Self-Training Redesign

The XGBoost pipeline included an implicit self-training step that injected cosine pseudo-labels (confidence ≥ 0.50) from evaluation data into the training set. For benchmark evaluation (where test/train splits matter), this constituted target leakage. The implicit self-training was removed and replaced with an explicit, opt-in --self-train mode (see Self-Training Mode) for the LLM annotation reproduction workflow where it is legitimate.

2. Ordered Boosting

CatBoost’s posterior_sampling=True enables ordered boosting — a method that constructs each tree using only “historical” data points, preventing the prediction shift that occurs when a model is trained and evaluated on the same data distribution. This provides a principled alternative to self-training without leakage.

Hyperparameter Mapping

XGBoostCatBoostNotes
objective="multi:softprob"loss_function="MultiClass"Both produce probability vectors
n_estimatorsiterationsNumber of boosting rounds
max_depthdepthMaximum tree depth
reg_alphal2_leaf_regL2 regularization
colsample_bytreersmRandom subspace method (feature sampling)
min_child_weightmin_data_in_leafMinimum samples per leaf
—posterior_sampling=TrueOrdered boosting (CatBoost-specific)

Evidence Fusion Integration

CatBoost’s predict_proba() output is now available as a mass function source for evidence fusion. When a trained CatBoost model is loaded, its class probabilities are converted to a BeliefAssignment via catboost_to_mass(), contributing independent evidence alongside cosine similarity, pattern detection, and name matching. CatBoost’s virtual ensembles can additionally provide per-class variance estimates, enabling adaptive discounting: high model uncertainty → more mass allocated to \(\Theta\) (total ignorance).

SHAP Explanations

The pipeline computes per-item feature explanations using CatBoost’s built-in TreeSHAP (\(O(TLD)\) complexity). For each classified column, the top 3 most influential feature groups are reported:

ColumnFeature GroupDescription
shap_top1_nameFeature group namee.g., full_embedding, sample_values, column_name
shap_top1_valueSHAP magnitudeSummed absolute SHAP value for the feature group

CatBoost’s 992-dimensional feature space is grouped into interpretable categories: full_embedding (384 dims), value_only_embedding (384 dims), and 11 discrete features (column_name, sample_values, cardinality, null_ratio, entropy, pattern_*, value_description, sibling_context) plus cosine similarities. SHAP values within each group are summed to produce a single importance score.

Disable SHAP with --no-shap for faster runs that skip the TreeSHAP computation.

Remaining Errors

Remaining errors cluster in confusable category pairs where values are structurally identical — for example, two hex identifier types or two address subtypes that share the same format. These are registered as confusable pairs in the DST frame so that mass flows to the pair rather than forcing an arbitrary leaf choice.

These pairs cannot be distinguished by value patterns alone — resolution requires richer context (table-level schema, data dictionary, or category consolidation).

Self-Training Mode

The --self-train flag activates self-training, where GT-labeled evaluation data is injected into the CatBoost training set. Off by default.

When Self-Training is Legitimate

The primary workflow for this pipeline is LLM annotation reproduction:

  1. An LLM provides initial column classifications (the “ground truth”)
  2. ML models learn to reproduce those LLM annotations transparently
  3. The ML pipeline provides explainability, reproducibility, and measured feature importance

In this workflow, self-training is not target leakage — the LLM annotations ARE the ground truth to learn. The ML pipeline’s goal is highly accurate recreation of the LLM-provided mappings.

Validated Results

Self-training (1 round, direct GT injection) achieves 99.4% (348/350) — only 2 residual errors, both in the Masked PAN category (one opaque annotation column confused with Security Question at 0.039 confidence, one data column confused with PAN India at 0.050 confidence). Training set: 7,684 synthetic + 212 reference + 348 GT-injected = 8,244 samples. Pipeline wall clock: 68 minutes (CatBoost CPU training dominates).

When Self-Training is Illegitimate

For benchmark evaluation (test/train splits measuring generalization to unseen data):

  • Do NOT use --self-train — this inflates accuracy metrics
  • The default mode (no self-training) maintains strict train/test separation
  • All published benchmark accuracy numbers use the default mode

Multi-Round Refinement

With --self-train-rounds N (N > 1), the pipeline iterates:

  1. Round 1: Inject all GT-labeled eval columns with their LLM labels
  2. Round 2+: Inject columns whose CatBoost confidence exceeds --self-train-threshold (default 0.80) using CatBoost’s predicted labels as pseudo-labels
  3. Repeat until no new columns qualify or round limit reached

Embedding encoding happens once before the loop — only CatBoost training repeats per round.

Commands

The --auto-generate flag inlines the synthetic data generator, producing a single command that generates training data, trains CatBoost, classifies, computes SHAP explanations, and writes the output parquet:

uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> \
    --taxonomy <taxonomy> --threshold 0.25 \
    --ground-truth <ground-truth.json> \
    --auto-generate --variants-per-category 50 \
    --output build/sigint_shap_eval.parquet

The output parquet includes all classification columns plus item-wise SHAP explanations (shap_top{1,2,3}_{name,value}), suitable for visualization with embedding-atlas.

Two-step workflow

For iterating on the synthetic generator separately:

# Step 1: Generate synthetic training data
uv run python scripts/generate_meta_tagging_train.py \
    --data-dir <data-dir> \
    --output-dir build/datasets/sigint_train/ \
    --variants-per-category 50

# Step 2: Train→eval pipeline
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> \
    --taxonomy <taxonomy> --threshold 0.25 \
    --ground-truth <ground-truth.json> \
    --train-dir build/datasets/sigint_train/ \
    --output build/sigint_embeddings.parquet

Self-training (LLM reproduction mode)

# Direct GT injection — learn the LLM's mapping
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> --taxonomy <taxonomy> \
    --ground-truth <ground-truth.json> \
    --auto-generate --self-train \
    --output build/sigint_self_train.parquet

# Multi-round refinement (3 rounds, 85% confidence threshold)
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> --taxonomy <taxonomy> \
    --ground-truth <ground-truth.json> \
    --auto-generate --self-train \
    --self-train-rounds 3 --self-train-threshold 0.85 \
    --output build/sigint_self_train_3r.parquet

Other commands

# Disable SHAP (faster, skips TreeSHAP computation)
uv run python scripts/build_sigint_embeddings.py \
    --data-dir <data-dir> --taxonomy <taxonomy> \
    --ground-truth <ground-truth.json> \
    --auto-generate --no-shap \
    --output build/sigint_eval.parquet

# Run tests
uv run pytest tests/sigint/test_generate_train.py -v

Heuristic Elucidation

Each classification dataset presents unique properties — naming conventions, column structure, value distributions — that affect classifier performance in dataset-specific ways. Rather than treating techniques as ad-hoc fixes, the pipeline follows a systematic methodology: observe a phenomenon, hypothesize a mechanism, implement it as a feature or technique, quantify its contribution with SAGE, and validate across benchmarks.

We call this cycle de novo heuristic elucidation: the disciplined reduction of observed dataset-specific phenomena to practice, with measured contribution to overall accuracy.

This page ties together the three classification architecture pages — Context Engineering, Classification Training, and Evidence Fusion — by documenting the methodology that produces the features, techniques, and evidence sources they describe.

The Methodology Cycle

Each step is concrete:

  1. Observe: Run the pipeline on a benchmark dataset. Examine accuracy by column kind (data vs. annotation), by category group, and by naming convention. Identify where accuracy drops below baseline expectations.

  2. Hypothesize: Formulate the failure mode as a testable phenomenon. “Annotation columns fail because the column name carries no semantic signal” is a hypothesis. “The training set has only 2 samples per class” is another.

  3. Implement: Reduce the hypothesis to a feature (added to the CatBoost feature vector) or a technique (added to the pipeline). Every implementation must be expressible as a SAGE-ablatable feature or a measurable pipeline stage.

  4. Quantify: Run SAGE to estimate the new feature’s Shapley value — its marginal contribution to accuracy across all columns. Features with near-zero SAGE importance are noise; features with high importance validate the hypothesis.

  5. Validate: Test the technique on a second benchmark with different properties. A technique that helps on the meta-tagging dataset but hurts on GitTables reveals a dataset-specific assumption that needs gating.

Heuristic Catalog

Seven heuristics have been discovered through this cycle. Each row links to the architecture page where the technique is described in detail.

#HeuristicObservationImplementationGeneralizes?
1Dual embeddingAnnotation columns have opaque names; the full embedding encodes a misleading name signalSecond embedding with name, table, and siblings stripped (value-only); concatenated as 384 additional dimsYes
2Category reference augmentationMany classes with few real samples each; CatBoost cannot learn class boundariesInject taxonomy reference embeddings as anchor training pointsYes
3Cosine similarity featuresCosine similarity to category references is a strong zero-shot signal on semantic namesEncode per-category cosine similarities as CatBoost input features, bridging zero-shot and trained classifiersYes
4Column propagationStructurally related columns share the same category; one may have a confident prediction while the other does notPost-prediction pass: propagate confident predictions to uncertain structurally-related columnsNo — dataset-specific
5Value descriptionGeneric names (col0, field_1) have no semantic content; cosine similarity on these is randomSubstitute NL descriptions of value patterns when the column name is generic (12th SAGE feature)Yes
6Discrete feature scaling12 discrete features are ignored by gradient boosting when 384-dim embedding vectors dominate split gainScale discrete features by \(\sqrt{384/12}\) so magnitudes competeYes
7Synthetic data generationEvaluation data generation process is known or inferable; reverse-engineering it produces unlimited training data70+ value generators covering all SIGDG categories, with 50/50 semantic/opaque name splitPartially

Generalization Status

Heuristics 1-3, 5-6 are universally applicable — they address structural properties (class imbalance, feature scale mismatch, uninformative names) that arise in any column classification task.

Heuristic 4 (column propagation) is dataset-specific: it exploits a structural property where columns are related and share category assignments. Other datasets may have different column relationships that warrant different propagation heuristics — but the methodology of exploiting structural relationships is universal.

Heuristic 7 (synthetic data generation) applies when the evaluation data’s generation process is known or inferable. For production data with unknown provenance, the role of synthetic generation shifts to LLM bootstrapping — the LLM classifies a sample of real columns to produce training signal.

Cross-Benchmark Validation

Two benchmarks with complementary properties test whether heuristics generalize:

PropertySIGDG evaluation setGitTables CTA benchmark
Column namesMixed semantic (payment_card_number) and opaque (col_42)100% generic (col0, col1, empty)
TaxonomySIGDG (42 categories, 30 leaves, BFO-grounded)BFO-grounded (122 types mapped from DBpedia)
SourceSynthetic + real enterprise dataAcademic benchmark (SemTab 2021)

Method Accuracy by Benchmark

MethodSIGDG (semantic)SIGDG (opaque)SIGDG (overall)GitTables
Cosine (zero-shot)99.4%8.0%53.7%1.6%
CatBoost (standalone)66.3%29.7%48.0%81.6%
DST fusion———71.4%
Union ceiling——66.0%—

The cross-benchmark comparison reveals three regimes:

  1. Cosine dominates (semantic names): Semantic column names directly match category labels. Cosine similarity achieves 99.4% accuracy. CatBoost adds noise.

  2. CatBoost dominates (generic/opaque names): Column names are uninformative. CatBoost trained on value patterns achieves 81.6% on GitTables. Cosine adds pure conflict (K > 0.5 on 100% of GitTables columns).

  3. Methods are complementary (mixed names): The union of correct answers reaches 66.0% — 12 points above either method alone. A well-tuned DST fusion captures both.

Confidence-Gated Fusion

The regimes map to a principled DST integration rule: use cosine evidence’s own confidence to decide how to weight it against CatBoost.

Cosine ConfidenceRegimeObserved Behavior
> 0.35High — cosine is reliableCosine near-certain; CatBoost adds marginal value
0.05 - 0.35Medium — both contributeStandard DST combination is appropriate
< 0.05Low — cosine has no signalCosine creates destructive conflict; CatBoost should dominate

This regime analysis motivates a planned confidence-gated adaptive discounting refinement (tracked in R-02). The current implementation uses fixed discounts, but the empirical observation is that cosine and CatBoost, despite sharing the embedding space, contribute discriminatively in different regimes — their functional orthogonality mitigates (but does not eliminate) the source independence concern (R-01).

The DST conflict metric \(K\) provides the runtime diagnostic: when \(K > 0.5\), the sources disagree enough that one should be discounted. On GitTables, 100% of columns have \(K > 0.5\) — a clear signal that cosine should be discounted. On meta-tagging data columns, \(K\) is moderate (mean 0.47) — the sources agree enough for standard combination.

Agentic Integration

The heuristic elucidation cycle maps to an agentic workflow where frontier LLMs play the roles of observer, hypothesis generator, and training-signal bootstrapper:

Role of Each Component

LLM as observer: Given a new dataset, the LLM examines column names, value distributions, table structure, and identifies phenomena. For example: “these columns have generic positional names — value patterns are the only discriminative signal.”

LLM as bootstrapper: The LLM classifies a sample of columns to generate training signal for CatBoost, replacing manual ground-truth labeling. This is the target design for production deployment — the LLM runs once to create training data, then the transparent CatBoost+DST pipeline handles ongoing classification without LLM dependency.

SAGE as quality gate: After implementing a new feature, SAGE automatically measures whether it improved accuracy. Features with near-zero Shapley values are noise — they should be discarded regardless of how plausible the hypothesis seemed. This prevents accumulation of heuristics that seemed helpful on one dataset but don’t generalize.

DST conflict as disagreement detector: When a new evidence source conflicts with existing sources (\(K > 0.5\)), the conflict metric flags it for review. High conflict doesn’t necessarily mean the new source is wrong — it may mean the confidence gate needs adjustment. But it does mean the sources should not be naively combined.

Self-Improving Pipeline

The agentic workflow creates a self-improving classification pipeline: each new dataset encounter produces new heuristic candidates, which are automatically validated by SAGE and incorporated (or rejected) based on measured contribution. The LLM’s role shifts from classifier to methodology driver — it doesn’t classify columns directly in production, but it discovers the features and training signal that make CatBoost+DST effective.

Relationship to Research Roadmap

Several items on the research roadmap are consequences of the heuristic elucidation methodology:

Research ItemConnection
R-01: Source IndependenceConfidence-gated fusion is the principled response to shared embedding space between cosine and CatBoost
R-02: Calibration ExperimentCalibrating the 13 DST constants requires cross-benchmark data — heuristic validation produces this data
R-05: Cautious ClassificationWhen no heuristic resolves a leaf-level ambiguity, cautious classification returns the deepest confident hierarchy node
R-08: Confusable PairsThe 16 remaining errors (ADID/GUID, BAN/PAN, etc.) are phenomena that no current heuristic resolves — they define the next cycle

The research roadmap is not a static list but a backlog of unresolved observations from previous heuristic elucidation cycles. Each completed research item either produces a new heuristic or validates that a phenomenon is irreducible (e.g., ADID and GUID values are structurally identical — no embedding-based feature can distinguish them without external context).

Evidence Fusion

The evidence fusion layer replaces single-point confidence scores with belief intervals derived from Dempster-Shafer Theory (DST). Each evidence source — cosine similarity, CatBoost prediction, pattern detection, name matching, and SVM short-text classification — produces an independent mass function. These are combined via Dempster’s rule of combination to yield belief intervals \([Bel(A), Pl(A)]\) at every level of the category hierarchy.

The key insight: a flat confidence of 0.85 conflates “definitely Payment Card Number” with “definitely some kind of Payment Information but unsure which sub-type.” Belief intervals expose this distinction. When \(Bel(\text{PaymentCardData}) = 0.6\) but \(Pl(\text{PaymentCardData}) = 0.95\), the gap signals that evidence supports the broader category but is ambiguous at the leaf level — a meaningful signal for downstream consumers and human reviewers.

Theoretical Foundation

Dempster-Shafer Theory

DST generalizes Bayesian probability by assigning mass not only to singleton hypotheses but to subsets of the frame of discernment \(\Theta\). A mass function \(m: 2^\Theta \to [0,1]\) satisfies \(m(\emptyset) = 0\) and \(\sum_{A \subseteq \Theta} m(A) = 1\). The belief function \(Bel(A) = \sum_{B \subseteq A} m(B)\) gives a lower bound on the evidence supporting \(A\), while the plausibility function \(Pl(A) = \sum_{B \cap A \neq \emptyset} m(B)\) gives an upper bound. The interval \([Bel(A), Pl(A)]\) represents epistemic uncertainty: what the evidence commits to versus what it does not rule out.

For decision-making, the pignistic probability transform [Smets & Kennes, 1994] redistributes mass from composite focal elements to singletons:

\[BetP(\{x\}) = \sum_{A \ni x} \frac{m(A)}{|A|}\]

This converts belief intervals into point probabilities suitable for classification decisions while preserving uncertainty information in the intervals themselves.

Dempster’s Rule of Combination

Given two independent mass functions \(m_1\) and \(m_2\), Dempster’s rule produces:

\[m_{1 \oplus 2}(A) = \frac{1}{1-K} \sum_{B \cap C = A} m_1(B) \cdot m_2(C)\]

where \(K = \sum_{B \cap C = \emptyset} m_1(B) \cdot m_2(C)\) measures conflict between sources. High conflict (\(K > 0.2\)) signals that sources disagree — a diagnostic the flat-confidence approach discards entirely.

Closed-World Assumption

The implementation enforces \(m(\emptyset) = 0\) (closed-world assumption), appropriate for a classification task with an exhaustive taxonomy. Mass on \(\Theta\) represents ignorance (“one of these categories, but I don’t know which”), not rejection. The Transferable Belief Model’s open-world extension [Smets, 1990] would allow \(m(\emptyset) > 0\) for unknown-category detection — a potential future direction.

Architecture

Restricted Focal Set

For a taxonomy with \(N\) leaves, the power set \(2^N\) is computationally intractable for any non-trivial taxonomy. The implementation uses a restricted focal set [Denoeux, 2008] containing only semantically meaningful subsets:

Element TypeCount (SIGDG)Description
Singletons30One per leaf category
Internal nodes12Descendant leaf sets for each parent in the hierarchy
Confusable pairs~10Manually specified pairs from error analysis
\(\Theta\)1Full frame (total ignorance)
Total~53vs. \(2^{30} \approx 10^{9}\)

This exploits the category hierarchy: rather than tracking arbitrary subsets, focal elements correspond to nodes in the taxonomy tree. An internal node like IdentityInformation (0010) maps to the focal element \(\{0011, 0012, 0013\}\) — the set of its descendant leaves.

Complexity

Each Dempster combination computes pairwise intersections over focal elements. With \(F\) focal elements and \(S\) sources requiring \(S-1\) combinations:

\[\text{Cost per column} = (S-1) \times F^2\]

With 5 sources and \(F \approx 53\) (SIGDG taxonomy), this is ~11,200 operations per column — negligible compared to the ~50ms sentence-transformer encode step. Even for larger taxonomies with hundreds of focal elements, DST overhead remains sub-millisecond.

Mass Function Converters

Each evidence source implements a converter from raw signal to BeliefAssignment (mass function). All converters produce valid mass functions: non-negative masses summing to 1.0, with a fraction allocated to \(\Theta\) representing the source’s inherent uncertainty.

Cosine Similarity → Mass

cosine_to_mass(similarities, frame, discount=0.3)

Applies softmax to cosine similarities across all leaf categories, then discounts by allocating 30% of mass to \(\Theta\):

\[m(\{c_i\}) = \text{softmax}(s_i) \times (1 - d), \quad m(\Theta) = d\]

The softmax transform converts raw cosine similarities (which may be negative or arbitrarily scaled) into a probability distribution. The discount parameter \(d\) controls how much trust is placed in cosine similarity as a standalone signal.

CatBoost Probability → Mass

catboost_to_mass(proba, frame, virtual_ensembles_variance=None)

Maps predict_proba() output directly to singleton masses. When CatBoost’s virtual ensembles provide per-class variance estimates, high variance increases the discount (more mass to \(\Theta\)):

\[d = \min(0.5,\ 0.1 + \bar{\sigma}^2 \times 1.6)\]

This adaptive discounting means CatBoost contributes less evidence when its own uncertainty is high — a principled integration of aleatoric uncertainty from the model into the epistemic uncertainty framework.

Pattern Detection → Mass

pattern_to_mass(pattern_signals, frame, pattern_category_map=None)

Binary pattern signals (email regex, SSN regex, credit card regex, etc.) map to known category codes. When patterns are detected, mass is concentrated on matched categories with high confidence (0.9) because regex patterns have near-zero false positive rates:

PatternCategoryRationale
email_patternEmailAddress (0076)user@domain.tld is unambiguous
ssn_patternTaxIdentifier (0085)XXX-XX-XXXX format is distinctive
credit_card_patternPaymentCardData (0070)Luhn-valid 16-digit sequences
phone_patternPhoneNumber (0074)E.164 / national format detection
uuid_patternDeviceIdentifier (0013)RFC 4122 hex format
ipv4_patternConfigurationData (0041)Dotted quad notation
url_patternConfigurationData (0041)HTTP(S) URL format

When no patterns are detected, the function returns a vacuous mass function (all mass on \(\Theta\)), contributing no evidence rather than misleading evidence.

Name Match → Mass

name_match_to_mass(column_name, frame, category_set)

Replaces the previous additive boost heuristic with a proper evidence source. Three matching tiers:

Match LevelMass on SingletonMass on \(\Theta\)Example
Exact0.700.30"tax identifier" = "TaxIdentifier"
Abbreviation0.500.50"ssn" = "SSN" (abbrev)
Word overlap0.300.70"customer email address" ⊃ "email address"
No match0.001.00Vacuous — no evidence contributed

This formalization means name matching no longer inflates confidence scores. Instead, its evidence is combined with other sources via Dempster’s rule, where agreement reinforces and disagreement raises the conflict diagnostic.

SVM Short-Text Classification → Mass

svm_to_mass(proba, frame, discount=0.20)

Converts calibrated probability estimates from a TF-IDF + LinearSVC classifier into a mass function. The SVM operates on sparse lexical features — character n-grams (3–6) and word bigrams — making it architecturally independent from the dense sentence-transformer embedding shared by the cosine and CatBoost sources. This directly addresses the source independence concern (R-01): correlation between TF-IDF features and dense embeddings is measurably lower than the correlation between two dense-embedding-derived sources.

The classifier uses scikit-learn’s CalibratedClassifierCV (Platt scaling) to produce well-calibrated probability estimates from the LinearSVC decision function. These probabilities map directly to singleton masses:

\[m(\{c_i\}) = p_i \times (1 - d), \quad m(\Theta) = d\]

The default discount (\(d = 0.20\)) is lower than cosine (0.30) because Platt-scaled SVM probabilities tend to be well-concentrated on the correct class for short-text classification. Like other sources, when the SVM has no meaningful prediction, it can be omitted entirely — classify() accepts svm_proba as an optional parameter, and the combination proceeds with only the non-vacuous sources.

The SVM source is always active when training data is available. The main pipeline (build_sigint_embeddings.py) trains SVM inline on synthetic data before the classification loop, making it the default 5th evidence source. Standalone accuracy: 84.6% on the meta-tagging evaluation set. A validation script (scripts/svm_pilot.py) measures source correlation and the marginal impact on fused belief intervals and conflict \(K\).

Training/evaluation text format. The SVM is trained on short text (name | type | values) and must receive the same format at evaluation time. The EmbeddingClassifier._build_svm_text() method ensures format consistency between training and inference, regardless of the full embedding text used by other evidence sources.

PropertySVM (5th source)Cosine (1st)CatBoost (2nd)
Feature spaceSparse TF-IDF (char + word n-grams)Dense embedding (384-dim)Dense embedding + discrete features
Inductive biasMaximum-margin separationNearest-neighbor similarityGradient-boosted decision boundaries
Independence from cosineHigh (different feature space)—Low (shared embedding)
CalibrationPlatt scaling (CalibratedClassifierCV)Softmax + fixed discountVariance-adaptive discount
ExplainabilityLinear feature weightsEmbedding similarity scoresSAGE Shapley values

Cross-Benchmark Analysis: Cosine Reliability Regimes

Cross-benchmark experiments reveal that cosine similarity evidence is not uniformly helpful — it ranges from near-perfect (99.4% on semantically named columns) to destructive (1.6% on generic names, where it adds pure conflict to CatBoost’s 81.6% accuracy).

Empirical Evidence

On GitTables (2517 columns, all generic names): CatBoost standalone achieves 81.6%, but DST fusion with cosine drops to 71.4%. Mean Dempster conflict \(K = 0.65\), with 100% of columns exceeding \(K > 0.5\). Cosine evidence is near-random and creates systematic conflict.

On the SIGDG evaluation set (mixed semantic and opaque names): cosine achieves 99.4% on semantically named columns but only 8.0% on opaque-name columns. CatBoost adds value precisely where cosine fails (29.7% on opaque columns). The union ceiling of both methods reaches 66.0% — 12 points above either alone.

Three Regimes

Cosine ConfidenceRegimeObserved Behavior
> 0.35High — cosine is reliableCosine near-certain; CatBoost adds marginal value
0.05 - 0.35Medium — both sources contributeStandard Dempster combination is appropriate
< 0.05Low — cosine has no signalCosine creates destructive conflict; CatBoost should dominate

The current implementation uses fixed discounts (0.3 for cosine, 0.15 for CatBoost). The regime analysis motivates a planned refinement: confidence-gated adaptive discounting, where the cosine discount increases when cosine confidence is low and decreases when it is high. This is tracked as part of the calibration experiment (R-02) in the research roadmap.

The regime structure also informs the source independence analysis — cosine and CatBoost share the embedding space, but in different regimes their contributions are nearly orthogonal: cosine dominates on semantic names (where CatBoost is redundant) and CatBoost dominates on opaque names (where cosine is noise).

See Heuristic Elucidation for the full cross-benchmark analysis.

Hierarchical Classification Output

classify_dst() returns a HierarchicalClassification that provides belief intervals at every level of the taxonomy:

result = classifier.classify_dst(sample, frame, category_set)

# Leaf-level belief interval
bel, pl = result.interval_at("0085")   # TaxIdentifier
# → (0.72, 0.91) — strong evidence, moderate uncertainty

# Parent-level belief interval
bel, pl = result.interval_at("0011")   # GovernmentIdentifier
# → (0.72, 0.95) — higher plausibility at parent level

# Root-level belief interval
bel, pl = result.interval_at("0010")   # IdentityInformation
# → (0.72, 0.98) — near-certain at this level

# Diagnostics
result.uncertainty_gap    # Pl - Bel for predicted leaf
result.conflict           # Dempster conflict K
result.needs_clarification  # gap > 0.3 or K > 0.2

Evidence String Format

When DST is active, the evidence string encodes source contributions and the belief interval for the predicted category:

dst(cosine=0.621, catboost=0.834, patterns=0.900) → TaxIdentifier [Bel=0.72, Pl=0.91, K=0.05]

Parquet Output Columns

The pipeline always includes these DST columns in the parquet output:

ColumnTypeDescription
dst_belieffloat64\(Bel\) for predicted leaf singleton
dst_plausibilityfloat64\(Pl\) for predicted leaf singleton
dst_uncertainty_gapfloat64\(Pl - Bel\): width of the belief interval
dst_conflictfloat64Dempster conflict \(K\) across sources
dst_needs_clarificationbool\(\text{gap} > 0.3\) or \(K > 0.2\)
dst_evidence_sourcesstringJSON: per-source mass summaries
dst_belief_pathstringJSON: belief intervals at each hierarchy level

Relationship to Prior Pipeline

The classify() method is unchanged — existing callers see no difference. classify_dst() is a parallel path that reuses the same embedding infrastructure but wraps evidence in mass functions before combining:

Implementation

FilePurpose
src/sigint/belief.pyFocalElement, BeliefAssignment, dempster_combine(), FrameOfDiscernment
src/sigint/mass_functions.pycosine_to_mass(), catboost_to_mass(), pattern_to_mass(), name_match_to_mass(), svm_to_mass()
src/sigint/svm_classifier.pySVMClassifier: TF-IDF + LinearSVC + CalibratedClassifierCV pipeline
src/sigint/classifier.pyHierarchicalClassification with belief methods and from_combined_evidence()
src/sigint/embedding_classifier.pyclassify() orchestration (up to 5 sources)
src/sigint/category_set.pyHierarchicalCategorySet with tree navigation

Test Coverage

86 tests cover the DST layer (400 total across the sigint package):

Test FileCountScope
test_belief.py26Mass functions, Bel/Pl computation, Dempster combination, frame construction
test_mass_functions.py20All 5 converters: high-confidence, uniform, vacuous, discount, edge cases
test_svm_classifier.py9SVM train/predict, save/load, config, 5-source DST integration
test_hierarchical_category_set.py16Tree navigation, backward compatibility, factory functions
test_hierarchical_classification.py15Belief methods, uncertainty diagnostics, classify_dst integration

Combination Rule Choice & Limitations

Why Classical Dempster

The implementation uses Dempster’s conjunctive rule with normalization (the orthogonal sum) for several engineering reasons:

  1. Associativity and commutativity. The combination order does not affect the result. Sources can be added or removed without restructuring the pipeline. combine_multiple() applies left-to-right sequential combination, but the final mass function is order-independent.
  2. Traceability. Every combined mass can be decomposed back to source contributions. The evidence string records per-source mass summaries alongside \([Bel, Pl, K]\), making audit straightforward.
  3. Conflict as diagnostic. Dempster’s \(K\) is not a flaw — it is a feature. High conflict between sources is a meaningful signal that something is wrong (ambiguous column, mismatched evidence). The needs_clarification flag (\(K > 0.2\) or gap \(> 0.3\)) routes uncertain columns to human review rather than suppressing disagreement.
  4. Restricted focal set efficiency. With ~53 focal elements and 3 pairwise combinations, the \(O(F^2)\) cost per column is negligible. More complex rules (e.g., PCR6) would add implementation complexity without proportional benefit at this scale.

Known Limitations

High-conflict sensitivity (Zadeh’s paradox). Classical Dempster with normalization concentrates mass on the intersection even when conflict is extreme. Zadeh (1986) demonstrated that when two sources assign mass to disjoint singletons, the normalized result assigns all mass to a third category with negligible original support. In our setting, this scenario is mitigated by three factors: (a) all four sources operate over the same embedding-derived information, making fully disjoint evidence unlikely in practice; (b) pattern and name-match sources return vacuous mass functions when they have no signal, contributing zero conflict rather than misleading evidence; (c) the \(K > 0.2\) flag identifies problematic cases for human review. However, no formal fallback rule is triggered — this is a passive diagnostic, not an active conflict resolution mechanism.

Source independence. Dempster’s rule assumes independent evidence sources. Cosine similarity and CatBoost both consume the same sentence-transformer embedding vector — they are not independent. Name matching partially overlaps with cosine (column name is part of embedding text). The cross-benchmark regime analysis suggests their contributions are functionally orthogonal in most cases (cosine dominates on semantic names, CatBoost on opaque names), but this is an empirical observation, not a theoretical guarantee. The optional SVM source directly addresses this concern: it operates on sparse TF-IDF features (character and word n-grams) with no dependency on the sentence-transformer embedding, providing a genuinely independent evidence channel. Additional options under consideration include merging cosine and CatBoost into a single source, or adopting a cautious rule that does not require independence (Denoeux, 2008). See R-01: Source Independence Analysis.

Heuristic discounting. The fixed discount constants (cosine: 0.30, CatBoost: 0.15, pattern: 0.10, name match: 0.30/0.50/0.70) were set by engineering judgment. No sensitivity analysis, grid search, or Bayesian optimization has been published. CatBoost’s variance-adaptive discount is the one exception — it uses virtual ensemble variance to modulate trust. A systematic calibration experiment (R-02) is planned: sweep constants against held-out data, measure accuracy and Expected Calibration Error (ECE), and extract optimized values with confidence intervals.

Hierarchical propagation. Belief and plausibility at internal taxonomy nodes are computed by standard summation over subsets (\(Bel(A) = \sum_{B \subseteq A} m(B)\)), which is correct for arbitrary focal sets. The restricted focal set — singletons, internal nodes, confusable pairs, \(\Theta\) — means these sums involve only the pre-computed focal elements that are subsets of a given node’s descendant set. This is not optimized via dedicated hierarchical algorithms (e.g., Shafer-Logan or evidential network propagation). For the current focal set size (~53 elements), brute-force summation is adequate. Larger taxonomies or dense confusable-pair sets would benefit from lattice-based optimization.

Closed-world assumption. The implementation enforces \(m(\emptyset) = 0\), appropriate for classification against an exhaustive taxonomy. Mass on \(\Theta\) represents “one of these categories, but I don’t know which” — not “none of the above.” The Transferable Belief Model’s open-world extension (Smets, 1990) would allow \(m(\emptyset) > 0\) for unknown-category detection. This is a potential extension for schema-drift scenarios where new column types appear that do not map to any SIGDG category.

Decision mapping. The pignistic transform (BetP) always selects a leaf singleton for the classification decision. When evidence supports a parent node but is ambiguous among its children, the classifier still commits to a specific leaf — even when \(Bel(\text{leaf}) \ll Bel(\text{parent})\). A cautious classification approach following Denoeux & Zouhal (2001) would return the deepest hierarchy node where \(Bel(A) > \tau\), preserving the hierarchy’s value. See R-05: Cautious Hierarchical Classification.

Alternative Rules Considered

RulePropertyTrade-off
Yager (1987)Conflict mass → \(\Theta\) instead of normalizationConservative; avoids Zadeh paradox but increases ignorance
Dubois-Prade (1988)Disjunctive: conflict mass → union \(A \cup B\)Preserves information but produces large focal elements
Murphy (2000)Average masses before single Dempster combinationSimple; handles high conflict but loses sequential diagnostics
Denoeux cautious (2008)Weight-based; does not require independenceTheoretically cleanest for our setting; more complex to implement
PCR6 (Smarandache-Dezert)Proportional conflict redistributionAddresses Zadeh paradox directly; computationally expensive

The modular architecture (belief.py + mass_functions.py) is designed to allow rule substitution — dempster_combine() can be swapped for any pairwise combination function with the same signature (BeliefAssignment, BeliefAssignment) → (BeliefAssignment, float). A planned branch will benchmark Murphy averaging and Yager’s rule against classical Dempster on high-\(K\) synthetic columns.

References

  • Dubois, D. & Prade, H. (1988). Representation and combination of uncertainty with belief functions and possibility measures. Computational Intelligence, 4(3), 244-264.
  • Denoeux, T. (2008). Conjunctive and disjunctive combination of belief functions induced by nondistinct bodies of evidence. Artificial Intelligence, 172(2-3), 234-264.
  • Denoeux, T. & Zouhal, L.M. (2001). Handling possibilistic labels in pattern classification using evidential reasoning. Fuzzy Sets and Systems, 122(3), 409-424.
  • Murphy, C.K. (2000). Combining belief functions when evidence conflicts. Decision Support Systems, 29(1), 1-9.
  • Sentz, K. & Ferson, S. (2002). Combination of Evidence in Dempster-Shafer Theory. Sandia National Laboratories, SAND2002-0835.
  • Shafer, G. (1976). A Mathematical Theory of Evidence. Princeton University Press.
  • Smarandache, F. & Dezert, J. (2005). Information fusion based on new proportional conflict redistribution rules. Proceedings of Fusion 2005.
  • Smets, P. (1990). The combination of evidence in the Transferable Belief Model. IEEE Transactions on Pattern Analysis and Machine Intelligence, 12(5), 447-458.
  • Smets, P. & Kennes, R. (1994). The Transferable Belief Model. Artificial Intelligence, 66(2), 191-234.
  • Yager, R.R. (1987). On the Dempster-Shafer framework and new combination rules. Information Sciences, 41(2), 93-137.
  • Zadeh, L.A. (1986). A simple view of the Dempster-Shafer theory of evidence and its implication for the rule of combination. AI Magazine, 7(2), 85-90.

Bootstrap Agent

The bootstrap agent is an LLM-driven pipeline for classifying novel databases when no ground truth exists. It uses DST conflict K as a convergence signal between LLM classifications and ML pipeline predictions, producing ground truth that feeds directly into the self-training pipeline.

Problem

When a novel database arrives — real tables with no codes in column names, no label columns, no pre-existing ground truth — the ML pipeline has no training signal. Manual classification of thousands of columns is infeasible. The bootstrap agent solves this cold-start problem by using an open-weights LLM as a zero-shot classifier, validating outputs through 5-source DST fusion, and iteratively refining disagreements until convergence.

Architecture

Three-Phase Architecture

Phase 1: LLM Sweep

The LLM classifies all columns in a single pass using table-aware batching. Each batch prompt includes:

  • Table name and discovered data elements for context
  • Column metadata: name, type, sample values, sibling columns, DE membership
  • Full taxonomy as a markdown table (code, label, description)
  • Instructions to return a JSON array with primary classification, confidence, evidence, and up to 3 alternatives

The alternatives field is critical — it feeds directly into llm_to_mass() where the primary prediction and alternatives are converted into a Dempster-Shafer mass function with calibrated uncertainty.

Batch mechanics:

ParameterValue
Columns per call50 (configurable)
Max tokens65,536 (required for reasoning models)
Temperature0.0 (deterministic)
Retry logic3 attempts, exponential backoff (2s → 4s → 8s) on 429/502/503/504
Truncation detectionfinish_reason == "length" triggers warning

Phase 2: ML Validation

After LLM labels are assigned, the ML pipeline runs as a validator. It produces independent evidence from 5 orthogonal sources and combines them via Dempster’s rule. The key output is conflict K — a rigorous measure of how much the sources disagree.

A column is flagged for revisit when both conditions hold:

  1. The LLM label differs from the ML prediction: llm_code != ml_code
  2. The DST conflict exceeds the threshold: K > k_threshold (default 0.2)

Phase 3: Targeted Revisit

When disagreements exist, the agent re-sends only contentious columns to the LLM with enriched context from the ML validation. The convergence loop iterates up to max_iterations (default 5) times with three exit conditions: all resolved, budget exhausted, or mean K below threshold.

Schema Discovery & Data Elements

Before the LLM sees any columns, the system performs structural analysis to discover Data Elements — composite semantic concepts that span columns across one or more tables.

Data Element Pattern Library

11 well-known patterns across 7 governance domains:

Data ElementDomainIndicatorsMin Match
PaymentCardfinancecard_number, pan, cvv, expiry, cardholder2
PostalAddresscontactstreet, city, state, zip, postal_code, country2
PersonNameidentityfirst_name, last_name, full_name, middle_name2
ContactInfocontactemail, phone, mobile, fax, telephone2
BankAccountfinanceaccount_number, routing_number, iban, swift2
GovernmentIDidentityssn, passport, driver_license, tax_id, ein2
BiospecimenCollectionhealthcaresample_location, specimen_type, collection_date2
PatientDemographicshealthcaredate_of_birth, gender, ethnicity, blood_type, mrn2
GeoLocationspatiallatitude, longitude, coordinates, altitude2
DeviceIdentitytechnologydevice_id, mac_address, ip_address, imei2
Credentialsecuritypassword, pin, secret, token, api_key2

Table-Aware Batching

When table_aware_batching = true (the default), columns are grouped by source table instead of flat slices. This ensures the LLM prompt includes a ## Table: payments header and lists discovered data elements, giving the model coherent schema context.

FK Hint Detection

Columns named <entity>_id (e.g., instrument_id) are matched against table names (instruments) with singular/plural handling, identifying cross-table relationships that inform data element membership.

Post-Classification DE Refinement

After bootstrap classification, a second pass discovers data elements that naming patterns missed — using category co-occurrence within tables. Known category combinations (e.g., PAN + Name + Address → PaymentCard) trigger new data element emissions with source="co-occurrence".

Label Propagation

After LLM labels, similar unlabeled columns receive the same label via embedding cosine similarity. This exploits the SAGE finding that column_name accounts for 53% of classification signal — columns with similar names and values likely share the same category.

Propagation rules:

  • Only from LLM-direct labels (no cascading)
  • Cosine similarity > 0.85 (configurable)
  • Skip if ML strongly disagrees (confidence > 0.5 and different code)

After all LLM phases complete, any remaining unlabeled columns with high ML confidence (>= confidence_floor) receive the ML prediction as their label. Every label carries a source_map entry tracking provenance: "llm", "llm_revisit", "propagated", or "ml".

LLM Revisit

When the LLM and ML disagree on a column AND K is high, the column is re-sent to the LLM with enriched context:

### Column 3: account_number (REVISIT)
Type: STRING | Values: ["1234567890", "9876543210"]
Siblings: [customer_name, routing_number, balance]
Data elements: [BankAccount]
ML prediction: BankAccountNumber [Bel=0.35, Pl=0.68, K=0.31]
Confusable: BankAccountNumber / PaymentAccountNumber
Your previous: PaymentAccountNumber (conf=0.65)

This gives the LLM three pieces of information it didn’t have before:

  1. ML prediction with belief interval — what the evidence sources think, with calibrated uncertainty
  2. Confusable pair — the specific known ambiguity between categories
  3. Its own previous answer — a reminder of what it said and at what confidence

LLM Backends

The agent supports three LLM backends via the OpenAI chat completions protocol:

BackendModelUse Case
cerebrasGLM-4.7 (358B MoE, open-weights)Default — fast inference, reproducible
anthropicClaude Opus 4.6Cloud deployment
openai_compatibleDevstral Small 2 / anyAir-gap (vLLM, Ollama, etc.)

All backends use the same batch prompt format and JSON response parsing. The OpenAI-compatible backend includes lenient parsing (regex fallback) for smaller models.

GLM-4.7 is the default because it is an open-weights model — critical for reproducibility and on-premises deployment in air-gapped environments. The same weights can run via cloud inference APIs or locally via vLLM/TGI, ensuring identical classification behavior regardless of deployment mode. Its reasoning capability (chain-of-thought) improves classification accuracy on ambiguous columns, though it consumes ~80-85% of output tokens on reasoning, requiring llm_max_tokens >= 65536.

LLM as DST Evidence Source

The LLM’s predictions become proper mass functions via llm_to_mass(), not overrides:

SourceDiscountIndependence
Cosine0.30Embedding similarity
CatBoost0.15Gradient boosting on features
Pattern0.10Regex detection
Name match0.30-0.70Lexical matching
SVM0.20TF-IDF sparse features
LLM0.10Full contextual reasoning

The low discount (0.10) reflects the LLM’s high reliability. DST conflict K between LLM and ML sources is a genuine disagreement signal.

Configuration

All bootstrap parameters flow through HOCON (config/base.conf):

KeyDefaultDescription
bootstrap.max_iterations5Max convergence loop iterations
bootstrap.k_threshold0.2DST K threshold for LLM revisit
bootstrap.uncertainty_gap_threshold0.3Pl - Bel gap threshold
bootstrap.coverage_target0.95Target label coverage
bootstrap.confidence_floor0.5Min ML confidence for fallback labels
bootstrap.initial_sample_fraction0.3Initial LLM sample size fraction
bootstrap.propagation_similarity0.85Cosine sim for label propagation
bootstrap.max_llm_calls_per_iteration500Per-iteration LLM call cap
bootstrap.max_total_llm_calls5000Total LLM call budget
bootstrap.columns_per_call50Batch size per LLM call
bootstrap.outputbuild/bootstrap_gt.jsonGround truth output path
bootstrap.llm_backendcerebrasLLM provider
bootstrap.llm_api_keynullAPI key (env var fallback)
bootstrap.llm_base_urlnullCustom base URL
bootstrap.llm_model(from llm.model)Model name
bootstrap.llm_max_tokens65536Max response tokens
bootstrap.llm_discount0.10LLM evidence discount in DST
bootstrap.table_aware_batchingtrueGroup columns by table

Integration with Self-Training

The bootstrap output feeds directly into the self-training pipeline:

# Step 1: Bootstrap — produce initial ground truth
uv run python scripts/bootstrap_classify.py \
    --data-dir ~/data/novel_tables/ \
    --data-elements \
    --output build/bootstrap_gt.json

# Step 2: Self-train — produce production model
uv run python scripts/build_sigint_embeddings.py \
    --data-dir ~/data/novel_tables/ \
    --ground-truth build/bootstrap_gt.json \
    --self-train --auto-generate \
    --output build/sigint_final.parquet

The --data-elements flag enables schema discovery and data element output alongside the ground truth JSON. The self-trained CatBoost model learns to reproduce LLM classifications with 99.4% fidelity — providing deterministic inference, per-item SHAP explanations, calibrated belief intervals, and zero marginal cost at scale.

Key Files

FilePurpose
src/sigint/bootstrap_agent.py3-phase agent: LLM sweep → ML validation → targeted revisit
src/sigint/llm_backend.pyLLM abstraction: Anthropic, OpenAI-compatible, Cerebras factory
src/sigint/schema_discovery.py11 DE patterns, FK hints, category co-occurrence refinement
src/sigint/data_element.pyDataElement, DataElementMember, DataElementCatalog
src/sigint/mass_functions.pyllm_to_mass() converter
scripts/bootstrap_classify.pyCLI entry point
config/base.confHOCON configuration (18 bootstrap keys)

Scenarios Overview

BDD scenarios (behave) cover the hub and the in-tree classification pipeline. Active domains have passing coverage; backlog domains document later work.

Active Domains

Classification Pipeline (Tier 0 — 40 scenarios)

Offline tests that validate the sigint classification engine without infrastructure dependencies:

FeatureFileScenariosDescription
Embedding Classificationclassification/embedding_classification.feature7Confidence thresholds, hierarchical output, name-boost
Evidence Fusionclassification/evidence_fusion.feature6DST mass combination, conflict detection, belief intervals
Feature Extractionclassification/feature_extraction.feature512 SAGE-ablatable features, pattern signals
Taxonomy Managementclassification/taxonomy_management.feature5SIGDG hierarchy, sensitivity mapping
Vocabulary Mappingclassification/vocabulary_mapping.feature4Custom term→category overrides
Config Lifecycleclassification/config_lifecycle.feature4HOCON config load, resolve, materialize, validate
GitTables Benchmarkclassification/benchmark_gittables.feature5BFO-grounded 122-type evaluation
Bespoke Datasetclassification/bespoke_dataset.feature4Custom dataset classification

Platform Health (Tier 1 — 22 scenarios)

Infrastructure health checks that validate each service in the devenv stack:

FeatureFileScenariosDescription
PostgreSQLplatform/health_postgres.feature3Connection, AGE + pg_cron + pg_trgm extensions
Kerberosplatform/health_kerberos.feature3KDC ticket granting, keytab validation
Kuduplatform/health_kudu.feature3Master API, tablet servers, table creation
Impalaplatform/health_impala.feature3SQL round-trip, DDL, data types
Atlasplatform/health_atlas.feature10REST API, bootstrap types, entity CRUD, classifications, FTS, suggestions, glossary, lineage

Integration & Tagging (Tier 1 — 12 scenarios)

Cross-component integration validating the Impala→Kudu→Atlas→sigint pipeline:

FeatureFileScenariosDescription
Catalog Syncplatform/integration_catalog_sync.feature4Impala DDL → Kudu, Atlas catalog bridge
Meta-Taggingplatform/integration_meta_tagging.feature6SIGDG type setup, column tagging, classification search
Pipeline Taggingtagging/pipeline_tagging.feature2Full Tagger pipeline: sample → classify → tag

Planned Enhancement

Data Lifecycle (Tier 1 — 5 scenarios, not yet passing)

FeatureFileScenariosDescription
Data Lifecycleplatform/data_lifecycle.feature5Hot→warm Kudu→Iceberg via CTAS, transparent queries

The hot→warm data lifecycle is the keystone capability of the stack: Kudu for upsert-heavy hot-tier ingest, Iceberg for warm-tier analytics, and Impala providing transparent queries across both tiers. See Roadmap.

Backlog Domains

The following scenario domains are archived in features_archive/ as future specifications. They document capabilities that will be revisited once the core platform (query stack, metadata governance, classification pipeline) is fully validated.

Feature Set A: Agent + Visualization (S01–S03)

ScenarioDescriptionPrerequisites
S01: Agent-Mediated VisualizationInteractive exploration through web terminal with resolution autoscalinggRPC engine, Dask, HoloViews
S02: Algorithm ExtensionPackage, deploy, and invoke custom analysis as platform extensionsgRPC engine, extension registry
S03: Agent Self-ImprovementDefine performance objectives, iterate toward quantitative improvementsgRPC engine, evaluation harness

Archived features: features_archive/agent/visualization.feature, extension.feature, evolution.feature

Feature Set B: Analytics (S04–S06)

ScenarioDescriptionPrerequisites
S04: OTel Root Cause AnalysisCorrelate degradation signals with infrastructure changesOTel collector, Dask analytics
S05: Cybersecurity InvestigationInvestigate malicious behavior using correlated log sourcesSecurity data pipeline
S06: Streaming OntologyOntology-grounded feature discovery from high-velocity streamsStreaming ingest, ontology service

Archived features: features_archive/analytics/otel_investigation.feature, cybersec_investigation.feature, streaming_ontology.feature

Also archived: features_archive/grpc_engine.feature — gRPC engine health and connectivity checks.

These scenarios remain valid specifications. When the gRPC engine, Dask compute layer, or streaming pipeline is implemented, the corresponding features can be moved from features_archive/ back to features/ and connected to step definitions.

Current Status

DomainScenariosStatus
Classification (tier-0)40All passing
Platform health (tier-1)22All passing
Integration + tagging (tier-1)12All passing
Active total7474/74 passing
Data lifecycle (planned)5Not yet passing
Backlog (S01–S06 + gRPC)~29Archived

See Test Infrastructure for the tier system, how to run scenarios, and the BDD framework architecture.

Test Infrastructure

BDD scenarios are implemented with the behave framework. Tests are organized into tiers based on what they validate. Tier-0 classification scenarios run offline (no infrastructure). Tier-1 integration scenarios require the full devenv stack (devenv up).

Test Tiers

TierScopeWhat It ValidatesInfrastructure
Tier 0Classification pipelineEmbedding classification, evidence fusion, feature extraction, taxonomy, config lifecycle, benchmarksNone (offline)
Tier 1Component health + integrationService health, Impala DDL → Kudu, Atlas catalog bridge, meta-tagging pipeline, end-to-end taggingFull devenv stack
Tier 2Engine workflowsgRPC engine, agent-mediated visualizationEngine binary (backlog)
Tier 3Full stackEnd-to-end with Dask/HoloViews visualization pipelineEngine + Dask + Datashader (backlog)

Running Tests

# ── Tier 0: Classification (no infrastructure needed) ──────────
uv run behave features/classification/ --no-capture

# ── Tier 1: Integration (requires devenv up) ───────────────────
uv run behave features/platform/ features/tagging/ --no-capture

# ── All tiers ──────────────────────────────────────────────────
uv run behave --no-capture

# ── By tag ─────────────────────────────────────────────────────
uv run behave --tags=@tier-0 --no-capture
uv run behave --tags=@tier-1 --no-capture
uv run behave --tags=@health --no-capture
uv run behave --tags=@integration --no-capture

# ── Specific feature ──────────────────────────────────────────
uv run behave features/platform/integration_meta_tagging.feature --no-capture
uv run behave features/classification/evidence_fusion.feature --no-capture

# ── Dry run (parse only) ─────────────────────────────────────
uv run behave --dry-run

# ── Air-gap mode (no external network calls) ─────────────────
HF_HUB_OFFLINE=1 uv run behave --no-capture

Air-Gap Testing

The devenv shell sets HF_HUB_OFFLINE=1 and preserves host HF_HOME / HF_HUB_CACHE / SENTENCE_TRANSFORMERS_HOME (lab RAID under /raid/cache/*), falling back to build/models only if no shared cache exists. Models must already be on the shared cache (or cached once via just cache-models):

just cache-models                        # Download model once
# or: devenv tasks run sigint:cache-models

# All subsequent runs are fully offline
uv run behave --no-capture               # Zero external network calls

This ensures BDD scenarios validate the same execution path used in air-gap deployments. See Air-Gap (Zarf) for packaging model artifacts into Zarf archives.

Feature Tags

Features use hierarchical tags for domain, tier, and scope:

@platform @integration @catalog-sync   # Domain tags
@tier-1                                 # Tier tag (controls which scenarios run)

Domain tags: @platform, @integration, @catalog-sync, @meta-tagging, @health, @classification, @tagging, @pipeline

Scenario Coverage

Tier 0 — Classification Pipeline (40 scenarios)

FeatureScenariosWhat It Tests
embedding_classification.feature7Embedding classifier: confidence thresholds, hierarchical output, name-boost, batch consistency
evidence_fusion.feature6DST evidence fusion: mass combination, conflict detection, belief intervals, needs-clarification
feature_extraction.feature512 SAGE-ablatable features: pattern signals, entropy, cardinality, value description
taxonomy_management.feature5SIGDG taxonomy: hierarchy loading, sensitivity mapping, category resolution
vocabulary_mapping.feature4Vocabulary mapping: custom term→category overrides, conflict resolution
config_lifecycle.feature4HOCON config lifecycle: load, resolve, materialize, validate
benchmark_gittables.feature5GitTables benchmark: BFO-grounded taxonomy, 122-type evaluation, accuracy metrics
bespoke_dataset.feature4Bespoke dataset classification: custom data, ground truth comparison

Tier 1 — Component Health (22 scenarios)

FeatureScenariosWhat It Tests
health_postgres.feature3PostgreSQL connection, AGE + pg_cron + pg_trgm extensions
health_kerberos.feature3KDC ticket granting, keytab validation, principal listing
health_kudu.feature3Kudu master API, tablet servers, table creation
health_impala.feature3Impala SQL round-trip, DDL, data types
health_atlas.feature10Atlas REST API, bootstrap types, entity CRUD, classifications, FTS, suggestions, glossary, lineage

Tier 1 — Cross-Component Integration (12 scenarios)

FeatureScenariosWhat It Tests
integration_catalog_sync.feature4Impala DDL → Kudu storage, Atlas catalog bridge, entity lifecycle
integration_meta_tagging.feature6SIGDG type setup, table/column tagging, classification search, Tagger dry-run
pipeline_tagging.feature2Full Tagger pipeline (live + dry-run) against Impala tables

Catalog sync scenarios:

  1. Impala CREATE TABLE registers in Kudu
  2. Impala CRUD operations work end-to-end on Kudu
  3. Atlas discovers Impala-managed Kudu tables (via catalog bridge)
  4. Atlas entity lifecycle follows Impala DDL (create → register → delete)

Meta-tagging scenarios: 5. Tagger creates SIGDG classification types in Atlas 6. Tagger classifies columns and applies tags (live mode) 7. Tagger classifies columns without writing (dry-run mode) 8. Apply classification to a specific column (column-level tagging) 9. Search for entities by classification 10. Tagger setup + dry-run pipeline validates without Atlas writes

Pipeline tagging scenarios: 11. Full Tagger pipeline: sample → classify → tag on a live Impala/Kudu table 12. Dry-run pipeline: classify without Atlas writes, verify report output

Planned — Data Lifecycle (not yet passing)

FeatureScenariosWhat It Tests
data_lifecycle.feature5Hot→warm Kudu→Iceberg via CTAS, transparent cross-tier queries

Total

TierScenariosFeatures
Tier 0 — Classification408 features
Tier 1 — Health225 health features
Tier 1 — Integration123 integration features
Total (passing)7416 features
Planned (data lifecycle)51 feature

Feature Organization

features/
├── environment.py                              # Stack health + cleanup hooks
├── conftest.py                                 # Pytest bridge (optional)
├── classification/                             # Tier-0: offline classification tests
│   ├── steps/
│   │   └── classification_steps.py             # All classification step definitions
│   ├── benchmark_gittables.feature
│   ├── bespoke_dataset.feature
│   ├── config_lifecycle.feature
│   ├── embedding_classification.feature
│   ├── evidence_fusion.feature
│   ├── feature_extraction.feature
│   ├── taxonomy_management.feature
│   └── vocabulary_mapping.feature
├── platform/                                   # Tier-1: health + integration
│   ├── steps/
│   │   ├── __init__.py                         # Cross-domain: imports tagging steps
│   │   ├── helpers.py                          # Connections, Atlas API, catalog bridge
│   │   ├── health_steps.py                     # Health check steps (idempotent)
│   │   └── integration_steps.py                # Catalog sync + meta-tagging steps
│   ├── health_atlas.feature
│   ├── health_impala.feature
│   ├── health_kerberos.feature
│   ├── health_kudu.feature
│   ├── health_postgres.feature
│   ├── integration_catalog_sync.feature
│   ├── integration_meta_tagging.feature
│   └── data_lifecycle.feature                  # Planned: Kudu→Iceberg lifecycle
├── tagging/                                    # Tier-1: end-to-end tagging pipeline
│   ├── steps/
│   │   ├── __init__.py                         # Cross-domain: imports integration steps
│   │   └── tagging_steps.py                    # Pipeline tagging steps
│   └── pipeline_tagging.feature
└── steps/
    └── steps.py                                # Re-exports for behave discovery

Cross-Domain Step Discovery

Behave discovers step definitions from the feature file’s parent steps/ directory. When a feature in features/platform/ uses steps defined in features/tagging/steps/, behave won’t find them automatically.

Each domain’s steps/__init__.py imports steps from sibling domains:

  • features/platform/steps/__init__.py → imports features.tagging.steps.tagging_steps
  • features/tagging/steps/__init__.py → imports features.platform.steps.integration_steps

This ensures steps are discoverable regardless of which domain’s feature file is being run.

Config-Driven BDD

All integration step helpers (features/platform/steps/helpers.py) load connection parameters from the HOCON config (config/base.conf) via sigint.config.load_config(). This ensures BDD tests use the same configuration as the application code — no hardcoded hosts, ports, or credentials that could drift.

from sigint.config import load_config

_CFG = load_config()          # HOCON base.conf + env var overrides

def impala_conn():
    return impala_connect(host=_CFG.impala_host, port=_CFG.impala_port, ...)

def atlas_api(path, method="GET", **kwargs):
    url = f"{_CFG.atlas_url}/api/atlas/v2{path}"
    auth = (_CFG.atlas_user, _CFG.atlas_password)
    ...

Catalog Bridge

The tier-1 integration tests use a Python catalog bridge function (register_impala_table_in_atlas in helpers.py) that registers Impala-managed Kudu tables in Atlas without requiring Kafka or the Atlas hook infrastructure:

  1. Runs DESCRIBE {table} on Impala to get column metadata
  2. Builds rdbms_* entities with temporary GUIDs (Atlas 2000-RDBMS model, Aegir-aligned)
  3. POSTs to Atlas POST /v2/entity/bulk — single atomic call
  4. Atlas resolves rdbms_table_columns COMPOSITION and assigns real GUIDs

This validates the Atlas entity contract for governed Kudu tables and enables classification and search tests without Java hook changes.

qualifiedName Convention

Entity TypePatternExample
rdbms_instanceinstance@{cluster}instance@signals
rdbms_db{db}@{cluster}integration_test@signals
rdbms_table{db}.{table}@{cluster}integration_test.orders@signals
rdbms_column{db}.{table}.{col}@{cluster}integration_test.orders.email@signals

Product registration uses rdbms_* only (not hive_*). Physical Kudu names remain separate (kudu_table FDW option / impala::db.table). See Roadmap: Entity Type Evolution.

After-Scenario Cleanup

environment.py runs best-effort cleanup after each scenario:

  • Deletes Atlas entities created by bridge registration (columns first, then table)
  • Deletes Atlas entities tagged during classification tests
  • Drops Impala tables created during the scenario

Health check steps are idempotent — they verify resources exist rather than requiring a clean state. This allows health scenarios to pass on repeated runs without cleanup.

Unit Tests

In addition to BDD scenarios, the sigint package has comprehensive pytest coverage:

uv run pytest tests/sigint/ -v          # 363 tests

These test individual modules (features, belief, mass functions, config, classifier, etc.) in isolation. The BDD scenarios validate end-to-end behavior across the full stack.

S01: Agent-Mediated Visualization

Feature Set A — features/agent/visualization.feature

Intent

A user interacts with the agent through the web terminal. The agent directs HoloViews/Datashader/Dask visualizations so the user can explore data interactively with resolution autoscaling and persona-appropriate views.

Scenarios

User issues command via WASM terminal

Tier 2 @engine-required

The user connects to the web terminal and submits an instruction. The instruction is delivered to the engine via gRPC and the agent acknowledges the request.

This validates the basic instruction path: terminal → gRPC → engine → acknowledgement.

Agent recomputes visualization on interaction

Tier 3 @viz-required

Given an active HoloViews session, user interaction with a visualization component triggers Dask to recompute the view in parallel. Datashader rasterizes the updated result and the visualization streams to the web client.

Resolution autoscaling on zoom

Tier 3 @viz-required

When the user changes the zoom level on a Datashader-rendered view, the view recomputes at the new resolution. Detail increases as the viewport narrows — no raw data is transferred to the client.

Persona-appropriate view (Scenario Outline)

Tier 3 @viz-required

The agent adapts visualization conventions based on the selected persona. Each persona foregrounds relevant metrics:

PersonaEmphasis
Data ScientistStatistical distributions, feature correlations
Domain ResearcherDomain-specific metrics, temporal patterns
Quantitative AnalystNumerical precision, confidence intervals
Operational AnalystSystem health, throughput, latency
Business AnalystKPIs, trends, executive summaries

Capabilities Addressed

From the draft overview:

  • Single data product for temporal, spatial, and spectral analysis
  • Data resolution autoscaling with active recomputation
  • Single data product with visualization for multiple personas
  • Base line agent flows
  • MLOps for feature extraction, correlation, outlier detection, signals evaluation

S02: Algorithm Extension Lifecycle

Feature Set A — features/agent/extension.feature

Intent

An algorithm developer independently develops analysis code leveraging Dask, packages it as a platform extension, and deploys it so the agent can invoke it in distributed compute contexts.

Scenarios

Package algorithm as extension

Tier 0 (pure, no services required)

The developer has a Dask-based analysis module. They package it as a platform extension with valid metadata and a distributable archive. This validates the packaging format without requiring any running services.

Deploy extension to running platform

Tier 2 @engine-required

A packaged extension is deployed to the platform. The agent registers the new capability and it appears in the capability inventory.

Agent invokes extension in compute context

Tier 3 @engine-required @viz-required

The agent selects a deployed extension for an analysis task. The extension executes within the Dask distributed context and results are surfaced through HoloViews components.

Draft Scenario Mapping

From the draft overview (Scenario 02):

  • Algo dev identifies analysis needs → Package algorithm as extension
  • Independently develops code leveraging Dask or Ray → Package algorithm as extension
  • Packages and deploys as platform extension → Deploy extension to running platform
  • Loads, deploys, tests, and manages the extension → Deploy extension to running platform
  • Agent includes algorithm as optional capability → Agent invokes extension in compute context

S03: Agent Self-Improvement

Feature Set A — features/agent/evolution.feature

Intent

An evaluation developer defines high-level performance objectives with discrete outcomes. The agent engages in self-improvement cycles to satisfy quantitative improvements, producing enhanced workflow templates available for download and redeployment.

Scenarios

Define performance objective

Tier 2 @engine-required

The developer specifies a quantitative performance target. The agent receives the objective, establishes a baseline measurement, and begins an improvement iteration.

Agent completes improvement cycle

Tier 2 @engine-required

When an improvement iteration completes, quantitative results are recorded and the agent reports whether the target was met. This validates the feedback loop between objective definition and measured outcome.

Export enhanced workflow template

Tier 2 @engine-required

After a successful improvement cycle, the developer requests the enhanced workflow. A workflow template is generated and made available for download and redeployment.

Draft Scenario Mapping

From the draft overview (Scenario 03):

  • Eval dev identifies high-level performance objectives with discrete outcomes → Define performance objective
  • Agent engages in self-improvement to satisfy quantitative improvements → Agent completes improvement cycle
  • Enhanced agent workflow templates available for download and redeployment → Export enhanced workflow template
  • Optional direct integration with GitHub/GitLab SDLC → Future extension

S04: OTel Root Cause Analysis

Feature Set A — features/analytics/otel_investigation.feature

Intent

An OTel analyst performing RCA correlates observed degradation with telemetry signals and infrastructure changes, engaging the agent to capture evidence of correlation and compose multi-system views.

Scenarios

Correlate degradation with telemetry signals

Tier 3 @engine-required @viz-required

Given access to OTel telemetry data, the analyst identifies an observed performance degradation. The agent correlates it with infrastructure change events and presents a timeline of contributing factors.

Compose multi-system RCA view

Tier 3 @engine-required @viz-required

Given correlated signals from multiple systems, the analyst requests a root cause analysis view. The agent composes a multi-system visualization with evidence of correlation captured in the report.

Draft Scenario Mapping

From the draft overview (Scenario 04):

  • OTel analyst performing RCA report generation → Both scenarios
  • Correlate observed degradation with telemetry signals with infrastructure changes → Correlate degradation with telemetry signals
  • Engage agent to capture evidence of correlation and likely root cause → Compose multi-system RCA view
  • Compose multi-system views → Compose multi-system RCA view

S05: Cybersecurity Investigation

Feature Set A — features/analytics/cybersec_investigation.feature

Intent

A KQL/SPL cybersecurity analyst investigates malicious behavior on their network/systems. They leverage AWS and systems logs alongside existing point solutions to triage threats, capture symptoms and representative signals, and validate novel detection logic.

Scenarios

Triage with AWS and system logs

Tier 3 @engine-required @viz-required

Given access to AWS logs and system telemetry, the analyst describes suspicious network behavior. The agent queries relevant log sources and surfaces symptoms and representative signals.

Secondary correlative investigation

Tier 3 @engine-required @viz-required

After primary indicators of compromise are identified, the analyst requests secondary system correlation. The agent correlates with OTel traces and eBPF data, consults historical binary fingerprints, and illustrates environmental conditions.

Validate novel detection logic

Tier 3 @engine-required @viz-required

Given a proposed detection rule, the agent evaluates it against historical data. It confirms whether the rule produces actionable views and reports false positive rates.

Draft Scenario Mapping

From the draft overview (Scenario 05):

  • KQL/SPL analyst investigating malicious behavior → All scenarios
  • Leverage AWS and systems logs alongside existing point solutions → Triage with AWS and system logs
  • Provide insight into primary behavior through secondary correlative investigation → Secondary correlative investigation
  • Illustrate system/environment conditions in support of findings → Secondary correlative investigation
  • Confirm novel detection logic provides actionable views → Validate novel detection logic

S06: Streaming Ontology and Feature Discovery

Feature Set B — features/analytics/streaming_ontology.feature

Intent

A data scientist performing click stream analysis intends to inform persona-oriented strategy decisions. High-velocity wide-schema records yield ontology-grounded feature patterns. The analyst observes and directs feature growth via agent-mediated sketch-based algorithms, reviews proposed software changes, and validates persona-grounded visit behavior views.

Scenarios

Ingest wide-schema stream records

Tier 3 @engine-required @viz-required

A high-volume click stream source is configured. As records begin arriving, the agent identifies ontology-grounded feature patterns and pattern counts accumulate over the stream.

Human-in-the-loop feature direction

Tier 3 @engine-required @viz-required

As feature patterns are being discovered, the analyst observes and directs pattern growth via the agent. The agent adjusts sketch-based algorithms accordingly and the feature set reflects the analyst’s guidance.

Agent proposes software changes

Tier 3 @engine-required @viz-required

Once directed feature patterns have stabilized, the agent identifies implementation requirements and proposes source changes via the SDLC integration. The analyst can review the proposed changes before acceptance.

Validate persona-grounded visit behaviors

Tier 3 @engine-required @viz-required

After processing a stream accumulation, the analyst requests a persona-grounded behavior view. The sketch algorithm produces actionable visit behavior views differentiated by persona segment.

Draft Scenario Mapping

From the draft overview (Scenario 06):

  • Data Scientist performing click stream analysis → All scenarios
  • High volume/velocity wide-schema records yield ontology-grounded feature patterns → Ingest wide-schema stream records
  • Observe and direct feature pattern growth via agent-mediated sketch-based algorithms → Human-in-the-loop feature direction
  • Review proposed software changes (agent-driven SDLC) → Agent proposes software changes
  • Confirm sketch algo provides actionable views of persona-grounded visit behaviors → Validate persona-grounded visit behaviors

Infrastructure Overview

The infrastructure stack is layered: OpenTofu provisions cloud resources, Ansible configures them, Zarf packages for air-gap, Tilt enables dev iteration, and OPA policies provide guardrails.

Layers

LayerToolDirectoryPurpose
ProvisioningOpenTofuinfra/aws/tofu/AWS VPC, EC2, S3, Cloudflare Tunnel
ConfigurationAnsibleinfra/aws/ansible/RKE2 install, Dask, services, ingress
PackagingZarfzarf/Air-gap Kubernetes deployment
Dev IterationTilttilt/, TiltfileLive-reload development on RKE2/k3d
PolicyOPA/Conftestpolicy/IaC and K8s guardrails

Deployment Flow

For air-gap environments, the Ansible flow uses the zarf-deploy role instead of individual service roles.

Ansible Roles

RolePurpose
commonBase node setup: packages, kernel modules, sysctl, data volumes
rke2-serverControl plane installation with token propagation
rke2-agentWorker installation with automatic token fetch
daskHelm-based Dask operator + DaskCluster CRD
jupyterhubHelm-based JupyterHub with Dask integration
signals-enginegRPC engine deployment + service
cloudflare-tunnelcloudflared with config-driven ingress routing
zarf-deploy4-phase air-gap orchestrator (stage/init/deploy/verify)

Playbooks

PlaybookDescription
site.ymlFull stack: RKE2 + Dask + Engine + JupyterHub + Ingress
dask-only.ymlStandalone Dask operator deploy
airgap-deploy.ymlAir-gap deployment via Zarf
teardown.ymlRemove workloads, preserve RKE2 cluster
validate.ymlPost-deployment health checks

Task Reference

Local Cluster (k8s:*)

TaskDescription
k8s:provisionCreate k3d cluster
k8s:statusCheck cluster connectivity
k8s:deploy-daskDeploy Dask operator + cluster
k8s:deploy-jupyterDeploy JupyterHub
k8s:forwardPort-forward services
k8s:destroyDelete k3d cluster

AWS Cloud (aws:*)

TaskDescription
aws:provisionOpenTofu apply (VPC, EC2, S3)
aws:inventoryGenerate Ansible inventory from Tofu state
aws:deployFull stack: RKE2 + Dask + services
aws:verifyPost-deployment health checks
aws:sshSSH to bastion host
aws:destroyDestroy infrastructure (preserves S3)
aws:teardownFull teardown including S3

OpenTofu (AWS)

AWS infrastructure is provisioned with OpenTofu (open-source Terraform fork) in infra/aws/tofu/.

Resources

FileResources
vpc.tfVPC, public/private subnets, IGW, conditional NAT, S3 VPC endpoint
security.tfBastion SG (SSH from WARP CGNAT), RKE2 cluster SG
ec2.tfUbuntu 24.04 AMI, SSH key pair, bastion, control plane, workers
s3.tfEncrypted data bucket with public access blocked
outputs.tfBastion IP, node IPs, SSH key path, S3 bucket name

Network Architecture

Air-Gap Mode

Setting airgap_mode = true in terraform.tfvars:

  • Removes the NAT gateway (no outbound internet from private subnet)
  • S3 access preserved via VPC gateway endpoint
  • All software must be pre-staged via Zarf packages

Variables

Key variables in variables.tf:

VariableDefaultDescription
projectsignals-360Resource naming prefix
aws_regionus-east-1AWS region
control_plane_count1Number of control plane nodes
worker_count4Number of worker nodes
worker_instance_typer6i.xlargeWorker EC2 instance type
data_volume_size100Worker data volume (GB)
allowed_ssh_cidrs100.96.0.0/12WARP CGNAT range for SSH
airgap_modefalseEnable air-gap (no NAT)

Usage

cd infra/aws/tofu

# Initialize
tofu init

# Plan with policy check
tofu plan -out=plan.tfplan
conftest test plan.tfplan -p ../../policy/tofu/

# Apply
tofu apply plan.tfplan

# Outputs for Ansible inventory
tofu output -json

Ansible Roles

Configuration management for RKE2 clusters and application services lives in infra/aws/ansible/.

Directory Structure

infra/aws/ansible/
├── group_vars/all.yml        # Shared variables
├── inventory/
│   └── hosts.example         # Template (tofu-generated in practice)
├── playbooks/
│   ├── site.yml              # Full stack deployment
│   ├── dask-only.yml         # Standalone Dask
│   ├── airgap-deploy.yml     # Zarf air-gap
│   ├── teardown.yml          # Clean workloads
│   └── validate.yml          # Health checks
└── roles/
    ├── common/               # Base node setup
    ├── rke2-server/          # Control plane
    ├── rke2-agent/           # Workers
    ├── dask/                 # Dask operator + cluster
    ├── jupyterhub/           # JupyterHub
    ├── signals-engine/       # gRPC engine
    ├── cloudflare-tunnel/    # Ingress
    └── zarf-deploy/          # Air-gap orchestrator

Roles

common

Prepares Ubuntu 24.04 nodes for RKE2:

  • System packages (iptables, open-iscsi, nfs-common)
  • Kernel modules (br_netfilter, overlay)
  • Sysctl tuning for Dask workloads (vm.max_map_count, somaxconn)
  • Swap disabled, data volume formatted and mounted (workers)

rke2-server

Installs RKE2 control plane:

  • Downloads and runs official RKE2 installer
  • Configures Canal CNI, TLS SANs, disables default ingress
  • First server: waits for node token, propagates to all cluster members
  • Additional servers: joins existing cluster with token

rke2-agent

Installs RKE2 worker nodes:

  • Fetches node token from control plane automatically
  • Configures node labels (dask.org/node-type=worker, signals.io/role=compute)
  • Joins the cluster

dask

Deploys Dask via Helm:

  • Adds Dask Helm repository
  • Installs dask-kubernetes-operator
  • Deploys DaskCluster CRD with configurable worker replicas, memory limits, and spill-to-disk
  • Waits for scheduler pod to be ready

jupyterhub

Deploys JupyterHub via Helm:

  • Dummy authenticator for development (password: signals)
  • Singleuser image shared with Dask workers
  • ClusterIP service (accessed via Cloudflare Tunnel)

signals-engine

Deploys the gRPC engine:

  • Namespace, Deployment, and Service
  • gRPC health check probes (readiness + liveness)
  • Configurable resource limits

cloudflare-tunnel

Deploys cloudflared for Zero Trust ingress:

  • Validates tunnel token and ID (from OpenTofu outputs)
  • Creates credentials Secret and config ConfigMap
  • Routes: Dask dashboard, JupyterHub, Signals Engine
  • 2 replicas with health checks

zarf-deploy

4-phase air-gap deployment orchestrator:

  1. Stage — copy Zarf binary and packages to control plane
  2. Init — create PVs, initialize Zarf registry
  3. Deploy — deploy application package with variables
  4. Verify — check all components running

Usage

cd infra/aws/ansible

# Full stack deployment
ansible-playbook playbooks/site.yml

# Deploy only Dask
ansible-playbook playbooks/dask-only.yml

# Air-gap deployment
ansible-playbook playbooks/airgap-deploy.yml

# Teardown workloads (keep RKE2)
ansible-playbook playbooks/teardown.yml

# Post-deployment validation
ansible-playbook playbooks/validate.yml

Air-Gap (Zarf)

Zarf enables deployment to disconnected (air-gap) environments by packaging container images, Helm charts, and manifests into a single archive that can be transferred to isolated networks.

Package Definition

The Zarf package (zarf/zarf.yaml) defines the following components:

ComponentRequiredDescription
signals-imagesYesCore container images (Dask, operator, engine)
dask-operatorYesDask Kubernetes Operator Helm chart
dask-clusterYesDaskCluster CRD manifest
signals-engineNo (default: on)gRPC engine deployment
jupyterhubNo (default: on)JupyterHub Helm chart
ingressNo (default: on)Ingress resources

Variables

VariableDefaultDescription
DASK_WORKER_REPLICAS4Worker count
DASK_SPILL_DIR/tmp/dask-spillSpill-to-disk path
S3_ENDPOINT(empty)S3-compatible endpoint
S3_BUCKETsignals-360-dataData bucket name
S3_REGIONus-east-1AWS region

Workflow

Build the package

cd zarf

# Download Helm charts to charts/
helm pull dask/dask-kubernetes-operator --version 2024.1.0 -d charts/
helm pull jupyterhub/jupyterhub --version 4.0.0 -d charts/

# Create manifests
# ... (namespace.yaml, dask-cluster.yaml, engine.yaml, etc.)

# Build package
zarf package create --confirm

Deploy to air-gap cluster

Using Ansible:

ansible-playbook playbooks/airgap-deploy.yml

Or manually:

# Stage on control plane
scp zarf signals-360-amd64.tar.zst user@cp:/opt/zarf/

# Initialize (creates internal registry)
zarf init --components=git-server --confirm

# Deploy
zarf package deploy signals-360-amd64.tar.zst \
  --set DASK_WORKER_REPLICAS=4 \
  --set S3_BUCKET=signals-360-data \
  --confirm

Air-Gap Network Requirements

When airgap_mode = true in OpenTofu:

  • NAT gateway is removed — no outbound internet from private subnet
  • S3 access preserved via VPC gateway endpoint
  • Container images served from Zarf’s internal registry (port 31999)
  • Git repos served from Zarf’s internal Gitea instance
  • ML model artifacts served from local filesystem or S3 via VPC endpoint

ML Model Artifacts

The sigint classification pipeline uses a sentence-transformer model (all-MiniLM-L6-v2, ~80MB) for embedding classification. In air-gap environments, this model must be pre-cached — the pipeline cannot download from HuggingFace at runtime.

Model Cache Bootstrap

# Ensure MiniLM via HF_HOME / SENTENCE_TRANSFORMERS_HOME (RAID preferred)
just cache-models
# or: devenv tasks run sigint:cache-models

Lab hosts pin caches on RAID (HF_HOME, HF_HUB_CACHE, SENTENCE_TRANSFORMERS_HOME). just cache-models skips download when MiniLM is already there and does not copy into build/models/ (root disk is tight). Tree-local build/models is only a last-resort fallback when no shared cache is configured.

Runtime Isolation

The devenv shell configures air-gap isolation automatically:

VariableValuePurpose
HF_HUB_OFFLINE1Prevent HuggingFace Hub API calls
HF_HOMEe.g. /raid/cache/huggingfacePrimary HF cache root
HF_HUB_CACHEe.g. /raid/cache/rch/huggingfaceHub package cache (may differ from $HF_HOME/hub)
SENTENCE_TRANSFORMERS_HOMEe.g. /raid/cache/sentence-transformersST model blobs
SIGINT_EMBEDDING_CACHE_DIRdefaults to $SENTENCE_TRANSFORMERS_HOMEHOCON embedding.cache_dir → SentenceTransformer(cache_folder=...)

HOCON resolves embedding.cache_dir as: default → ${?SENTENCE_TRANSFORMERS_HOME} → ${?SIGINT_EMBEDDING_CACHE_DIR} (last set wins).

Packaging for Zarf

For fully disconnected deployments, the model cache can be included in Zarf packages:

  1. Container image bake-in: Copy build/models/ into the sigint container image at build time
  2. S3 staging: Upload build/models/ to S3, download via VPC endpoint during deployment
  3. Zarf data injection: Add as a Zarf files component for direct filesystem staging
# Example zarf.yaml component for model artifacts
components:
  - name: sigint-models
    required: true
    files:
      - source: build/models/
        target: /opt/sigint/models/

Set SENTENCE_TRANSFORMERS_HOME=/opt/sigint/models/ and HF_HUB_OFFLINE=1 in the container environment to complete the air-gap chain

signals-federation Zarf package

Dedicated air-gap control-plane package for federation sentinels on RKE2.

Pathzarf/federation/
Package namesignals-federation
Version0.1.0
Zarfv0.70.1
Specminifi_sentinels.md

Why dedicated

Knative / YuniKorn / MiNiFi lifecycle is not owned by cybersec-dask or the legacy zarf/zarf.yaml signals-360 scaffold. This package takes precedence: residual cybersec app namespaces can be removed with zarf/federation/scripts/teardown-precedence.sh. Cybersec later adopts these standards rather than the reverse.

Ships

  1. Knative Serving (CRDs → controllers, Kourier, KPA enable-scale-to-zero)
  2. YuniKorn 1.9.0 (resource-class queues; policy promoted from config/scheduler/federation-queues.yaml)
  3. MiNiFi C++ sentinel Knative Service (federation-signals/minifi-sentinel)
  4. DCGM exporter (federation-system/dcgm-exporter) — live GPU watts, no TSDB

Does not ship: host gRPC engines, Dask, Jupyter, Panel-Viz, Prometheus/ELK.

Images (linux/amd64 digests)

Zarf rejects multi-arch OCI indexes. Knative release digests in upstream YAML are indexes — pin platform digests in zarf.yaml and manifests (recorded in BOOTSTRAP_VERSIONS.txt). Rebuild:

cd zarf/federation
# re-resolve if versions change:
# podman manifest inspect <ref>  → digests[].platform.architecture=amd64
zarf package create . --confirm --output /raid/signals/zarf-build

Deploy

export KUBECONFIG="${KUBECONFIG:-$HOME/.kube/rke2.yaml}"
# optional clean slate for cybersec apps
./zarf/federation/scripts/teardown-precedence.sh

zarf package deploy \
  /raid/signals/zarf-build/zarf-package-signals-federation-amd64-0.1.0.tar.zst \
  --confirm

cd zarf/federation && python3 -m converge verify

Converge FSM

zarf/federation/converge/ — Layer A/B catalog with live kubectl detects:

TierInvariants
T0node Ready, zarf ns
T2Knative CRDs, controllers, scale-to-zero CM
T3YuniKorn Ready, federation queues
T4ksvc present, STZ posture
T5C2 path packaged, DCGM exporter (pull-only OTel yield)
cd zarf/federation && python3 -m converge list
python3 -m converge verify

Scale-to-zero

config-autoscaler sets enable-scale-to-zero=true. Sentinel ksvc uses min-scale: "0". Idle revisions should drop to 0 pods after the grace period; activator wakes them on request.

Multi-engine K8s coordination (soon)

RKE2 on the lab node is scarce shared compute. Multiple sibling repos (Aegir, Atelier, Gaius, cybersec, Hermes, …) currently reach the same cluster via independent Tilt / Helm / ad-hoc apply paths. That collides with federation ownership:

Failure modeExample observed
Redeploy thrashAegir Tilt (infra/tilt/) recreated aegir-metaflow after teardown-precedence.sh
Image-pull noiseMetaflow images rewritten by Zarf agent to missing registry digests
Queue bypassWorkloads not submitted under root.{aegir,atelier,gaius,signals,hermes}
Scheduler conflictDefault kube-scheduler vs YuniKorn admission for capacity

Direction (federation-owned):

  1. YuniKorn is the scheduler of record for all federation and engine workloads on this RKE2 instance — queues already declared in package values (root.default + per-engine leaves).
  2. No long-running out-of-band Tilt against shared namespaces without an explicit engine queue + resource request/limit that YK can enforce.
  3. Engine admission via sentinels / package surface — host gRPC engines stay off-cluster; anything that does land on K8s goes through signals-federation (or a later per-engine Zarf component) so teardown and converge FSM remain authoritative.
  4. Coordination API (TBD) — signals control plane (signals-ui + protocol) should expose queue/capacity and “who may deploy what” so sibling devenvs do not fight for the same node.

Until that lands: stop engine-local process-compose / Tilt that targets RKE2 before federation teardown or redeploy (devenv processes down in Aegir, etc.).

See also

Dev Iteration (Tilt)

Tilt enables rapid development iteration on Kubernetes by watching source files and live-syncing changes into running pods.

Strategy

The Tiltfile uses a two-tier approach:

TierTriggerActionSpeed
Live updateSource file changes (.rs, .py)Sync into running pod~3-5s
Full rebuildDockerfile or dependency changesPodman build + push~30-60s

Configuration

The Tiltfile at project root configures:

  • Image build: Custom build using tilt/build-and-push.sh (Podman-based)
  • K8s overlay: tilt/engine-dev.yaml overrides the production deployment with dev settings
  • Port forwards: gRPC engine on localhost:50051

Usage

# With RKE2
KUBECONFIG=~/.kube/rke2.yaml tilt up

# With k3d
KUBECONFIG=~/.kube/k3d.yaml tilt up

Tilt will:

  1. Build the signals-engine image using Podman
  2. Push to the local registry (127.0.0.1:31999)
  3. Apply the dev overlay to the cluster
  4. Watch for source changes and live-sync

Dev Overlay

The tilt/engine-dev.yaml configures the engine for development:

  • Single replica
  • RUST_LOG=debug for verbose logging
  • Relaxed resource limits (2Gi memory, 2 CPU)

Files

FilePurpose
TiltfileTilt configuration (image build, resources, port forwards)
tilt/engine-dev.yamlDev Kubernetes overlay for signals-engine
tilt/build-and-push.shPodman build and push script

Policy (OPA)

Open Policy Agent (OPA) with Conftest provides guardrails for infrastructure provisioning and Kubernetes operations. Policies are written in Rego and live in policy/.

Policy Structure

policy/
├── tofu/
│   ├── base.rego          # Common helpers
│   ├── provision.rego     # Create guardrails
│   └── destroy.rego       # Delete guardrails
└── k8s/
    └── base.rego          # Tool + cluster validation

OpenTofu Policies

Provision guardrails (provision.rego)

Deny rules (block apply):

  • All resources must have an Owner tag matching the developer’s email
  • All named resources must include the developer prefix (when set)

Warning rules (advisory):

  • Alerts on EC2 instance, VPC, S3 bucket, and security group creation

Destroy guardrails (destroy.rego)

Deny rules (block destroy):

  • Cannot destroy resources owned by a different developer (cross-developer isolation)

Warning rules (advisory):

  • Lists all resources being destroyed
  • Extra warning for S3 bucket deletion (data loss)

Common helpers (base.rego)

Utility functions shared across policies:

  • Resource change filtering (created, deleted, updated)
  • Developer isolation helpers (Owner tag, prefix validation)
  • Resource type filtering

Kubernetes Policies

Base validation (k8s/base.rego)

Deny rules:

  • kubectl must be available in PATH
  • helm must be available in PATH
  • KUBECONFIG must exist
  • Cluster must be reachable

Info rules:

  • Reports connected cluster context and node count

Usage

Validate OpenTofu plans

cd infra/aws/tofu

# Generate plan
tofu plan -out=plan.tfplan
tofu show -json plan.tfplan > plan.json

# Validate against provision policies
conftest test plan.json -p ../../../policy/tofu/ --namespace signals.tofu.provision

# Validate destroy plans
conftest test plan.json -p ../../../policy/tofu/ --namespace signals.tofu.destroy

Validate Kubernetes environment

# Generate environment snapshot
kubectl cluster-info dump --output-directory=/tmp/k8s-info
conftest test /tmp/k8s-info -p policy/k8s/

Developer Isolation

The policy system enforces developer isolation in shared AWS accounts:

  1. Set developer_email and developer_prefix in terraform.tfvars
  2. All resources are tagged with Owner = <developer_email>
  3. Provision policies enforce naming conventions
  4. Destroy policies prevent cross-developer resource deletion

Components Overview

Apache and sibling trees live as git submodules under components/. ASF forks track rch/devenv. Signals pins SHAs from that line.

ComponentPurpose
signals-protocolShared zndx.engine.v1 / scheduler.v1 (trunk)
AtlasGovernance + OpenLineage REST; AGE on :21010
MarquezOpenLineage UI; proxies Atlas /api/v1 (:21011)
RangerTag-based authz from Atlas classifications
KuduHot-tier columnar store
ImpalaSQL across Kudu and Iceberg
Impala FDWPostgreSQL → Impala HS2 / kudu_scan
IcebergWarm tier on RustFS
PolarisIceberg REST catalog (rch/asf-polaris)
YuniKornApplication admission and queues
AirflowCoordination Activities and Metaflow DAGs
MetaflowPlatform metadata + @kubernetes tasks
Hermes AgentAgent peer pin (zndx/oss-hermes-agent)
MiNiFi C++Sentinels (C2 + OTel) on Knative
Signals Control UIPrimary backplane, :9889 (weathership/signals-ui)
NiFiFlow routing (planned)
git clone --recurse-submodules git@github.com:weathership/signals.git
git submodule update --init --depth 1

impala_fdw is weathership/impala_fdw (trunk). Marquez is zndx/oss-marquez. signals-protocol is zndx/signals-protocol (trunk). Hermes is zndx/oss-hermes-agent. MiNiFi is weathership/oss-minifi-cpp.

C++ builds (Kudu, Impala) need cmake, ninja, gcc, protobuf, flatbuffers, cyrus_sasl, openssl — all in devenv. The FDW needs PostgreSQL 16 headers (pg_config).

signals-protocol

Shared wire contracts for the zndx signals federation. Vendored as a git submodule:

components/signals-protocol  →  git@github.com:zndx/signals-protocol.git  (trunk)

Role in Signals

Signals pins this repo so the product SoR and the fleet share one wire contract. Engines (Ægir, Atelier, Gaius) and external peers generate bindings from proto/; Signals publishes discovery for Atlas, OpenLineage, and Ranger and adopts OIP mapping for multi-agent model ops.

The protocol is not finished foundation — it grows as peer engines surface needs that belong on a shared path (additive within a version). Signals does not replace peer engine design; it holds the pin and platform attachment. See How the federation contract evolves.

See Signals protocol core.

Layout

PathContent
proto/zndx/engine/v1/engine.protoComplete, Status, Remediate
specification/protocol/engine_grpc.mdHuman spec + OIP mapping notes

Bump

git -C components/signals-protocol fetch origin
git -C components/signals-protocol checkout origin/trunk
git add components/signals-protocol
git commit -m "chore: bump signals-protocol"

Protocol changes land as PRs on zndx/signals-protocol (additive within a version), then submodule pin here — never fork a second proto tree in Signals.

Atlas

Apache Atlas is the governance and lineage catalog. Impala tables and columns register here; sigint writes SIGDG classifications; Ranger reads those tags. The same process serves OpenLineage REST for Marquez-web.

See OpenLineage + Atlas and Metadata Tagging.

Services

ServicePortPurpose
Atlas21010/api/atlas/* and OpenLineage /api/v1/*

The graph backend is Apache AGE on PostgreSQL 16 (signals database).

AGE Backend

The Atlas fork (rch/asf-atlas, rch/devenv line; AGE work may land here or as host config) includes a graph database backend that uses Apache AGE (A Graph Extension for PostgreSQL):

  • Module: components/atlas/graphdb/age/ — 26 Java files implementing all graphdb/api interfaces
  • Backend class: AtlasAgeGraphDatabase loaded via atlas.graphdb.backend config property
  • Graph queries: openCypher via AGE, stored in PostgreSQL

This eliminates the HBase + Solr dependencies from the standard Atlas deployment.

Impala / Kudu Integration

Impala-managed Kudu tables are registered in Atlas with the stock RDBMS model (addons/models/2000-RDBMS/) — same family as Aegir (rdbms_*). A Python catalog bridge reads table metadata from Impala via DESCRIBE and creates entities through the Atlas REST API. Hive entity types are not used for product registration.

Entity Types

Atlas Entity TypequalifiedName PatternSource
rdbms_instanceinstance@signalsLab cluster (rdbms_type=Kudu)
rdbms_db{db}@signalsCREATE DATABASE
rdbms_table{db}.{table}@signalsCREATE TABLE
rdbms_column{db}.{table}.{col}@signalsColumn definitions from Kudu schema

Relationships: rdbms_instance_databases → rdbms_db_tables → rdbms_table_columns (COMPOSITION). Atlas resolves these when entities are created via POST /v2/entity/bulk with temporary GUIDs. See Roadmap: Entity Type Evolution.

Catalog Bridge

The bridge function (register_impala_table_in_atlas in features/platform/steps/helpers.py, mirrored by sigint.atlas_client.AtlasClient.register_table) performs entity registration without Kafka or the Atlas hook infrastructure:

  1. DESCRIBE {table} on Impala → column names, types, comments
  2. Build rdbms_instance + rdbms_db (referred), rdbms_table (main), rdbms_column (referred) entities with negative temp GUIDs (data_type on columns, type=TABLE on tables)
  3. POST /v2/entity/bulk — single atomic call, idempotent via qualifiedName matching
  4. Extract real GUIDs from guidAssignments (create) or mutatedEntities (update)

Used by the tier-1 BDD integration tests. See Test Infrastructure.

Metadata Tagging Pipeline

Impala CREATE TABLE (Kudu)
    → Catalog Bridge (DESCRIBE → Atlas REST API)
        → Atlas (rdbms_table + rdbms_column entities)
            → Tagging Service (classifies against SIGDG ontology)
                → Atlas (SIGDG classifications applied)
                    → Ranger (tag-based policies enforced)

The tagging service reads new entities from Atlas and classifies table and column names against the SIGDG ontology (BFO-grounded data governance vocabulary). Classifications are written back as Atlas tags.

Classification Examples

Column NameSIGDG ClassSensitivity
customer_idSIGDG:0012 PlatformIdentifierInternal
emailSIGDG:0025 ContactInformationConfidential
ssnSIGDG:0011 GovernmentIdentifierRestricted
total_amountSIGDG:0021 FinancialInformationConfidential
order_statusSIGDG:0050 TransactionInformationInternal

BDD Validation

The tier-1 integration tests validate the full classification lifecycle:

  1. Catalog bridge — Impala table registered in Atlas with correct column metadata
  2. Classification CRUD — Create PII classification type, apply to tables and columns
  3. Column-level tagging — Apply classification to individual columns (e.g., email → PII)
  4. Entity lifecycle — Create, register, verify, delete entities through Impala DDL + Atlas API

Build

cd components/atlas
mkdir -p webapp/target/api/v2/apidocs/ui
mvn package -pl webapp -am -Dmaven.test.skip=true -DskipUTs=true \
  -DGRAPH-PROVIDER=age -Dcheckstyle.skip=true -DskipEnunciate=true \
  --no-transfer-progress

Runtime requires JDK 21 with --add-opens flags for HBase shaded JAXB classes.

Marquez

Submodule: components/marquez → git@github.com:zndx/oss-marquez.git (main).

Marquez-web is the OpenLineage UI. It proxies /api/v1 and /api/v2beta to Atlas. Lineage events, jobs, datasets, and graphs live in AGE on the Signals Postgres. Architecture: OpenLineage + Atlas.

Port convention

Marquez UI port = Atlas HTTP port + 1.

ServiceDefault portEnv override
Atlas21010SIGNALS_ATLAS_HTTP_PORT (process uses fixed -port 21010 today; keep in sync)
Marquez-web21011MARQUEZ_WEB_PORT (default ATLAS + 1)
OL proxy targetAtlas host/portSIGNALS_OL_API_HOST / SIGNALS_OL_API_PORT

:3000 is reserved for ad-hoc local frontend dev (Vite/CRA, Atelier, etc.) and must not be the lab Marquez bind.

LAN example (this project’s Atlas on the node IP):

  • Atlas: http://192.168.1.55:21010
  • Marquez UI: http://192.168.1.55:21011 → proxies /api/v1 + /api/v2beta → Atlas

Ops

devenv tasks run marquez:build-web   # also before devenv up
# UI → Atlas OL
# http://localhost:21011  (or http://<node>:21011)
# proxies to SIGNALS_OL_API_HOST:PORT (default 127.0.0.1:21010)

Acceptance (API completeness)

Marquez-web boot and navigation must not hit 404s on the Marquez API surface. Minimum probe against Atlas (not Marquez API):

for p in health tags jobs namespaces events/lineage stats/lineage-events search?q=test; do
  code=$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:21010/api/v1/$p")
  echo "$code  /api/v1/$p"
done
curl -sS -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:21010/api/v2beta/search/jobs?q=test'

Expect 200 (empty collections are fine). After seeding RunEvents via POST /api/v1/lineage, the UI jobs/datasets/events/lineage views should show content.

Ranger

Apache Ranger provides authorization and access control across the data platform. Ranger consumes Atlas classifications (SIGDG tags) and enforces tag-based policies at query time in Impala.

Role in the Stack

  1. Policy admin — define allow/deny/mask rules on resources and tags.
  2. TagSync — pull classifications from Atlas (http://localhost:21010) into Ranger’s tag store.
  3. Impala plugin — enforce policies when users query Kudu/Iceberg tables via Impala.

See Metadata Tagging for the Atlas → Ranger flow.

Local devenv (target)

ServicePort / endpointNotes
Ranger Admin6080SIMPLE auth for local dev
TagSyncbackground processREST source → Atlas :21010
Postgreslocalhost:5455/rangeradmin + audit schema

Kerberos (when enabled): realm DEV.VISTA.ZNDX.ORG, host tinybox.dev.vista.zndx.org (HTTP SPN for SPNEGO).

Status

Near-term / in progress — submodule at components/ranger (rch/asf-ranger, branch rch/devenv). Config scaffold under config/ranger/. Full admin + TagSync + Impala plugin wiring is part of the current stack plan.

Build (local, not Impala CDP package)

Impala’s toolchain can download a prebuilt CDP Ranger admin. Signals does not use that. We build components/ranger and point Impala at it:

git submodule update --init components/ranger
devenv tasks run ranger:build      # today: devenv jdk11 → .devenv/m2
devenv tasks run ranger:install    # .devenv/ranger/admin (not a system package)
devenv tasks run ranger:setup      # Postgres :5455/ranger + site.xml
devenv up -d                       # process ranger-admin → :6080

config/impala/impala-config-local.sh sets RANGER_VERSION_OVERRIDE + RANGER_HOME_OVERRIDE so bootstrap skips the CDP tarball and the FE resolves plugins from .devenv/m2.

JDK / Nashorn (interim)

Today: ranger:build pins devenv JDK 11 because parts of the current Ranger tree still rely on Nashorn (jdk.scripting.nashorn), which was removed from the JDK after 14. That is a local build constraint, not a product goal.

Direction: when appropriate, move the rch/asf-ranger / rch/devenv line forward to drop Nashorn and build/run on modern JDKs (align with host Java 21 / the rest of asf-signals). Until then, treat jdk11 as an explicit exception in devenv tasks—do not normalize “Ranger needs system JDK 11” as the long-term model.

Kudu

Apache Kudu is the hot-tier store: low-latency upserts and analytical scans on weekly range partitions. Iceberg on RustFS holds settled weeks. See Query Engine.

Role in the Stack

Kudu stores the primary (hot) copy of analytical tables. Tables are created via Impala’s STORED AS KUDU syntax and managed by Kudu’s master server. In HMS-free mode, table metadata is registered in a PostgreSQL catalog registry rather than the Hive Metastore.

Impala DDL → KuduCatalogOpExecutor → Kudu Master → Tablet Servers
                                   → PG catalog registry

Services

ServicePortPurpose
kudu-master7051 (RPC), 8051 (HTTP)Schema management, tablet locations
kudu-tserver7050 (RPC), 8050 (HTTP)Data storage, scan execution

Table Naming

Kudu tables created through Impala use the naming convention impala::<db>.<table>. This is stored as the kudu.table_name property in the metastore table object and in the PostgreSQL catalog registry.

Data Lifecycle

Tables begin in Kudu (hot tier) and can be migrated to Iceberg (warm tier) via CREATE TABLE ... AS SELECT through Impala:

-- Hot tier: Kudu (low-latency reads/writes)
CREATE TABLE db.events (
  event_id INT, ts BIGINT, data STRING,
  PRIMARY KEY(event_id)
) STORED AS KUDU;

-- Warm tier: migrate to Iceberg via Polaris
CREATE TABLE db.events_archive
  STORED AS ICEBERG
  TBLPROPERTIES('iceberg.catalog'='polaris')
AS SELECT * FROM db.events WHERE ts < 1710000000;

Build

Kudu requires both C++ binaries and a Java client:

  • C++ binaries: Built from components/kudu/ with cmake/ninja. The Impala toolchain includes a pre-built Kudu client for ABI compatibility.
  • Java client: kudu-client via devenv tasks run kudu:install-java into .devenv/m2 (project Maven repo), not ~/.m2.

Build dependencies: cmake, ninja, gcc, protobuf, flatbuffers, cyrus_sasl, openssl.

Impala

Apache Impala is the SQL engine across Kudu (hot) and Iceberg (warm). Applications reach it through PostgreSQL and impala_fdw.

Role in the Stack

Impala runs in HMS-free mode, eliminating the dependency on the Hive Metastore. DDL operations route through SignalsDdlExecutor to create tables directly in Kudu or via the Polaris REST catalog for Iceberg. Table metadata is loaded from Kudu master (not HMS) at query time.

See Query Engine & Catalog Stack for the full architecture.

Services

ServicePortPurpose
statestore24000Cluster membership, metadata broadcast
catalogd26000Metadata management, DDL execution
impalad21050 (HS2), 21001 (beeswax)Query execution, client connections

Web UIs: statestore :25010, catalogd :25020, impalad :25000.

Connecting

Product default (direction): impala_fdw from PostgreSQL — FDW-only / no-JDBC for apps and agents. HS2 remains the engine wire protocol behind the FDW and for cluster bring-up.

# Interim / ops: direct HS2 (not the long-term app contract)
jdbc:hive2://localhost:21050/default;auth=noSasl

# Prefer: Postgres + CREATE SERVER / FOREIGN TABLE via impala_fdw (see that page)
# impala-shell is broken with Python 3.12 (PY_SSIZE_T_CLEAN)

HMS-Free DDL

-- Databases
CREATE DATABASE sample_db;
DROP DATABASE sample_db;

-- Kudu tables
CREATE TABLE sample_db.events (
  id INT, name STRING, ts BIGINT,
  PRIMARY KEY(id)
) STORED AS KUDU
TBLPROPERTIES('kudu.master_addresses'='127.0.0.1:7051');

-- Iceberg tables (via Polaris — near-term)
CREATE TABLE sample_db.archive (
  id INT, name STRING
) STORED AS ICEBERG
TBLPROPERTIES('iceberg.catalog'='polaris');

Verified Operations

The following have been tested end-to-end in HMS-free mode:

  • CREATE/DROP DATABASE and TABLE
  • INSERT INTO (single and multi-row)
  • SELECT with WHERE, ORDER BY
  • Multi-table JOINs (3-way)
  • GROUP BY with COUNT, SUM, CAST
  • Subqueries with HAVING
  • Data types: INT, STRING, DOUBLE, BOOLEAN, BIGINT

Build

Prefer host tasks so Maven/Ranger isolation stays under .devenv/:

devenv tasks run impala:bootstrap   # toolchain (long first time)
devenv tasks run impala:build       # or scripts/impala-build-isolated.sh

Produces impalad, catalogd, statestored, admissiond under be/build/latest/service/.

Storage default: Kudu-only / no-HDFS

Future (and product) default: Impala is a SQL front end over Kudu (and later non-HDFS object/block stores). HDFS is not a storage tier we run or design for. There is no NameNode/DataNode/YARN in the signals service graph. Catalog is HMS-free (PostgreSQL registry). See Query Engine.

Longer-term object/block storage for the platform is moving toward rustfs and Ceph — not “more Hadoop.” Iceberg warm tier (when present) sits on object storage via modern catalogs (e.g. Polaris), not HDFS.

Build reality today (upstream Impala debt): the tree is still HDFS-first. CMake does find_package(HDFS REQUIRED) (libhdfs), packaging expects libhadoop.so, and the FE still compiles against Hadoop client jars. Bootstrap therefore materializes a Hadoop client/native tarball under toolchain/ even though we never run a Hadoop cluster for Kudu-only tables.

LayerStill wants HadoopProduct default
BE linklibhdfsShould not; link tax until no-HDFS default lands
Package checklibhadoop.soDistro packaging leftover
FE Mavenhadoop-hdfs / client APIsGate/remove for Kudu-only profile
Runtime servicesHDFS/YARNNone

Treat any Hadoop tarball as a temporary link-time SDK, not a product component. Do not document “running Hadoop” as part of devenv services.

Fork direction (rch/devenv): Kudu-only / no-HDFS is the intended default build and runtime profile — not an optional niche. Stub or drop HDFS BE I/O; fail closed on hdfs://; stop requiring libhdfs/libhadoop.so for daemon builds. Same class of interim debt as Ranger JDK 11/Nashorn.

Impala FDW

PostgreSQL foreign data wrapper for the Impala + Kudu data plane, maintained as components/impala_fdw → weathership/impala_fdw.

Full specification: components/impala_fdw/docs/SPEC.md (in-tree: components/impala_fdw/docs/SPEC.md after submodule init).

Scope

Hot storageKudu (STORED AS KUDU); kudu_scan via libkudu_client
Warm storageIceberg and Impala UNION ALL views via impala_sql (HS2)
IdentityKerberos: Postgres GSSAPI role is the Impala/Kudu principal
Access controlPostgres GRANT + RLS / security-barrier views

Applications and governance tooling reach the data plane through PostgreSQL + impala_fdw. Impala remains the SQL engine; HS2 is the transport behind the FDW.

Role

PostgreSQL (:5455)  --impala_fdw-->  Impala HS2 (:21050)  -->  Kudu (:7051)
                         \------ kudu_scan (gov shapes) ------/

Postgres is the primary front end for AGE / Atlas / Ranger metadata SQL and, going forward, for row/sample data via the FDW. Impala is the SQL adapter behind the FDW; Kudu scans cover the known Atlas+Ranger+sigint algebra (SPEC §6).

Build

git submodule update --init components/impala_fdw
just impala-fdw-build

Status

Phase 1a: HS2 foreign scans with column projection, eq/range predicates, IN / = ANY pushdown, and EXPLAIN ShapeId / remote SQL. Atlas typed Kudu projections: config/atlas/kudu_projections.sql + kudu_projections_fdw.sql. Outbox: Atlas → Kudu outbox.

Phase 3 (lab-ready): direct kudu_scan via libkudu_client (kudu_scan.md). PR-K0–K4 + checkpoint-02 (LIMIT under agg/sort, multiset gates, warm-cache ~0.5 ms, hs2-smoke link). Atlas FTs default access=auto. PR-K5 Kerberos plan verified (staged K5a–K5e in kudu_scan.md); not yet implemented.

just impala-fdw-build && just impala-fdw-install
just atlas-kudu-projections-seed
just atlas-frontier-bench

Iceberg

Apache Iceberg provides an open table format for large analytic datasets with schema evolution, hidden partitioning, and time travel.

Role in the Stack

Iceberg serves as the warm-tier storage format. Tables are managed through the Polaris REST catalog and queried via Impala. Data migrates from Kudu (hot) to Iceberg (warm) as it ages, using CREATE TABLE ... AS SELECT through Impala.

Impala DDL → IcebergCatalogOpExecutor → Polaris REST Catalog → Object Storage

Polaris REST Catalog

Apache Polaris provides the Iceberg REST catalog API (port 8181). It manages Iceberg table metadata, namespace operations, and storage credentials. Polaris is backed by PostgreSQL for metadata persistence.

ServicePortPurpose
Polaris8181Iceberg REST catalog API

Impala Integration

The IcebergRESTCatalog class in the Impala fork implements createTable, dropTable, and renameTable against the Polaris REST API. Catalog configuration is in config/impala/catalog_config_dir/polaris.properties:

connector.name=iceberg
iceberg.catalog.type=rest
uri=http://localhost:8181/api/catalog
iceberg.rest.warehouse=signals

Current Status

  • Implemented: IcebergRESTCatalog.createTable(), dropTable(), renameTable() in the Impala fork
  • Near-term: End-to-end testing of Iceberg table creation via Polaris, SELECT queries on Iceberg tables, Kudu-to-Iceberg data migration

Data Lifecycle

Hot (Kudu)  ──CTAS──►  Warm (Iceberg/Polaris)  ──archive──►  Cold (S3/Object Storage)
 - Low-latency R/W       - Batch analytics              - Long-term retention
 - Recent data            - Historical data              - Compliance

Polaris

Apache Polaris is the Iceberg REST catalog. Signals vendors it as components/polaris from rch/asf-polaris pinned at apache-polaris-1.3.0-incubating (c940ded0) — the same pin realized in cldr/cybersec/thirdparty/polaris.

Role

Polaris is the catalog for data-product Iceberg tables on RustFS. It is not a product warehouse. JDBC persistence on Postgres :5455 / database polaris holds catalog metadata (namespaces, table pointers). Product details / tx / hx settle as Iceberg *_tier1 under s3a://signals-dataproducts/iceberg (RustFS). Hot land is Kudu *_tier0.

See Data Products History.

Lab surfaces

SurfacePort
REST catalog:8181
Management / health:8182 (/q/health/ready)
git submodule update --init components/polaris
just rebuild                         # assemble + restart polaris + wait :8182
# or: just polaris-install && devenv up -d
just signals-ready                   # polaris is WARN until :8182 answers
SKIP_POLARIS_BUILD=1 just rebuild    # restart only (Metabase SKIP_FE analogue)

Catalog create: scripts/setup_polaris_catalog.sh (catalog name signals, warehouse s3://signals-dataproducts/iceberg).

systemd

signals-polaris.service is a foundation unit (PartOf=signals.target). systemctl restart signals.target waits for (or starts) Polaris after signals.service. Prefer the devenv process when just up is the owner.

Impala

config/impala/catalog_config_dir/polaris.properties points Impala at http://localhost:8181/api/catalog with warehouse signals.

YuniKorn

Submodule: components/yunikorn-core → git@github.com:rch/asf-yunikorn-core.git.

Role in Signals federation

Apache YuniKorn is the required admission/scheduling instance for MiNiFi sentinels on system-wide RKE2—not the sentinel substrate and not the scale-to-zero controller (that is Knative Serving). Product path is K8s-first (RKE2 + Knative + YK); no permanent no-K8s sentinel mode.

ConcernAuthority
Sentinel agent, C2, overwatchMiNiFi C++ (components/minifi-cpp)
Scale sentinel pods to zeroKnative Serving (KPA)
Multi-tenant queues, fair-share, preemption of claimsYuniKorn (this submodule)
Actual GPU/engine workHost gRPC engines

See signals-protocol:

specification/operations/minifi_sentinels.md

Web UI

Primary surface is always signals-ui. Stock YuniKorn web is secondary.

As an Apache project, YuniKorn prioritizes core scheduler features. The bundled SPA (scheduler :9889, same-origin /ws/v1) may still ship with a given release, or ship disabled, by upstream preference—not something Signals owns or maintains. We do not deploy a separate yunikorn-web chart or invest in Angular yk-web ourselves.

While our pinned YK version still includes and enables the stock UI, keeping it available is fine (lab NodePort / just yk-ui-forward). When we move to a YK release that ships it disabled (or without it), we simply stop exposing it — no Signals feature depends on the stock SPA.

PieceRole
signals-uiPrimary backplane — strict superset of yk-web capabilities
Repogit@github.com:weathership/signals-ui.git
Submodulecomponents/signals-ui
StackRust / Axum (no Node); Keiretsu + Cloudera logo (Atelier continuity)
YKRequired once the stack lands (SIGNALS_YK_API_URL)
YK APIFull /ws/v1/* surface used by yk-web (contract-tested)
Value-addSentinels, OTel, Atlas OL lineage, engine discovery
Stock YK webOptional while upstream still embeds it; not a product dependency

Marquez-web remains an OL validation UI only; operators should prefer signals-ui for day-to-day control-plane work.

Lab exposure (tinybox RKE2)

PathURL
LAN classic (just yk-ui-forward)http://192.168.1.55:9889/
LAN NodePort (durable)http://192.168.1.55:30889/
RESThttp://127.0.0.1:30080/ (NodePort; also LAN :30080)
Recipesjust yk-ui-forward / just yk-ui-forward-stop
signals-ui YK URLSIGNALS_YK_API_URL — required; devenv default http://127.0.0.1:30080

Stack path: devenv up runs task signals:federation-ready (scripts/federation_preflight.sh) before signals-ui. That confirms (and if needed deploys) YuniKorn + Knative on the local RKE2 instance and probes YK REST. signals-ui does not start without a live scheduler — SIGNALS_UI_ALLOW_NO_YK is a hermetic-test escape only, never set by devenv.

just federation-ready          # same preflight
devenv tasks run signals:federation-ready

Zero Trust: this host’s WARP client is include-mode for Cloudflare CGNAT only — it does not publish the lab LAN. For iPad/laptop off-LAN:

  1. Zero Trust private network route for 192.168.1.0/24 (or the node IP) via a connector on this host, or
  2. cloudflared named tunnel + Access application → http://127.0.0.1:9889 (cloudflared tunnel login first).

Until either is configured, use the LAN URLs on the same Wi‑Fi/subnet.

Airflow

Apache Airflow 3.x is the production orchestrator for platform Metaflow DAGs on Signals RKE2 (not Argo Workflows). Lab-grade install uses the official Helm chart from the components/airflow submodule (appVersion 3.1.7).

ConcernChoice
Chartcomponents/airflow/chart (1.20.0-dev / AF 3.1.7)
ExecutorLocalExecutor (lab baseline; no Redis/Celery)
Metadata DBHost devenv PostgreSQL (airflow DB, bridged into ns)
UI / RESTAPI server NodePort 30800
Metaflow deploypython flow.py airflow create dag.py
Task runtimeKubernetesPodOperator → RKE2 pods (RBAC → metaflow)
Scheduler of podsYuniKorn (federation queues)
Time triggersMetaflow @schedule
Event triggersKnative Eventing → Airflow DAG runs (M3)

Full architecture: Platform Metaflow · Critical plane.

Status

PhaseState
Component submodulecomponents/airflow
Platform deploy on RKE2M2 — just airflow-platform
CI DAGsignals_ci (ConfigMap) · just airflow-platform-ci
Knative Eventing → DAG triggerM3 — just knative-eventing / just airflow-eventing-ci

Lab operations

# Deploy / upgrade (idempotent)
just airflow-platform

# Status
just airflow-platform-status

# UI URL + component health (OPEN=1 opens a browser)
just airflow-ui

# Trigger CI DAG and wait for success
just airflow-platform-ci

# Theta: daily LIGHT increments, earliest day of the range toward the present
just theta-backfill --from-date 2026-08-03 --to-date 2026-09-14 --dry-run
just theta-backfill --from-date 2026-08-03 --to-date 2026-09-14

# Critical plane preflight (auto-bootstraps Airflow when SIGNALS_STACK_AUTO_AIRFLOW=1)
just stack-ready
just signals-ready                  # check-only oneshot
SIGNALS_STACK_REQUIRE_AIRFLOW=1 just stack-ready
SurfaceValue
UI / APIhttp://127.0.0.1:30800 (NodePort 30800)
Adminadmin / admin (lab only)
API authAF3 JWT: POST /auth/token → Authorization: Bearer … for /api/v2
Namespaceairflow
Secrets filebuild/config/airflow-secrets.env (gitignored; fernet/jwt/api)
Valuesconfig/k8s/airflow/values-signals.yaml
Host PG bridgesignals-postgres-proxy (hostNetwork socat → 127.0.0.1:5455)
Zarfzarf.dev/agent: ignore until packaged into federation

UI

The Airflow 3 UI is the API server itself (React SPA + /api/v2) on NodePort 30800 — the RKE2 analogue of Marquez-web on :21011; both are listed in the devenv shell summary. just airflow-ui prints the URL and the /api/v2/monitor/health component status.

Direction — top-down schedule view. Airflow is where every federated engine’s schedule should be visible in one place: Metaflow flows deploy as DAGs (python flow.py airflow create), and pg_cron clocks on peer engines (e.g. Gaius gpu-metrics-settle) are to be mirrored here as DAGs. Until an image carries the signals.* package, config/k8s/airflow/dags/gpu_metrics_settle_dag.py is not mounted (its signals.ops import would surface as a DAG import error).

Stability note. scripts/signals_stack_preflight.sh lets an installed Broker settle (SIGNALS_STACK_EVENTING_WAIT, default 120s) before re-running the Eventing bootstrap, and the bootstrap only replaces Zarf-rewritten pods and only restarts the dag-processor when the DAG ConfigMap changed — re-bootstrapping on every signals-ui restart used to roll the eventing control plane and the dag-processor each iteration.

Metaflow

Platform Metaflow for Signals — ground-up capability for any federated engine via signals-protocol. Not Gaius-owned; not Marquez.

Submodulecomponents/metaflow
Remotegit@github.com:weathership/oss-metaflow.git
Branchrch/devenv
ArchitecturePlatform Metaflow

Role

ConcernAuthority
Flow authoring / run metadataMetaflow (this submodule + metadata service)
Production DAG scheduleAirflow (components/airflow)
Event / reactive triggerKnative Eventing → Airflow DAG runs
Task podsRKE2 + YuniKorn (@kubernetes)
ArtifactsRustFS (S3)
Lineage SoRAtlas OL (not Marquez DB, not Metaflow cards alone)
Operator UIsignals-ui Applications / Queues

Customization

Platform patches to Metaflow land on rch/devenv in weathership/oss-metaflow (same rch/devenv convention as other ASF/oss submodules). Signals pins that branch via .gitmodules.

git submodule update --init components/metaflow
cd components/metaflow && git checkout rch/devenv && git pull

Status

PhaseState
Submodule pinAdded (rch/devenv)
Platform metadata service on RKE2M1 — just metaflow-platform
Airflow + YKPlanned (M2)
Knative Eventing bridgeM3 — just knative-eventing · CE → Airflow (no Argo)

Lab deploy (M1)

# PG :5455 + RustFS :9010 up (devenv), RKE2 Ready
just metaflow-platform
just metaflow-platform-status
# → curl http://127.0.0.1:30180/ping

mkdir -p ~/.metaflowconfig
cp config/metaflow/platform.json ~/.metaflowconfig/config.json
export METAFLOW_SERVICE_URL=http://127.0.0.1:30180

Hermes Agent

Submodule: components/hermes-agent → git@github.com:zndx/oss-hermes-agent.git (upstream lineage: NousResearch Hermes Agent).

Role in Signals / Weathership

Hermes is a valuable agent runtime surface for multi-agent and operator workflows. For now, we do not treat Signals as a fork of Hermes core: we pin the submodule and ship exceptional plugins (memory + compaction) so Hermes can use Signals as SoR, governance, and federation.

That is a sequencing choice, not a ceiling. Surveying the federated engine-service fleet (Ægir, Atelier, Gaius, Signals stack, and related labs) shows a healthy superset of what Hermes offers today as a product agent. Weathership may later deepen integration (including deeper core contribution or a Weathership-hosted agent face) when product timing warrants it—without abandoning Hermes-compatible plugins or an official memory-provider path.

Hermes surfaceSignals / Weathership contributionOfficial Hermes docs
Memory provider (plugins/memory/<name>/)Reasoning-enabled long-term memory over Signals (AGE + OL + optional DST outcomes); path to official memory service provider listingMemory providers · Memory provider plugins
Context engine (plugins/context_engine/<name>/)Compaction that preserves lineage, tags, and authz-scoped contextPlugins · Context engine plugins
General plugins / toolsAtlas/OL/Ranger tools via discovery; OIP model ops with provenancePlugins overview
Signal (messenger)signal-cli in devenv packages; phone via secretspec SIGNAL_CLI_PHONEHermes gateway/platform once number is linked

Target product name for the official provider: Weathership (Signals-backed reasoning-enabled memory), alongside providers such as Honcho, Mem0, OpenViking, Hindsight, etc. in Hermes’s memory ecosystem.

Signal CLI (operator messaging)

pkgs.signal-cli is on the devenv package set so Hermes can message operators over Signal once the account is registered:

# declare in .env / secretspec provider (never commit the number)
# SIGNAL_CLI_PHONE=+1…
# SIGNAL_CLI_CONFIG_DIR=$DEVENV_STATE/signal-cli   # optional
signal-cli -a "$SIGNAL_CLI_PHONE" …               # link device / send as per signal-cli docs

Hermes platform wiring is follow-on; the CLI is available in-shell after devenv shell.

Federation superset (why “for now”)

Hermes is an excellent agent shell and plugin host. The constellation’s operating surface already goes further in directions Hermes may never fully adopt as first-class product:

Capability classWhere it lives (examples)Relation to Hermes
Multi-engine federation (zndx.engine.v1, OIP, co-tenancy)signals-protocol + Ægir / Atelier / GaiusHermes can call engines; it is not the federation SoR
Governance + lineage SoRAtlas /api/atlas/*, OL /api/v1/*, RangerHermes consumes via Weathership plugins / tools
DST / classification methodAtelier (healthy); sigint outcomes in SignalsBeyond stock Hermes memory providers
Mechanistic interpretability (SAE / CLT, feature analysis)Constellation ML/ops labs (nascent)Optional Hermes skills exist; depth is fleet-side
Topological analysis (persistent homology, Ollivier–Ricci curvature)e.g. signals.persistence / openph; geometry labsStructural memory & graph geometry Hermes is unlikely to own end-to-end

Plugins are the compatibility bridge into Hermes users and the official provider listing. The superset stays in federated engines and Signals so interpretability and topology can grow without waiting on upstream Hermes roadmap.

Plugin discovery (Hermes conventions)

From upstream:

  • Memory providers: plugins/memory/<name>/, subclass MemoryProvider; one active external provider via memory.provider in config.
  • Context engines: plugins/context_engine/<name>/, ctx.register_context_engine(...); one active via context.engine.
  • General tools/hooks: plugin.yaml + register(ctx) under ~/.hermes/plugins/ or project/pip entry points.

Signals-owned plugin code should live in this monorepo (e.g. plugins/hermes/ or under src/signals/hermes/) and install into Hermes discovery paths. Prefer upstream contribution for loader changes; deeper core integration is deferred (for now), not forbidden.

Submodule use

git submodule update --init components/hermes-agent

# inspect upstream plugin layouts
ls components/hermes-agent/plugins/memory
ls components/hermes-agent/plugins/context_engine

The submodule is the API and reference implementation surface (bundled providers, schemas, loaders). Product plugins that implement Weathership memory and compaction depend on it and on Signals services (Atlas, /api/v1, Ranger, discovery from signals-protocol).

Near-term constraints (not permanent non-goals)

  • For now: no Hermes-core fork for discovery/loaders; plugins first.
  • Do not ship a private memory DB that bypasses Atlas/AGE/OL provenance.
  • Do not make Hermes the system of record — Signals remains SoR; Hermes is an agent client (and listing channel) that remembers through us.
  • Later: optional deeper Weathership agent face or upstream core work if the superset (SAE/CLT, topology, federation) needs a first-class home beyond plugins.

MiNiFi C++ (federation sentinels)

Submodule: components/minifi-cpp → git@github.com:weathership/oss-minifi-cpp.git (Apache NiFi MiNiFi C++).

Role

Sentinel substrate for signals-protocol process coordination:

  • Thin agents that represent host gRPC engines / agents to a control plane
  • C2 — agent HTTP to the Signals C2 server; C2 calls Engine/Yield
  • YK Application — the sentinel is the admitted workload
  • Metrics / OTel for activity truth (NiFi canvas later, not in signals-ui)

Host engines remain the execution plane. Product target is K8s-first (system-wide RKE2)—not a host-only MiNiFi deployment:

ComponentRole
Knative ServingScale sentinel Services to zero (KPA)
YuniKorn (components/yunikorn-core)Multi-tenant admission/queues for sentinel pods

Binding design:

components/signals-protocol/specification/operations/minifi_sentinels.md

NiFi

Apache NiFi provides data flow routing, transformation, and system mediation.

Federation C2 is not NiFi. The sentinel yield loop is a Signals-owned C2 HTTP server plus Engine/Yield gRPC. NiFi+canvas is an optional later K8s fielding for OTel wiring — not folded into signals-ui, not required for YK to end a sentinel Application. See Sentinel Applications and Yield.

Operations Guide

How to run the Signals hub on a workstation or lab host.

  1. Development Environment — devenv shell, just up, tasks
  2. Peer integration — join the lattice
  3. Secrets — SecretSpec, dotenv, keytabs
  4. Services — PostgreSQL (AGE, pg_cron)
  5. Kerberos — project-local KDC (DEV.VISTA.ZNDX.ORG)
  6. Storage and backup — SIGNALS_DATA_ROOT, stamps
  7. Peer data products — warehouse facts
  8. Peer unit acceptance — signals.target and lattice-ci
devenv shell
just up
just signals-ready
just kinit
just lattice-ci
just test
just docs-serve

Infrastructure provisioning (AWS, Zarf, Tilt) is under Infrastructure.

Development Environment

Signals uses devenv (Nix-based) with direnv for automatic shell activation.

Entering the Environment

devenv shell          # Manual entry
# or automatic via direnv when cd-ing into the repo

Secrets: SecretSpec is enabled in devenv.yaml (dotenv provider). Declarations live in secretspec.toml — see Secrets. Non-secret Kerberos identity (KRB5_REALM=DEV.VISTA.ZNDX.ORG, host FQDN) is set in devenv.nix.

secretspec run -- just tag default.my_table   # inject declared secrets for a job

Starting Services (turn-key)

# New machine (one-time): git submodule update --init --recursive
# One-time ASF builds if missing: devenv tasks run kudu:build-cpp && devenv tasks run impala:build

just up                 # devenv up -d — full stack; validates + bootstraps
devenv processes list   # Expect 12: kudu-*, impala-*, atlas, marquez-web, rustfs, signals-ui, …
just down               # processes down + stop *our* Postgres only (lattice-safe)
just stack-reset        # down + free our orphans + up -d
just rebuild            # Polarisfork assemble + devenv restart + wait :8182
just redeploy           # K8s product refresh (Metaflow, Airflow, Eventing → YK queues)
just kinit              # Refresh user ticket if needed
just kerberos-status    # Expect: impala HS2 GSSAPI OK

Prefer just up / just down. just down stops this tree’s Postgres on :5455 and leaves sibling project databases alone.

devenv up -d (just up) starts the hub:

  1. Ensures data layout under SIGNALS_DATA_ROOT
  2. Starts KDC, then kerberos-bootstrap (keytabs + kinit) before Kudu/Impala
  3. Starts the full host graph (Postgres, RustFS, Atlas, Marquez-web, Ranger, Kudu, Impala, signals-ui)
  4. Runs stack-ready before signals-ui: data-plane health, YuniKorn/Knative, Metaflow, Airflow (auto-deploy when missing). Peers use just signals-ready for check-only PASS/WARN/FAIL (critical includes Kudu + Metaflow).

just bootstrap remains a recovery alias for Kerberos outside a process session — not a separate curriculum step.

Full stack is always required. Host data/governance services and the RKE2 critical plane (YuniKorn, Knative, Metaflow, Airflow) are one Signals deployment — not optional bolt-ons. See Critical plane.

devenv.yaml sets strictPorts: true so Postgres stays on :5455 (no silent bump to :5456 that breaks Impala catalogd / JDBC).

Postgres port lattice (shared lab host)

Multiple devenv trees share the machine. Do not reuse each other’s PG ports:

PortProject
5432System/apt PostgreSQL (leave alone)
5438cybersec / cyberphy
5444gaius
5455signals (this tree)
5533atelier
5555aegir
5566synth

scripts/signals_port_lattice.sh runs at process start: if :5455 is held by a process that is not this tree’s postmaster, devenv up fails with a clear message — it will not kill another project’s Postgres. Stack reset only stops our .devenv/state/postgres postmaster.

Do not run Atlas/Marquez/Metaflow as long-lived orphans outside devenv up / devenv processes down (plus RKE2 platform bootstrap).

Python is devenv’s. languages.python (python312 + uv) owns .devenv/state/venv; every script, readiness oneshot and systemd unit resolves the interpreter through scripts/signals_python.sh (signals_python_path / signals_py, override SIGNALS_PYTHON) — never a host python3, which has no protobuf/grpc/kerberos wheels (the signals-engine false WARN in signals-ready). signals_python_path requires import grpc; a half-built venv (uv sync in flight) is not ready. Process execs use that venv binary directly — they must not uv run at start (that re-syncs torch/CUDA and races systemd). Task signals:uv-sync runs uv sync --frozen --no-dev only when grpc is missing (uv run without --no-dev pulls sentence-transformers/torch onto root). Wheel cache is UV_CACHE_DIR=/raid/cache/uv when /raid/cache exists. Justfile recipes use uv run. Cold-start gate: just signals-cold-start-ci.

Process manager: native (process.manager.implementation = "native"). Ordering uses process after / ready (Kudu → Impala; Postgres → Atlas → Marquez; signals:stack-ready before signals-ui). Control:

devenv up -d              # start full graph
just stack-ready          # preflight (+ optional auto-bootstrap)
just signals-ready        # check-only oneshot for peers / systemd
devenv processes list     # expect kudu-*, impala-*, atlas, marquez-web, rustfs, signals-ui, …
devenv processes down     # stop full graph (or: just down)
Process / platformRoleDefault port(s)
postgresdevenv PostgreSQL 16 + AGE (Atlas graph, Ranger, catalog). Not pglite.5455
kdcKerberos8848
kudu-master / kudu-tserverColumnar store7051/7050 (web 8051/8050)
impala-statestore / catalogd / impaladSQL + kudu_scanHS2 21050
atlasGovernance + OL API21010
marquez-webOL UI → Atlas /api/v121011
ranger-adminAuthz6080
rustfsS3 objects (critical object plane)9010
polarisIceberg REST catalog (warehouse on RustFS)8181 / admin 8182
signals-uiControl plane UI (requires stack-ready)9889
RKE2 YuniKornFederation schedulerREST 30080
RKE2 KnativeServing (+ Eventing M3)Serving ns
RKE2 MetaflowPlatform metadata service30180
RKE2 Airflow 3Platform DAG orchestration + UI (just airflow-ui; admin/admin)30800

Turn-key first run: devenv up [-d] is the only entry point users need for the core stack. Marquez-web is bootstrapped like other heavy UI deps (cybersec pattern):

  1. languages.javascript points at components/marquez/web with npm.install.enable = true (checksummed npm clean-install on shell enter).
  2. Task marquez:build-web has before = [ "devenv:processes:marquez-web" ], so process-compose runs submodule init + npm + webpack before the UI process.
  3. processes.marquez-web only serves (setupProxy.js → Atlas /api/v1).

Port convention: Marquez UI binds Atlas HTTP + 1 (defaults :21010 / :21011). Override with MARQUEZ_WEB_PORT / SIGNALS_ATLAS_HTTP_PORT. Do not use :3000 for Marquez — leave that for ad-hoc local frontends.

No separate “please run marquez:build-web first” step for a normal lab bring-up.

Data root: durable services use SIGNALS_DATA_ROOT (lab default /raid/signals — kudu/, rustfs/, flink/, backups/). See Storage and backup. Preserve Atlas/Ranger with just backup / just restore.

ASF submodules and nested devenv

Atlas, Ranger, Kudu, and Impala live under components/* as git submodules of rch/asf-* (and similar) forks. Tracked branch: rch/devenv — the line non-signals products should consume for Nix/devenv buildability. Optional rch/signals overlays (if any) rebase onto rch/devenv; prefer promoting shared tree fixes into rch/devenv instead of product-named branches.

Build knowledge lives next to those trees; the host devenv owns ports, realm, process graph, and product config.

LayerResponsibility
Component on rch/devenv (devenv*.nix)How to compile that ASF tree on Nix (packages, *:build-* tasks)
Host (repo-root devenv.nix)PG :5455, KDC, Atlas :21010, Marquez-web :21011 (Atlas + 1), wiring, SecretSpec
Product config (config/)install.properties, Impala HMS-free, AGE JDBC, local overrides

Kudu already ships a full nested devenv on rch/devenv. Treat that as the reference: split a library module (packages + build tasks) from optional standalone processes so hosts can import without starting a second KDC/cluster.

Maven isolation: do not install SNAPSHOTs into ~/.m2. devenv sets SIG_MAVEN_REPO=$PWD/.devenv/m2 and MAVEN_ARGS=-Dmaven.repo.local=…. Ranger → Impala FE resolution uses that store; Impala skips the CDP Ranger admin tarball via config/impala/impala-config-local.sh (RANGER_VERSION_OVERRIDE / RANGER_HOME_OVERRIDE).

JDK isolation: Prefer ${pkgs.jdkN} in tasks over scanning /nix/store or /usr/lib/jvm. Kudu Java/Gradle uses devenv jdk17. Ranger currently uses devenv jdk11 only because of remaining Nashorn build deps—that is interim; when appropriate we move Ranger forward to ditch Nashorn and modern JDKs (see Ranger).

Impala toolchain isolation: do not add pkgs.thrift or pkgs.boost to host packages — they leak into CMake and pull the wrong thrift. Impala uses its downloaded toolchain; only impala_fdw:build pins Nix thrift/boost for the FDW.

Design detail and phased rollout: docs/scratch/2026-08-05/234317_asf-devenv-nesting.md (and devenv’s monorepo guide).

Languages

LanguageVersionPurpose
RuststablePrimary application language
Python3.12Tooling, Airflow; uv for package management
Java21ASF component builds (Maven enabled)
TypeScript/JSlatestFrontend

Available Tasks

Service Tasks

devenv tasks run signals:kdc-init    # Initialize KDC
devenv tasks run signals:kdc-reset   # Reset KDC database

Documentation Tasks

devenv tasks run docs:build          # Build mdbook documentation
devenv tasks run docs:serve          # Serve docs with live reload

Local Cluster Tasks (k8s:*)

devenv tasks run k8s:provision       # Create k3d cluster
devenv tasks run k8s:status          # Check cluster connectivity
devenv tasks run k8s:deploy-dask     # Deploy Dask operator + cluster
devenv tasks run k8s:deploy-jupyter  # Deploy JupyterHub
devenv tasks run k8s:forward         # Port-forward services
devenv tasks run k8s:destroy         # Delete k3d cluster

AWS Cloud Tasks (aws:*)

devenv tasks run aws:provision       # OpenTofu apply (VPC, EC2, S3)
devenv tasks run aws:inventory       # Generate Ansible inventory
devenv tasks run aws:deploy          # Full stack: RKE2 + Dask + services
devenv tasks run aws:verify          # Post-deployment health checks
devenv tasks run aws:ssh             # SSH to bastion host
devenv tasks run aws:destroy         # Destroy infrastructure (preserves S3)
devenv tasks run aws:teardown        # Full teardown including S3

Included Tooling

The devenv environment includes tools for all deployment modes:

CategoryTools
Coregit, gh, jq
Securitykrb5, cyrus_sasl, openssl
ASF Buildcmake, ninja, gcc, protobuf, flatbuffers
Kuberneteskubectl, helm, tilt, k9s, k3d
Cloudawscli2, opentofu, ansible
Air-Gapzarf, conftest, podman
Ingresscloudflared
WASMwasmtime, wasm-pack, wasm-bindgen-cli, binaryen
Datagrpcurl, dbmate
Docsmdbook, mdbook-d2, d2, graphviz

BDD Testing

uv run behave                        # Run all BDD scenarios
uv run behave --dry-run              # Parse features without executing
uv run behave features/platform/     # Run platform scenarios only

See Test Infrastructure for the tier system.

Peer integration

How sibling engines and AGPL services attach to the Signals hub and consume the critical plane.

Signals holds control: signals.target, peer-contract, lattice-ci, platform Metaflow, YuniKorn, Atlas, the warehouse. Product engines (Gaius, Ægir, Atelier, Hermes) keep native gRPC and add zndx.engine.v1. See signals-protocol.

End state (lab): after each peer finishes its local unit work and is enabled under the group:

sudo systemctl start signals.target
# → foundation (signals.service) + signals-ready + every enabled peer

Bare systemctl start signals starts only the foundation unit — not peers.


Two layers

LayerWhatHow peers use it
Process / groupsignals.target + foundation ready gatesystemd After=signals-ready.service
Wire / productsignals-protocol + platform endpointsgRPC zndx.engine.v1.Engine, Metaflow/Airflow/CE, Atlas
ArtifactPath
Machine-readable contractconfig/platform/peer-contract.json
Resource-class queuesconfig/scheduler/resource-classes.md
Unit samplesinfra/systemd/
Peer-repo acceptance templatesPeer unit acceptance spec
Group control READMEinfra/systemd/README.md
                    ┌─────────────────────────────┐
                    │     signals.target          │
                    │  (group controller)         │
                    └─────────────┬───────────────┘
                                  │
           ┌──────────────────────┼──────────────────────┐
           ▼                      ▼                      ▼
   signals.service      signals-ready.service     signals-engine.service
   just up / down       just signals-ready        :50551 After=ready
           │                      │                      │
           ▼                      ▼                      ▼
   critical plane          PASS ⇒ exit 0           Engine + Scheduler
   PG Kudu Impala          Kudu+Metaflow           (enabled peers after ready)
   YK Metaflow AF          critical included       gaius · aegir · atelier
   Eventing Broker                                 hermes · synth · metabase (AGPL opt.)

Peers are peer-to-peer with each other (no ordering edges among engines). All order after signals-ready.service.


Foundation lifecycle

# Manual / CI
just up
just signals-ready          # check-only; exit 0 when critical plane is ready
just down                   # lattice-safe stop

# Group control
sudo systemctl start signals.target
systemctl list-dependencies signals.target
sudo systemctl stop signals.target

Do not treat devenv up -d returning as “ready.” Peers wait on signals-ready (or the oneshot that polls it). Prefer just down over bare devenv processes down so Postgres :5455 is released.

Host tools (system-wide)

Service units do not inherit devenv/nix PATH. Install under /usr/local/bin (or equivalent on systemd PATH):

ToolUsed by
justFoundation + peer wrappers
kubectlsignals-ready (Eventing Broker)
grpcurllattice-ci + peer accept probes

See infra/systemd/README.md.


What peers consume (not re-host)

ConcernUse platformAvoid
Workflow metadataMetaflow :30180 + config/metaflow/platform.jsonEngine-local Tilt Metaflow as SoR when in platform mode
DAG productionAirflow :30800Argo Workflows for Metaflow prod
EventsKnative Broker signals-events/defaultArgo Events
Schedule / STZYuniKorn + Knative ServingUnscheduled free-for-all pods
Lineage / governanceAtlas OL + tags (:21010)Peer Marquez DB
Analytic tablesImpala HS2 + Kudu (Kerberos)Parallel warehouses on :5455
Object storeRustFS :9010Competing S3 on same ports
Data product inventorySignals warehouse (details / tx / hx) + RustFS URIsPeer Postgres / JSON catalog as SoR

A peer that maintains its own product (Gaius prospects, Ægir corpora, …) follows Peer data products. Contract: data_products.md.

Postgres port lattice (do not collide):

PortOwner
5432system/apt (Metabase app DB may use)
5438cybersec / cyberphy
5444Gaius
5455Signals foundation
5533Atelier
5555Ægir
5566Synth
5577Metabase engine DB (AGPL product)

How to organize work on queues

YuniKorn leaves are resource classes, not project names. There is no root.gaius (or root.aegir) GPU slice. Stamp identity on the Application and pick a leaf from resource-classes.md.

WorkQueue
Host GPU: reasoning / CoT chatroot.internal.inference.reasoning
Host GPU: code modelroot.internal.inference.coding
Host GPU: planner / tool-routerroot.internal.inference.orchestration
Host GPU: instruct / short chatroot.internal.inference.instruct
Host GPU: embeddingsroot.internal.inference.embedding
Host GPU: OCR / docling / extractroot.internal.inference.extract
CPU-only burst / idle sentinelroot.internal.compute
Metaflow UI / Airflow / Eventingroot.platform
Pay-per-token APIroot.external.token-metered
RPM/TPM-limited APIroot.external.rate-metered
Grok ACP / xAI subscriptionroot.external.subscription.rate-limited

Required stamps:

  • yunikorn.apache.org/queue annotation (placement rule is provided)
  • yunikorn.apache.org/app-id = federation.workload_id
  • federation.project for C2 → Yield
  • Host GPU: request federation.zndx.org/gpu only (never nvidia.com/gpu on a CPU-only sentinel). In-cluster CUDA: both keys.

YK counts cpu/memory/GPU-token/maxapplications. Token, RPM, and subscription math stay in the peer engine.

Platform services we deploy (Metaflow, Airflow, Eventing sink): just redeploy. That is the K8s refresh — not systemctl restart signals.target. Signals just rebuild is the host Polarisfork loop (assemble components/polaris, restart devenv polaris, wait :8182) — same shape as Metabase just rebuild, different tree. Metabase stays a host peer under signals.target.

MCP catalog/suggest tools come after this tree is live. Assignment RPCs land in signals-protocol only after peers use that catalog.

signals-protocol engines (gRPC lattice)

Each peer registers zndx.engine.v1.Engine (federation face) beside any native service. That face is the shared federation contract in progress — it evolves as real peer needs land in engines and get promoted into signals-protocol (see protocol core — evolution). Peer units and lattice-ci only attach process lifecycle to whatever Status the contract currently requires; they do not define the full engine architecture.

Lab lattice:

PeerPortCapability (Status)Unit sampleNotes
Gaius50051cognitiongaius.serviceProduct engine / federation mesh
Ægir50151instructaegir.serviceCapability engine (+ native face)
Atelier50251refereeatelier.serviceCapability engine; native servicer may be :50071 on co-tenant hosts
Synth50351synthesissynth.serviceOptional peer
Metabase50451dashboardmetabase.serviceLicense-external (AGPL); isolated engine, not core family
# Reflection required on the lattice port (signals-protocol install requirement)
grpcurl -plaintext 127.0.0.1:<port> list   # must include zndx.engine.v1.Engine
grpcurl -plaintext 127.0.0.1:<port> zndx.engine.v1.Engine/Status
just lattice-ci                      # SKIP absent; PASS listening (uses reflection)
just lattice-ci --require gaius,aegir,atelier

Python engines: depend on grpcio-reflection and enable reflection at server start. Spec: engine_grpc.md — Server reflection · signals-protocol.

OIP (KServe Open Inference Protocol) is the long-term portable inference face; Complete remains a transitional convenience on many engines.


Common peer unit pattern (process attachment)

Reference implementations: Metabase (unit shape, license-external) and Gaius (core peer, first full lattice accept under signals.target). The process pattern is shared; engine lineage for core peers is Gaius/Ægir/Atelier — Metabase is license-external and isolated.

Doctrine: federation peerhood is total commitment — full engine capacity for that project, honest start/stop/restart, remediation on failure. Lattice Status is a gate on the real engine, not a substitute for it. Signals being early on its engine axis does not license half- committed peers or freeze future engine work here.

Must

  1. After=signals-ready.service + Wants=signals-ready.service
    Soft dependency: foundation failure does not hard-fail the peer (Requires= only if you want a hard gate).

  2. PartOf= / WantedBy=signals.target
    Group stop/restart and opt-in membership via systemctl enable/disable.

  3. Wrappers in the peer tree (not signals, not multiline shell in the unit):

    • scripts/systemd_start.sh — just up / full devenv up for this project (capability engine + product UI/gateway/DB); block until lattice Status and product surfaces answer
    • scripts/systemd_stop.sh — peer-scoped full-stack stop of this unit’s devenv graph (and vLLM children); lattice + product ports free
      Unit ExecStart= / ExecStop= point at those absolute paths under the peer checkout. systemd rejects fragile multiline shell; Metabase hit this first. Reference shapes: Gaius / Metabase (already full-stack); Ægir / Atelier remediated 2026-08-13 (were engine-only — incorrect under doctrine).
  4. Accept = full stack ready, not “Status alone” or “engine-only”:

    • Codegen Status (preferred in-peer): generated zndx.engine.v1 stubs, project matches contract (see Signals scripts/zndx_engine_status.py / Gaius scripts/zndx_status_ok.py)
    • Honest health — no synthetic always-healthy capabilities when backends are down (total-commitment doctrine)
    • Product UI/gateway as that peer documents (e.g. Ægir :8091+:5173, Atelier :8090+:3000, Metabase :3200)
    • Reflection on the lattice port so external bare grpcurl works (install grpcio-reflection / enable ServerReflection — required)
    • TCP listen alone is not enough (Gaius lesson)
    • Transition failures → remediation (peer-local health/FMEA; e.g. Gaius /health fix engine)
  5. Idempotent start only when the unit already owns a healthy single listener
    If accept probes pass and this unit is the sole owner of the lattice port with real capacity, exit 0 without tearing down a live stack. Multi- listener, orphans outside the unit cgroup, or optimistic Status that hides missing capacity are errors — stop/remediate, do not skip-up.

  6. Single owner of the lattice port
    Avoid dual listeners (orphan devenv + unit). Reclaim or refuse multi-bind (Gaius lesson on :50051). Interactive devenv up must not fight the unit.

  7. PATH
    System-wide just / grpcurl / kubectl on systemd PATH; bash -lc inside wrappers when devenv/direnv is required.

  8. Never bind Signals :5455 or RustFS :9010.

  9. Product doc in the peer tree (Gaius pattern): docs/current/src/operations/peer-unit.md — unit SoR for that project; not mesh/FEDERATION.md history.

Unit stop: peer-scoped (not “soft”)

Do not call this “soft stop.” That sounds like incomplete shutdown. The requirement is full stop of this peer’s unit, scoped so co-tenants survive.

MustMust not
Fully stop this peer’s lattice unit — engine process, process group, and vLLM/worker children it ownsLeave the lattice port listening “for convenience”
TERM → grace → KILL if needed; call engine mgr.shutdown() / process-group teardownHost-wide just teardown, gpu-deep-cleanup, or kill-by-pattern that matches sibling peers
Release this peer’s GPU leases / free this peer’s GPUsWipe /tmp/zndx-gpu-leases or nvidia processes owned by Gaius/Ægir/Atelier others
Stop the full project stack owned by the unit (devenv graph: UI + gateway + lattice engine)Engine-only stop that leaves product UI running (or vice versa) while claiming unit stopped
Restart = full stop then full start (total commitment)Treat orphan Status or multi-bind as success

Why the confusion: early Gaius unit stop used just down / devenv processes down and explicitly avoided teardown recipes that historically killed co-tenant GPUs. That is lease-safe / co-tenant-safe, not “don’t really stop.” Engine-only units (Ægir, Atelier) should stop the engine hard; they should not run product teardown.

Preferred names: peer-scoped stop, unit stop, or lease-safe stop.

Operator flow (any peer)

# In peer repo: implement wrappers + federation Status; standalone accept
# In signals:
just install-systemd --enable --start                    # foundation once
just install-systemd --peers <id> --enable               # opt-in peer sample
# Edit /etc/systemd/system/<id>.service paths if checkout ≠ lab default
sudo systemctl daemon-reload
sudo systemctl start signals.target                      # one-command group
just lattice-ci --require <id>
CommandStarts
systemctl start signalsFoundation only
systemctl start signals.targetFoundation + ready + all enabled peers (does not restart live members)
systemctl restart signals.targetComplete refresh — stop every PartOf= member, start all enabled members, signals-refresh.service verifies Status
just signals-restartSame recycle + fail if any enabled member is not active
just signals-cold-start-ciElevated gate — script contracts + complete recycle + lattice-ci (routine cold start; not a smoke)
systemctl start <peer>That peer alone (still After=signals-ready)

Acceptance templates: peer-unit-spec.


Federated in-org peers

These sections are the operations reference for peer-repo integration sessions and for Signals operators who enable the peer after that work lands.

PeerStatus
MetabaseComplete (license-external AGPL; isolated engine)
GaiusFull-stack under signals.target (Status + lattice-ci)
ÆgirFull-stack remediation 2026-08-13 (just up; was engine-only) — re-validate UI ports
AtelierFull-stack remediation 2026-08-13 (just up; was engine-only) — re-validate UI ports

Tick accept in peer-unit-spec.

Gaius

FactValue
RoleCognition / product engine; federation mesh participant
Checkout (lab)~/local/src/zndx/gaius
Unit sampleinfra/systemd/gaius.service → Gaius scripts/systemd_{start,stop}.sh
gRPC lattice:50051 — zndx.engine.v1.Engine beside native GaiusService + OIP
Postgres lattice:5444 (zndx_gaius) — never :5455
Capabilitycognition (Status.project=gaius)
Product lifecyclejust up / just down (wrappers); process gaius-engine
Platform MetaflowMETAFLOW_SERVICE_URL=http://127.0.0.1:30180 + platform profile when federated
Eventse.g. dev.gaius.article.curate.requested → platform Broker
Signals checklistpeer-unit-spec — gaius
Product ops (SoR for unit)Gaius tree docs/current/src/operations/peer-unit.md
Not the accept gatesrc/gaius/engine/FEDERATION.md (older KServe/GPU mesh)

Landed in Gaius tree (peer session):

  1. scripts/systemd_start.sh / systemd_stop.sh — wait for Status (not only TCP).
  2. GaiusZndxEngineServicer on the existing gRPC server (project=gaius, capability cognition).
  3. Product doc docs/current/src/operations/peer-unit.md.
  4. Unit installed: just install-systemd --peers gaius --enable (lab host).

Lessons for other peers (from Gaius):

LessonDetail
TCP ≠ StatusNative service already on :50051 did not imply zndx.engine.v1.Engine/Status
Third servicerRegister lattice face beside native (+ OIP); do not replace product gRPC
Status earlyStatus is live at gRPC bind (~phase GRPC); do not wait for vLLM/endpoint load
Stop is peer-scoped, not weakFully stop this peer’s engine (+ children); never host-wide teardown / gpu-deep-cleanup that kills sibling leases (see Unit stop)
One engine processTwo devenv daemons can both claim :50051; recycle this checkout’s engine for accept
FEDERATION.md vs peer-unit.mdMesh write-up ≠ lattice accept gate
lattice-ciElevated CI gate; reflection required on lattice port

Operator — finish accept (after engine recycle):

# In Gaius: restart gaius-engine so the lattice servicer is bound
# e.g. devenv processes restart gaius-engine   (from this checkout only)

cd ~/local/src/wxs/signals
sudo systemctl start signals.target    # or: sudo systemctl start gaius
grpcurl -plaintext 127.0.0.1:50051 zndx.engine.v1.Engine/Status
just lattice-ci --require gaius

Caution: Gaius-local Metabase on :3100 is not federation dashboard. That is the optional AGPL Metabase peer (:3200 / :50451).


Aegir

Ægir instruct / inference peer (product name often styled “Ægir”). Next peer session after Gaius.

Agent one-liner (full paths for the Aegir session):

Implement peer-unit@aegir per
/home/rch/local/src/wxs/signals/docs/current/src/operations/peer-unit-spec.md
(Filled: aegir) and
/home/rch/local/src/wxs/signals/docs/current/src/operations/peer-integration.md
(Aegir / #aegir). Contract:
/home/rch/local/src/wxs/signals/config/platform/peer-contract.json.
Mirror:
/home/rch/local/src/zndx/gaius/docs/current/src/operations/peer-unit.md,
/home/rch/local/src/zndx/gaius/scripts/systemd_start.sh,
/home/rch/local/src/zndx/gaius/scripts/systemd_stop.sh,
/home/rch/local/src/zndx/gaius/scripts/zndx_status_ok.py.
Full-stack under signals.target: unit = just up (capability-engine + gateway + vite). Accept = Status + product UI health.

FactValue
RoleOntology / corpus engine; the federation Announce directory; hosts no model — forwards instruct (thinking on, effort low) and thinking to the peer that hosts Qwen3.8-27B (Gaius)
Architecture classcore_federated_engine (Gaius-lineage capability engine)
Checkout (lab)~/local/src/zndx/aegir
Unit sampleinfra/systemd/aegir.service → Ægir scripts/systemd_{start,stop}.sh
gRPC lattice:50151 — native + zndx.engine.v1.Engine + OIP
Gateway / Vite UI:8091 / :5173
Postgres lattice:5555
Capability / StatusStatus.project=aegir; endpoints empty (nothing hosted — honest); no capability_hint in the contract
Unit process modelFull devenv (just up) — process capability-engine + gateway + vite
AcceptStatus :50151 + gateway health + vite
Session checklistpeer-unit-spec — aegir
Product SoRÆgir tree docs/current/src/operations/peer-unit.md

Remediation (2026-08-13): earlier engine-only unit left product UI dark under signals.target — incorrect under full-stack doctrine. Wrappers now mirror Gaius/Metabase (just up / full-stack stop).

Operator:

just install-systemd --peers aegir --enable
sudo systemctl restart aegir   # or signals.target
curl -sf http://127.0.0.1:8091/api/health
curl -sf -o /dev/null http://127.0.0.1:5173/
grpcurl -plaintext 127.0.0.1:50151 zndx.engine.v1.Engine/Status
just lattice-ci --require aegir

Atelier

FactValue
RoleReferee / CAI; capability engine on lattice
Architecture classcore_federated_engine
Checkout (lab)~/local/src/zndx/atelier
Unit sampleinfra/systemd/atelier.service → wrappers
gRPC lattice:50251 — native + zndx.engine.v1.Engine + reflection
Product servicer:50071 (workbench; part of full stack)
Gateway / Vite UI:8090 / :3000
Postgres lattice:5533
Capability / Statusproject=atelier; referee (+ configured caps)
Unit process modelFull devenv — capability-engine + grpc-server + gateway + vite
AcceptStatus :50251 + :50071 + gateway + vite
Product SoRAtelier docs/current/src/operations/peer-unit.md
Session checklistpeer-unit-spec — atelier

Remediation (2026-08-13): engine-only unit was an oversight; both lattice and product UI belong under the unit.

Operator:

just install-systemd --peers atelier --enable
sudo systemctl restart atelier
curl -sf -o /dev/null http://127.0.0.1:3000/
grpcurl -plaintext 127.0.0.1:50251 zndx.engine.v1.Engine/Status
just lattice-ci --require atelier

Hermes Agent

Hermes is an in-org agent peer (Status.project=hermes, capability agent). It is not a user-session unit: signals.target is a system group, so membership is hermes.service with WantedBy=signals.target. A ~/.config/systemd/user/hermes.service on default.target never joins the group and stays down after a host reboot.

FactValue
Roleagent engine (Status.project=hermes)
gRPC:50651 — zndx.engine.v1.Engine
Dashboard:9119
Sample unitinfra/systemd/hermes.service
WrappersHermes tree scripts/systemd_start.sh / systemd_stop.sh
Peer contractpeers[] id hermes
Product SoRHermes README.engine.md
just install-systemd --peers hermes --enable
sudo systemctl start hermes.service
grpcurl -plaintext 127.0.0.1:50651 zndx.engine.v1.Engine/Status
just lattice-ci --require hermes

External engines: Metabase (AGPL)

Metabase is external by license requirement (AGPL vs ASL2), not merely “optional product preference.” The Metabase engine (mbengine) is therefore inherently isolated: separate checkout, no vendoring into Signals or core peer trees, process + wire only. Architecturally it is distinct from the core federated engine family (Gaius, Ægir, Atelier, and future core projects such as synth or vigil). See Core vs license-external engines.

The core Signals stack does not require Metabase. Install only when you want federated dashboard capability.

FactValue
LicenseAGPL-3.0 (Signals is Apache-2.0) — isolation is required
ArchitectureIsolated product engine; not Gaius-lineage / core peer family
RoleLicense-external dashboard engine (Status.project=metabase)
gRPC:50451 — zndx.engine.v1.Engine
Product HTTP:3200 (health: GET /api/health)
Engine Postgres:5577 (product-local; not signals :5455)
Sample unitinfra/systemd/metabase.service
WrappersAGPL tree scripts/systemd_start.sh / systemd_stop.sh
Peer contractpeers[] id metabase
Product docsAGPL tree README.engine.md
Accept templatepeer-unit-spec — metabase (landed)

License boundary

DoDon’t
Separate checkout (e.g. ~/local/src/agpl/metabase)Submodule / jar-vendor into signals
Unit WorkingDirectory / Exec* → AGPL tree onlyShip AGPL inside ASL2 images without a legal plan
Vendor only signals-protocol in MetabaseCopy FE/BE into weathership/signals
Process + network integrationSecrets on the federation wire

Optional install (operators)

# 1. Standalone product (AGPL tree)
cd ~/local/src/agpl/metabase
just up   # or just rebuild
curl -sf http://127.0.0.1:3200/api/health
grpcurl -plaintext 127.0.0.1:50451 zndx.engine.v1.Engine/Status

# 2. From signals — opt in
cd ~/local/src/wxs/signals
just install-systemd --enable --start
just install-systemd --peers metabase --enable
# Edit unit paths if needed
sudo systemctl start signals.target   # not bare "signals"

# 3. Accept
systemctl is-active metabase.service
just lattice-ci --require metabase

Federation leverage (Metabase)

  1. Foundation ready before start.
  2. Optional platform Metaflow profile for flows that feed dashboards.
  3. CloudEvents via platform Broker when DAGs exist.
  4. YK queues for any RKE2 work.
  5. No second critical plane (engine/app DBs stay product-local).

Synth (stub)

FactValue
gRPC:50351
Postgres:5566
Unitsynth.service
StatusSame pattern as Gaius/Ægir/Atelier when scheduled

Systemd membership (multi-peer)

# Foundation once
just install-systemd --enable --start

# Enable only peers whose local unit + Status work is done
just install-systemd --peers gaius,aegir,atelier,hermes,metabase --enable

sudo systemctl start signals.target
systemctl list-dependencies signals.target
just lattice-ci --require gaius,aegir,atelier,metabase   # whatever is enabled
  • PartOf=signals.target — stop/restart of the group propagates.
  • WantedBy=signals.target — enable/disable controls membership.
  • Do not enable a peer until peer-unit-spec accept is green.

Lattice CI

just lattice-ci
just lattice-ci --require gaius,aegir,atelier
just lattice-ci --all
just lattice-ci --json

Elevated CI gate — not part of signals-ready (critical plane only).


Checklist for a new peer

  1. Own lattice Postgres + gRPC ports (document in peer-contract.json).
  2. Vendor signals-protocol; implement zndx.engine.v1.Engine/Status (+ OIP path).
  3. Peer-tree scripts/systemd_{start,stop}.sh; unit points at them.
  4. After=signals-ready.service; never bind :5455 / :9010.
  5. Start blocks until lattice Status (and product health if required).
  6. Platform Metaflow / CE / YK when joining production federation.
  7. If license ≠ ASL2 → external tree only (Metabase pattern).
  8. just lattice-ci --require <id> green; then install-systemd --peers <id> --enable.

Peer Data Products (RustFS)

What another Signals project needs in order to formally maintain its own data product on the shared object plane. Contract: signals-protocol data products. Warehouse internals: Data Products History.

Signals runs the warehouse and RustFS. The peer runs the flow that produces the product and the ACP observation of what that flow retained. Gaius prospects is the next consumer of this shape.

1. Do not build a second warehouse

Peer doesPeer does not
Own {peer}.{domain}.{name}Create details / tx / hx tables
Write bytes to RustFSCopy product rows into Gaius/Ægir/Atelier Postgres
Point Metaflow at platform profileUse METAFLOW_DEFAULT_DATASTORE=local on the shared host
Emit dev.signals.dataproduct.updated + UUIDv7 tx_idMint v4 ids or rewrite a rejected id
Fill quality / lineage / delta / nominal on hx_*Re-inventory in the UI or a JSON file
Stamp a resource-class YK queueInvent root.gaius / root.aegir

devenv PostgreSQL 16 (:5455, AGE) is Signals admin (Atlas graph, Ranger). That is not pglite — pglite is Atelier AMP only. Peer PG ports stay engine-private. Never bind RustFS :9010.

2. Object plane

Lab: http://127.0.0.1:9010 (peer-contract.json → endpoints.rustfs_s3). In-cluster: http://signals-rustfs.metaflow.svc.cluster.local:9010.

Bucket / prefixUse
s3://metaflow/metaflow/Platform Metaflow CAS (code package, artifacts, env)
s3://signals-dataproducts/Iceberg warehouse + optional {peer}/… blobs

Copy config/metaflow/platform.json (or METAFLOW_PROFILE=platform):

{
  "METAFLOW_DEFAULT_METADATA": "service",
  "METAFLOW_SERVICE_URL": "http://127.0.0.1:30180",
  "METAFLOW_DEFAULT_DATASTORE": "s3",
  "METAFLOW_DATASTORE_SYSROOT_S3": "s3://metaflow/metaflow",
  "METAFLOW_S3_ENDPOINT_URL": "http://127.0.0.1:9010"
}

Fail closed if datastore is local, unset, a foreign bucket, or an endpoint that is not RustFS. Signals enforces this in signals.ops.metaflow_store.require_rustfs.

Product facts name the URI. A prospects parquet that landed at s3://signals-dataproducts/gaius/prospects/{run}/… is the object; the details row is the claim that it exists and is current.

3. Register the product

Stable id, peer prefix, YK leaf (resource class, not project):

gaius.prospects.<name>
peer: gaius
kind: corpus | cognition | snapshot | …
leaf: root.internal.inference.extract   # example — pick the class that matches the work

config/platform/data-products.json is a seed for a first INSERT, then retired. After that, SoR is the Impala views over Kudu + Iceberg. Ask Signals to seed once, or call signals.ops.history.record_event / review with the product dict.

Do not add a History row in signals-ui by editing HTML.

4. Map the Metaflow run into details

Each successful run is one tx (kind = updated | snapshot | peer kind, source = the peer). tx_id is UUIDv7 (Signals can mint; if the peer mints, it must be v7).

Map at least:

Fact (a)Typical v
peer, title, kind, leaf, agent_focusCatalog identity
spec / spec_idProduct specification (signals.spec.warehouse_product)
aspect.<aspect_id>SHACL binding (declared / conformant / failed / holding)
flow_name, run_id, pathspecMetaflow identity
snapshot_uri{sysroot}/{flow}/{run}
data_uri{sysroot}/{flow}/data (CAS)
code_package, code_package_shaImmutable source
deps@conda / @pypi / default
yk_app_id, yk_queueAdmission claim
object_store, datastore_root, rustfs_endpointMust be RustFS

Keep every retained version with run.{flow}/{run_id}.* so latest-wins does not erase prior snapshots. latest_* is the current run only.

Reference: signals.ops.metaflow_store.facts_from_run (first product signals.metaflow.snapshots).

# Signals-side record after a platform run (peer can wrap this)
uv run python -m signals.ops review-product gaius.prospects.corpus \
  --kind updated --summary 'prospects flow run 12'

Non-v7 tx_id → warehouse refuses → Engine/Remediate on Gaius (TX_ID_NOT_UUIDV7, capability=reauthor). Gaius remints and resubmits. Signals does not rewrite the id.

5. ACP assessment is part of the product

hx_reasoning on the same tx_id is not optional decoration. Empty quality / lineage / delta means History is a changelog.

For a prospects (or any operational) flow, the agent observes:

  1. Quality — is the retained object complete and on RustFS?
  2. Lineage — pathspec, code sha, YK app, Atlas OL job
  3. Delta — this run vs prior latest_run_id / last corpus pin
  4. Nominal — did the method stay on its legal path? Holding is in-progress, not failure and not done.

Walk data-product.history-review to understood when the observation is complete, failed when off-nominal. Do not treat a holding upkeep or a still-running curate as terminal.

Signals’ first product uses agent acp-observer for that walk. A Gaius prospects flow should pass its method doc (FSM current, inputs/outputs) into the same shape so an agent can say “proceeding nominally” without re-deriving the warehouse.

Peers do not implement data-product.tier-upkeep. That ADD / copy / verify / DROP RANGE PARTITION walk is Signals-owned. A peer flow that drops Kudu ranges is off-nominal by construction.

6. Queue and process

WorkQueue
Prospects GPU extract / OCRroot.internal.inference.extract
Instruct / short generationroot.internal.inference.instruct
Embeddingsroot.internal.inference.embedding
Platform Metaflow/Airflow itselfroot.platform
Grok ACP reviewroot.external.subscription.rate-limited

Stamp yunikorn.apache.org/queue, yunikorn.apache.org/app-id, federation.project=gaius (or aegir / atelier). Project is a label, not the queue parent. See Peer integration.

Schedule the flow with Airflow (airflow create or API trigger) on platform Metaflow. Publish dev.signals.dataproduct.updated (and optionally dev.metaflow.flow.finished) to the Knative Broker signals-events/default.

7. Gaius prospects (checklist)

  1. git submodule update --remote on signals-protocol (trunk after TX_ID_NOT_UUIDV7 + data-products spec). Regen engine stubs.
  2. Implement Remediate for TX_ID_NOT_UUIDV7 (remint v7).
  3. Flow uses platform Metaflow + RustFS profile — no local datastore.
  4. Choose gaius.prospects.* (or keep gaius.cognition.outputs if that is the product). Seed once via Signals.
  5. end step: map run → facts; record tx + details + hx; include the method walk so nominal can be scored.
  6. Objects live at s3://metaflow/metaflow/… and/or s3://signals-dataproducts/gaius/…. Facts store those URIs.
  7. Do not write prospects rows to Gaius PG :5444 as SoR.
  8. Do not DROP warehouse partitions. Do not bind :9010 or :5455.

8. Where to look in this tree

PiecePath
Contractcomponents/signals-protocol/specification/protocol/data_products.md
UUIDv7 + Remediate…/protocol/tx_id.md, SignalKind.TX_ID_NOT_UUIDV7
Seed catalogconfig/platform/data-products.json
Writersrc/signals/ops/history.py, src/signals/ops/warehouse.py
Snapshot mappingsrc/signals/ops/metaflow_store.py
Platform Metaflowconfig/metaflow/platform.json
Endpointsconfig/platform/peer-contract.json

Peer unit acceptance spec (shared handoff)

Template for completing local-to-peer-repo work (Gaius, Ægir, Atelier, Metabase, …) while preserving that project’s context. Signals owns the contract and lattice CI; peers own engine process + gRPC face + systemd wrappers.

Contract pin: config/platform/peer-contract.json (schema_version, commit).
Ops narrative (all peers): Peer integration.
Group control: infra/systemd/.
Lattice CI: just lattice-ci / scripts/lattice_ci.sh.
Doctrine: Total commitment as a federation peer — full engine capacity + honest start/stop/restart; lattice Status is not a quasi-engine substitute. Signals is early on its own engine axis (not “permanently thin”); peerhood still means total commitment to that project’s real engine and ops transitions.

Pattern source: Metabase landed first — peer wrappers wait for product health and Engine/Status; unit files call those scripts (no multiline shell). See common peer unit pattern.


Blank template (copy into peer work order)

Title: peer-unit@<id> lattice join
Contract: signals peer-contract.json schema_version=<X> @ <signals-git-sha>
Peer id: <id>
Repo path: <path_hint from contract>
Unit: <id>.service  (sample: signals infra/systemd/<id>.service)
gRPC port: <engine_grpc_lattice.<id>>
Postgres lattice: <pg_port or engine_pg_port>
Capability (Status): <capability or capability_hint>
License: <license>   external=<true|false>

Must:
  [ ] WorkingDirectory = path_hint (or documented override)
  [ ] scripts/systemd_start.sh + systemd_stop.sh in THIS tree
  [ ] Unit ExecStart/Stop → those scripts (absolute paths)
  [ ] After=signals-ready.service · WantedBy/PartOf=signals.target
  [ ] Start waits until Engine/Status on contract gRPC port
  [ ] gRPC **server reflection** enabled (`grpcio-reflection` or equivalent)
  [ ] Status.project matches contract (or project_status)
  [ ] Status advertises capability **honestly** (no synthetic always-healthy
      rows when backends are down — full-capacity doctrine)
  [ ] Unit starts the **full devenv stack** (`just up` / `devenv up`) for this
      project — capability engine **and** product UI/gateway/DB as applicable
      (not engine-only / lattice-only)
  [ ] Lattice engine is part of that stack (process-compose or equivalent)
  [ ] **Peer-scoped unit stop**: fully stop this project's stack; free lattice
      + product ports; do NOT host-wide teardown / gpu-deep-cleanup that kills
      siblings (not a weak stop)
  [ ] **Restart = full stop then full start** (orphans / multi-listener =
      error → remediate)
  [ ] No bind on signals :5455 or RustFS :9010
  [ ] If external/AGPL: no source/jar vendored into weathership/signals

Accept:
  [ ] systemctl start <id>.service → active (RemainAfterExit oneshot OK)
  [ ] systemctl stop <id>.service → lattice + product ports free; no leftovers
  [ ] systemctl restart <id>.service → single lattice listener; full stack; Status OK
  [ ] grpcurl -plaintext 127.0.0.1:<port> list   # includes zndx.engine.v1.Engine
  [ ] grpcurl -plaintext 127.0.0.1:<port> zndx.engine.v1.Engine/Status
  [ ] just lattice-ci --require <id>   # codegen Status + reflection check
  [ ] product UI/gateway health as documented for this peer
  [ ] peer remediation path on transition fail

Out of scope:
  - signals critical-plane changes
  - other peers' ports/units
  - Argo / Marquez DB / second Metaflow SoR

After accept (Signals operator):

just install-systemd --peers <id> --enable
sudo systemctl start signals.target   # not bare "signals"

Filled: gaius

Ops: Peer integration — Gaius
Product SoR (in Gaius tree): docs/current/src/operations/peer-unit.md

Title: peer-unit@gaius lattice join
Contract: signals peer-contract.json schema_version=1.0.0
Peer id: gaius
Repo path: ~/local/src/zndx/gaius
Unit: gaius.service  (sample: signals infra/systemd/gaius.service)
gRPC port: 50051
Postgres lattice: 5444   (db zndx_gaius)
Capability (Status): cognition  (capability_hint)
License: project-specific · external=false

Must:
  [x] WorkingDirectory = ~/local/src/zndx/gaius
  [x] scripts/systemd_start.sh + systemd_stop.sh (Metabase pattern)
  [x] Unit Exec* → those scripts; After=signals-ready.service
  [x] Wait until zndx.engine.v1.Engine/Status on :50051
      (native GaiusService + OIP already share this port — TCP listen ≠ Status)
  [x] Status.project ~ gaius; capability cognition advertised
  [x] PG only on :5444 — never :5455
  [x] Platform Metaflow URL when joining federation (not Tilt as SoR)
  [x] Peer-scoped unit stop = devenv processes down for this tree only
      (never host teardown / gpu-deep-cleanup that kill sibling GPU leases)

Accept:
  [x] Recycle via `systemctl restart signals.target` (lab 2026-08-13) + orphan :50051 cleanup
  [x] systemctl start/restart gaius.service → active (oneshot; Status body project=gaius)
  [x] grpcurl -plaintext 127.0.0.1:50051 list / Engine/Status (reflection)
  [x] just lattice-ci --require gaius     # elevated CI; reflection required
  [x] Group lifecycle (`systemctl` alone, 2026-08-13): stop reaps every
      Gaius-cwd devenv compose + frees :50051; start brings one listener in
      `system.slice/gaius.service` with reflection. `just lattice-ci --require
      gaius` PASS. Pin `XDG_RUNTIME_DIR` + reap-by-cwd (do not attach to a
      leftover login-shell compose).

Out of scope:
  - Metabase AGPL product, Ægir/Atelier internals
  - signals critical plane
  - Gaius-local Metabase :3100 as federation dashboard
  - Implementing Remediate on this face (Aegir :50151)
  - GPU-mesh write-up in src/gaius/engine/FEDERATION.md (not the accept gate)

Peer session focus (done): wrappers + third servicer zndx.engine.v1.Engine on the existing :50051 server (beside GaiusService

  • OIP). Health/FMEA stays Gaius-local (/health fix engine). Product notes: Gaius docs/current/src/operations/peer-unit.md — not FEDERATION.md (older KServe mesh).

Gaius-specific (lab):

  • Status is live at gRPC bind (engine init phase GRPC, ~1s). Do not wait for vLLM/endpoint load (~240s) to declare lattice ready.
  • just up / just down exist for the wrappers; restart-clean is a full product recycle, not the unit start path.
  • Two devenv process-compose daemons can both hold :50051 because gRPC defaults to SO_REUSEPORT (kernel load-balances Status). Engine now sets grpc.so_reuseport=0 and gaius-engine.sh refuses start if the port is taken (#EN.00000014.DUALBIND). devenv processes restart may no-op or spawn a second daemon (/tmp/devenv-<hash> vs $XDG_RUNTIME_DIR).
  • Gaius-local Metabase :3100 is not capability dashboard (AGPL peer :3200 / :50451).
  • Live 2026-08-13: one listener on :50051. grpcurl list shows zndx.engine.v1.Engine. just lattice-ci --require gaius PASS (codegen + reflection). Pin grpcio-reflection<1.82 — 1.83+ needs protobuf 7, which xai-sdk forbids.

Filled: aegir

Ops: Peer integration — Aegir
Copy into Aegir session as the work order.

Agent one-liner (full paths):

Implement peer-unit@aegir per
/home/rch/local/src/wxs/signals/docs/current/src/operations/peer-unit-spec.md
(section Filled: aegir) and
/home/rch/local/src/wxs/signals/docs/current/src/operations/peer-integration.md
(section Aegir / #aegir). Contract pin:
/home/rch/local/src/wxs/signals/config/platform/peer-contract.json.
Reference implementation:
/home/rch/local/src/zndx/gaius/docs/current/src/operations/peer-unit.md and
/home/rch/local/src/zndx/gaius/scripts/systemd_start.sh,
/home/rch/local/src/zndx/gaius/scripts/systemd_stop.sh,
/home/rch/local/src/zndx/gaius/scripts/zndx_status_ok.py.
Full-stack under signals.target (doctrine): unit = just up / devenv full graph including capability-engine + gateway + vite — not engine-only.

Title: peer-unit@aegir lattice join
Contract: signals peer-contract.json schema_version=1.0.0
Peer id: aegir
Repo path: ~/local/src/zndx/aegir
Unit: aegir.service  (sample: signals infra/systemd/aegir.service)
gRPC port: 50151
Product UI: gateway :8091 · vite :5173
Postgres lattice: 5555
Capability (Status): instruct  (project=aegir)
License: project-specific · external=false · architecture_class=core_federated_engine

Already present:
  [x] Multi-service on :50151 — native AegirEngine + zndx.engine.v1 + OIP
  [x] ZndxEngineServicer Status / Complete / Remediate
  [x] gRPC server reflection enabled

Must (full-stack remediation 2026-08-13):
  [x] scripts/systemd_start.sh + systemd_stop.sh — **just up / devenv full stack**
  [x] devenv process `capability-engine` on :50151 (not unit-only setsid)
  [x] Start waits on Status + gateway :8091 + vite :5173
  [x] Peer-scoped **full-stack** stop; no GPU wipe of siblings
  [x] Unit Exec* → wrappers; After=signals-ready · WantedBy=signals.target
  [x] docs/current/src/operations/peer-unit.md
  [x] PG only on :5555; never :5455 / :9010

Accept:
  [x] systemctl start/enable aegir.service → active under signals.target
  [ ] systemctl restart → full stack (UI + lattice); re-validate after remediation
  [x] grpcurl / lattice-ci --require aegir
  [ ] gateway :8091 + vite :5173 after unit start

Out of scope:
  - signals critical plane
  - requiring full vLLM cold-load for unit active (Status at gRPC bind is enough)

Implementation notes:

  • 2026-08-13 (earlier): engine-only unit — incorrect under total-commitment / full-stack doctrine; product UI stayed dark under signals.target.
  • 2026-08-13 (remediation): wrappers = Gaius/Metabase pattern (just up); capability-engine in devenv.nix; accept = Status + gateway + vite.
  • Dual-bind guard still required on :50151.

Filled: atelier

Ops: Peer integration — Atelier
Lattice green (engine-only) 2026-08-13; full-stack remediation required same day.

Title: peer-unit@atelier lattice join
Contract: signals peer-contract.json schema_version=1.0.0
Peer id: atelier
Repo path: ~/local/src/zndx/atelier
Unit: atelier.service  (sample: signals infra/systemd/atelier.service)
gRPC lattice: 50251
Product servicer: 50071 · gateway :8090 · vite :3000
Postgres lattice: 5533
Capability (Status): referee  (capability_hint)
License: project-specific · external=false

Must (full-stack remediation 2026-08-13):
  [x] scripts/systemd_start.sh + systemd_stop.sh — **just up / devenv full stack**
  [x] devenv process `capability-engine` on :50251 (+ existing grpc-server :50071)
  [x] Start waits on Status + :50071 + gateway :8090 + vite :3000
  [x] Peer-scoped **full-stack** stop; no GPU wipe of siblings
  [x] Unit Exec* → wrappers; After=signals-ready · WantedBy=signals.target
  [x] docs/current/src/operations/peer-unit.md
  [x] PG only on :5533; never :5455 / :9010

Accept:
  [x] systemctl start/enable atelier.service → active under signals.target
  [ ] systemctl restart → full stack; re-validate after remediation
  [x] grpcurl / lattice-ci --require atelier
  [ ] product :50071 + gateway :8090 + vite :3000 after unit start

Out of scope:
  - CAI single-tenant :50051 defaults on co-tenant hosts
  - signals critical plane
  - requiring vLLM cold-load for unit active

Implementation notes: engine-only unit was an oversight under full-stack doctrine. Both lattice (:50251) and product (:50071 + UI) belong under the unit via devenv.


Filled: metabase (AGPL external — isolated)

License-external by requirement (AGPL ↛ ASL2). mbengine is architecturally isolated from the core engine family (Gaius / Ægir / Atelier / synth / vigil). Process attachment may follow the same unit pattern; engine lineage does not. Operators: Peer integration — Metabase · Core vs external.

Title: peer-unit@metabase lattice join
Contract: signals peer-contract.json schema_version=1.0.0
Peer id: metabase
Repo path: ~/local/src/agpl/metabase   (or any path outside signals)
Unit: metabase.service  (sample: signals infra/systemd/metabase.service)
gRPC port: 50451
Engine PG: 5577 · dashboard HTTP: :3200
Capability (Status): dashboard  (required)
License: AGPL-3.0 · external=true

Must:
  [x] WorkingDirectory / ExecStart scripts = AGPL checkout only
  [x] scripts/systemd_start.sh + systemd_stop.sh (wait health+Status)
  [x] After=signals-ready.service · WantedBy/PartOf=signals.target
  [x] mbengine: zndx.engine.v1.Engine on :50451
  [x] Status.project=metabase · capability=dashboard · non-secret base_url
  [x] Never vendor Metabase into weathership/signals

Accept (lab host):
  [x] systemctl start metabase.service → active (or start signals.target)
  [x] grpcurl -plaintext 127.0.0.1:50451 zndx.engine.v1.Engine/Status
  [x] just lattice-ci --require metabase
  [x] GET http://127.0.0.1:3200/api/health

Out of scope:
  - ASL2 signals packaging of AGPL product
  - other engine ports

Operator one-liner: from signals tree, just install-systemd --peers metabase --enable then sudo systemctl start signals.target (not bare systemctl start signals).


Synth (stub)

idportpath_hintcapability_hint
synth50351~/local/src/zndx/synthsynthesis

Copy blank template when scheduled.


Signals-side responsibilities (not in peer session)

TaskRecipe / path
Foundation readyjust signals-ready · signals-ready.service
Group control samplesinfra/systemd/
Contractconfig/platform/peer-contract.json
Probe latticejust lattice-ci
Ops for all peerspeer-integration.md

Peers implement engines; signals verifies the lattice after they claim ready.

Storage root and stack backup

One path

just bootstrap           # Kerberos required (KDC, keytabs, kinit, FQDN env)
devenv up -d             # Impala + Kudu start only with Kerberos
just backup              # full portable snapshot of all services (GSSAPI Impala)
just restore <stamp>     # full portable restore + verify

There is no NOSASL fallback, no service menu, no Kudu physical mode, and no partial success. Bootstrap and stack processes require Kerberos.

Durable data plane

$SIGNALS_DATA_ROOT/          # default lab: /raid/signals  (user-chosen)
  kudu/                      # live Kudu FS — local to this host only
  rustfs/                    # RustFS volume (S3 API :9010; see governance-scale-plane)
  flink/
  backups/<stamp>/           # portable stamps only
  logical-restore/kudu/      # staged Parquet after just restore

Override once: SIGNALS_DATA_ROOT=/raid/signals in .env.

Scale: Atlas/Ranger bulk paths use Kudu projections (just gov-kudu-projections-seed); objects use RustFS on rustfs/ — not Postgres heap. Metaflow run snapshots (first data product) land in the metaflow bucket (s3://metaflow/metaflow); local Metaflow datastores are refused. Data-product details / tx / hx land in Kudu *_tier0, settle to Iceberg *_tier1 on that volume — no Postgres AGE copies. Doctrine: Governance scale plane, Data Products History.

What is portable

ServicePortable artifact
Atlas (signals PG + AGE)postgres/signals.dump
Rangerpostgres/ranger.dump (+ conf if installed)
Catalogpostgres/signals_catalog.dump
Polarisdump if DB exists, else polaris.ABSENT
Kudu tableslogical/kudu/*.parquet via DataFusion (not FS copy)
rustfs / flinkfilesystem/*.tgz or *.EMPTY

Kudu on-disk directories are never part of a successful stamp. Hostnames are embedded in tablet metadata; cross-host restore is logical only (Impala scan → Parquet on backup; DataFusion verify + stage on restore; CREATE/INSERT on the new cluster).

Stamp layout

$SIGNALS_BACKUP_DIR/<UTC-stamp>/
  MANIFEST.json          # path=full-portable-all-services, portable=true, status=ok
  SHA256SUMS
  postgres/
  logical/               # DataFusion package (signals-df)
    MANIFEST.json
    kudu/
  ranger-conf/
  filesystem/            # rustfs, flink only

Failed runs leave FAILED and status=failed — just restore refuses them.

Requirements

CommandNeeds
just backupKerberos ticket + GSSAPI Impala HS2 on FQDN; Postgres; Kudu; signals-df
just restorePostgres up; signals-df for logical verify; full ok stamp
just bootstrap           # required once per machine
devenv up -d
just kerberos-status     # expect: impala HS2 GSSAPI OK
just signals-df-build    # optional; also auto-built on backup

Impala and Kudu only run with Kerberos. Clients dial $SIGNALS_KRB_HOST (never 127.0.0.1). Logical Kudu export uses signals.impala / impala_query.py (GSSAPI only).

DataFusion

crates/signals-df owns the logical plane for every table-shaped service over time (logical/<service>/). Today Kudu export is Impala → CSV → Parquet; verify and SQL scale through DataFusion without Spark.

Atlas + OL

Atlas SoR is signals PG. Portable governance (+ future OL labels) rides in signals.dump. Marquez-web has no separate database.

Secrets (SecretSpec)

Signals uses SecretSpec with devenv integration so secrets are declared in-repo and provided per environment.

Layout

FileRole
secretspec.tomlDeclares secret names and descriptions — never values
devenv.yaml → secretspec:Enables SecretSpec (provider: dotenv, profile: default)
.env (gitignored)Local provider for dotenv; copy from .env.example
devenv.nix envNon-secret identity: realm, host, ports (KRB5_REALM, SIGNALS_KRB_ENV, …)

Kerberos + env segment

ConcernWhere
Realm DEV.VISTA.ZNDX.ORGdevenv.nix / KRB5_REALM (not a secret)
PG role signalsPrincipal primary via pg_ident
Keytab pathsSecretSpec: SIGNALS_KRB_USER_KEYTAB, POSTGRES_KRB_KEYTAB, …
Live TGTsRuntime (kinit / KRB5CCNAME) — not long-lived SecretSpec values

Align SecretSpec profile with SIGNALS_KRB_ENV when using multi-env:

ProfileTypical realm
default / devDEV.VISTA.ZNDX.ORG
stageSTAGE.VISTA.ZNDX.ORG
devenv --secretspec-profile dev shell
# or CI:
devenv --secretspec-provider env --secretspec-profile stage shell

Local workflow

cp .env.example .env          # edit as needed
just kdc-reset                # mints .devenv/kdc/*.keytab including signals.keytab
# optional in .env:
# SIGNALS_KRB_USER_KEYTAB=.devenv/kdc/signals.keytab
# POSTGRES_KRB_KEYTAB=.devenv/kdc/postgres.keytab

devenv shell                  # SecretSpec + dotenv enabled
secretspec run -- just tag default.my_table

Interactive lab uses kinit signals (password) or a user keytab — Kerberos is the expected path for signals users, not an optional add-on. Headless / FDW jobs prefer SIGNALS_KRB_USER_KEYTAB from SecretSpec.

Federation: Signals is the first adopter of Kerberos + SecretSpec. Sibling projects (engines, ACP agents) must follow the binding procedures in the signals-protocol submodule:

components/signals-protocol/specification/operations/kerberos_and_secretspec.md

That document covers principal catalog, shared secret names, keyring/sops providers, secretspec run + kinit wrappers, and Ranger onboarding—so every process that hits Impala/Kudu/Ranger has a principal without ambient shell secrets.

Declared secrets (summary)

See secretspec.toml for the full list. Groups:

  • Kerberos keytab paths — client + postgres/impala/kudu/HTTP service keytabs
  • LLM / Atlas — ANTHROPIC_API_KEY, CEREBRAS_API_KEY, SIGINT_ATLAS_PASSWORD, …

All Kerberos-related secrets are required = false so pure local kdc-init works without SecretSpec values.

Providers

ProviderUse
dotenvDefault local (.env)
envCI: secrets already in the environment
keyringPer-machine OS keyring
sops / othersShared encrypted stores when you add them
devenv --secretspec-provider env shell
secretspec run -- uv run pytest tests/sigint/ -v

Relation to HOCON

Pipeline config still flows CLI > env > config/base.conf. SecretSpec is how those env vars get into the process for secret-bearing runs; HOCON still reads ${?ANTHROPIC_API_KEY} etc. Do not put secrets in config/base.conf.

What not to put in SecretSpec

  • Realm / host / port (non-secret config)
  • Ticket cache contents (short-lived)
  • Keytab bytes in git (paths or encrypted providers only)

Services

PostgreSQL 16

Managed by devenv’s services.postgres, starts automatically with devenv up.

  • Database: signals (created automatically)
  • Extensions: Apache AGE (graph queries), pg_cron (scheduled jobs)
  • shared_preload_libraries: age,pg_cron
psql -d signals

Apache AGE

Graph query support via the AGE extension:

SELECT * FROM ag_catalog.cypher('my_graph', $$ MATCH (n) RETURN n $$) AS (n agtype);

pg_cron

Scheduled job support:

SELECT cron.schedule('nightly-job', '0 3 * * *', 'CALL my_procedure()');

Kerberos

A project-local MIT Kerberos KDC runs as a devenv process. Naming follows the ZNDX host taxonomy, with the environment segment in the realm (not only in DNS).

Expectation: users and services in this repo are expected to use Kerberos. Impala, Kudu, Postgres GSSAPI, and impala_fdw are designed around it. Full org-wide end-to-end security (including a future gRPC engine federation) is not required to land every feature, but components here should not invent a parallel long-term identity path. CI may use trust/nosasl for bootstrap only.

Federation adopters (Ægir, Atelier, Gaius, Hermes/ACP, …) follow the cross-project procedures in components/signals-protocol/specification/operations/kerberos_and_secretspec.md (SecretSpec allowlists, engine/agent principals, Ranger). This page remains the lab KDC / Signals-host reference.

Naming model

LayerValue
Org TLDzndx.org
Locationvista
Environment / posturedev → realm segment DEV
Host short nametinybox
Cloudflare / FQDNtinybox.dev.vista.zndx.org
Kerberos realmDEV.VISTA.ZNDX.ORG = {ENV}.{LOCATION}.ZNDX.ORG
User principalsignals@DEV.VISTA.ZNDX.ORG
PostgreSQL rolesignals (primary only; realm stripped via pg_ident)

Pattern:

  • FQDN: {host}.{env}.{location}.zndx.org
  • Realm: {ENV}.{LOCATION}.ZNDX.ORG (uppercase)
  • PG / app role: Kerberos primary only — env lives in the realm, not the role name

Examples:

PrincipalPG role
signals@DEV.VISTA.ZNDX.ORGsignals
signals@STAGE.VISTA.ZNDX.ORGsignals (different realm / KDC)
analyst@DEV.VISTA.ZNDX.ORGanalyst

Configuration

SettingValue
RealmDEV.VISTA.ZNDX.ORG
Principal instance (FQDN)tinybox.dev.vista.zndx.org
KDC port8848 (127.0.0.1)
Data directory.devenv/kdc/ (gitignored)
Env overridesKRB5_REALM, SIGNALS_KRB_ENV, SIGNALS_KRB_LOCATION, SIGNALS_KRB_HOST, KRB5_KDC_PORT
Keytab paths (secrets)SecretSpec: SIGNALS_KRB_USER_KEYTAB, POSTGRES_KRB_KEYTAB, … — see Secrets

kdc-init.sh builds the default realm from SIGNALS_KRB_ENV + SIGNALS_KRB_LOCATION unless KRB5_REALM is set explicitly. It also writes signals.keytab for headless/FDW use (optional SecretSpec path).

Local DNS

echo '127.0.0.1 tinybox.dev.vista.zndx.org' | sudo tee -a /etc/hosts

Principals

PrincipalTypeCredentials
postgres/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORGService (PG GSSAPI).devenv/kdc/postgres.keytab
impala/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORGService (Impala HS2).devenv/kdc/impala.keytab
HTTP/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORGService (Atlas/Ranger SPNEGO).devenv/kdc/http.keytab
kudu/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORGService (Kudu).devenv/kdc/kudu.keytab
signals@DEV.VISTA.ZNDX.ORGService principal for the Signals project (PG role + FDW outbound)Password: signals; .devenv/kdc/signals.keytab
signals/admin@DEV.VISTA.ZNDX.ORGAdmin (optional)Password: signals
zndx@DEV.VISTA.ZNDX.ORGUser principal. Semantica web login; the username is zndxPassword: zndx
semantica@DEV.VISTA.ZNDX.ORGService principal for the Semantica warehouse FDW.devenv/kdc/semantica.keytab (randkey)

End-to-end identity (impala_fdw)

Target: authenticate to Postgres with GSSAPI as signals@DEV.VISTA.ZNDX.ORG, map to PG role signals, then have impala_fdw use that same principal for Impala HS2 and Kudu. See components/impala_fdw/docs/SPEC.md §11.3.

Example pg_ident idea (also written to .devenv/kdc/pg_ident.map.example on init):

# signals@DEV.VISTA.ZNDX.ORG → signals
krb_map  /^(.*)@DEV\.VISTA\.ZNDX\.ORG$  \1

Usage

kinit signals          # default_realm is DEV.VISTA.ZNDX.ORG
# or: kinit signals@DEV.VISTA.ZNDX.ORG
klist
kdestroy

Management

devenv tasks run signals:kdc-init    # Initialize/verify KDC
devenv tasks run signals:kdc-reset   # Reset KDC database (required after realm renames)
# or: just kdc-reset

Migration from older realms

Prior checkouts used KRBTEST.COM or location-only VISTA.ZNDX.ORG. After pulling env-in-realm naming (DEV.VISTA.ZNDX.ORG):

devenv tasks run signals:kdc-reset
kdestroy   # discard old tickets

Headless browser (signals-ui verification)

UI changes must be exercised with a real browser, not only curl. Signals ships Playwright + Chromium for that.

What is installed

PieceSource
Python APIplaywright (uv dev group + optional extra browser)
Launchersignals.browser — resolves Chromium, fixes devenv/nix driver env
Chromiumdevenv package chromium on PATH, or SIGNALS_CHROMIUM_PATH, or Playwright-bundled after just browser-install
Node for driverdevenv nodejs_22 via PLAYWRIGHT_NODEJS_PATH (auto)

Recipes

just browser-install   # no-op when chromium already on PATH
just ui-verify         # queues lineup one-shot → build/ui-verify/*.png
just ui-test           # pytest tests/ui/ -m browser

just test ignores tests/ui/ (needs live UI). Unit suite stays hermetic.

Env

VariableRole
SIGNALS_UI_BASEdefault http://127.0.0.1:9889
SIGNALS_CHROMIUM_PATHforce Chromium binary
SIGNALS_BROWSER_HEADED=1show window
SIGNALS_BROWSER_SLOW_MO_MSPlaywright slow_mo
SIGNALS_UI_BROWSER_SKIP=1skip browser fixtures
SIGNALS_BROWSER_KEEP_LD_PATH=1do not clear LD_LIBRARY_PATH

devenv / nix note

Playwright ships its own Node. Under devenv, a nix LD_LIBRARY_PATH with a newer libstdc++ can break that Node (GLIBC_2.3x not found). signals.browser and just ui-* clear LD_LIBRARY_PATH and point the driver at system/devenv Node. Prefer system Chromium from devenv (packages = [ chromium … ]).

Code

from signals.browser import launch_chromium, sync_playwright, default_ui_base

with sync_playwright() as p:
    browser = launch_chromium(p)
    page = browser.new_page()
    page.goto(f"{default_ui_base()}/queues")
    page.locator("#queues-lineup-shell").wait_for()
    browser.close()

WASM Terminal

A Ghostty-based terminal compiled to WebAssembly. This surface belongs to the Gaius-era visualization track. The hub control plane is signals-ui on :9889.

User instructions in the terminal transit gRPC to a product engine and responses stream back to the client.

Design

The terminal provides a familiar command-line experience in the browser. Users type natural language instructions or structured commands, which are sent to the gRPC engine. The engine processes the instruction, potentially directing compute workloads, and streams responses back to the terminal.

This approach provides:

  • Zero install — runs entirely in the browser via WASM
  • Full terminal capabilities — Ghostty’s terminal emulation compiled to WASM
  • Bidirectional streaming — gRPC streams carry instructions and responses
  • Session persistence — terminal state survives page navigation

WASM Toolchain

The build toolchain (configured in devenv.nix):

ToolPurpose
wasmtimeWASM runtime for testing
wasm-packBuild Rust → WASM packages
wasm-bindgen-cliGenerate JS bindings
binaryenWASM optimization (wasm-opt)

Web Interface Layout

Instructions flow from the terminal through gRPC to the engine. The engine directs Dask/Datashader to recompute views, which are streamed to the visualization panel above the terminal.

Visualization Pipeline

HoloViews / Datashader / Dask rendering used by Gaius-era exploration UIs. Hub operators use signals-ui.

Stack

ComponentRole
HoloViewsDeclarative data visualization — the agent composes views from the HoloViews gallery
DatashaderServer-side rasterization for large datasets at viewport resolution
DaskDistributed parallel computation for on-demand view recomputation

Agent-Mediated Flow

The gRPC engine directs this pipeline. When users issue commands in the WASM terminal, the agent:

  1. Interprets the instruction and selects a visualization strategy
  2. Directs Dask to perform distributed computation
  3. Datashader rasterizes the result at the current viewport resolution
  4. Updated HoloViews visualizations stream back to the web client

Capabilities

Resolution Autoscaling

Datashader renders data at the pixel resolution of the current viewport. On zoom:

  • Dask recomputes the view in parallel over the narrowed data range
  • Detail increases as the viewport narrows
  • No client-side data transfer — only rasterized images transit the network

Multi-Persona Views

A single data product supports multiple persona conventions. The agent adjusts layout, metric selection, and visual conventions based on the active persona:

PersonaEmphasis
Data ScientistStatistical distributions, feature correlations
Domain ResearcherDomain-specific metrics, temporal patterns
Quantitative AnalystNumerical precision, confidence intervals
Operational AnalystSystem health, throughput, latency
Business AnalystKPIs, trends, executive summaries

MLOps Integration

The visualization pipeline supports:

  • Feature extraction and correlation views
  • Outlier detection and signal evaluation
  • Signal correlation across temporal, spatial, and spectral dimensions
  • Labeling workflows for different data modalities

SIGDG Ontology

The Signals Data Governance ontology (prefix SIGDG) is grounded in BFO 2020 (Basic Formal Ontology). BFO provides the upper-level categories; SIGDG specializes them for data governance.

BFO Grounding

BFO:0000031 generically dependent continuant
  └── SIGDG:0001 InformationEntity         ── "data that can be stored and transferred"

BFO:0000019 quality
  └── SIGDG:1001 SensitivityLevel          ── "inheres in an information entity"

BFO:0000023 role
  └── SIGDG:2001 DataSubjectRole           ── "externally grounded in data governance context"

BFO:0000015 process
  └── SIGDG:3001 DataLifecycleProcess      ── "transformation, migration, classification"

BFO’s generically dependent continuant (GDC) is the natural home for information entities — they depend on some material carrier (disk, memory) but can be copied and transferred between carriers. Sensitivity levels are qualities that inhere in information entities. Data subject roles are roles played by persons in a governance context.

CURIE Prefix

SIGDG → https://signals360.dev/ontology/dg/
BFO   → http://purl.obolibrary.org/obo/BFO_

Information Entity Hierarchy

SIGDG:0001  InformationEntity               (⊑ BFO:0000031)
├── SIGDG:0010  IdentityInformation         "data that identifies a natural or legal person"
│   ├── SIGDG:0011  GovernmentIdentifier    "state-issued ID: passport, SSN, driver's license"
│   ├── SIGDG:0012  PlatformIdentifier      "account-scoped ID: username, email, employee ID"
│   └── SIGDG:0013  DeviceIdentifier        "hardware-bound ID: IMEI, MAC address, UDID"
├── SIGDG:0020  PersonalInformation         "data about a natural person"
│   ├── SIGDG:0021  FinancialInformation    "monetary data: balances, scores, compensation"
│   │   └── SIGDG:0022  PaymentInformation  "payment instruments: card numbers, bank accounts"
│   ├── SIGDG:0023  DemographicInformation  "attributes: age, gender, ethnicity, education"
│   ├── SIGDG:0024  HealthInformation       "medical conditions, biometrics, genetic data"
│   ├── SIGDG:0025  ContactInformation      "addresses, phone numbers, email"
│   └── SIGDG:0026  CredentialInformation   "passwords, PINs, keys, security questions"
├── SIGDG:0030  BusinessInformation         "data about an enterprise"
│   ├── SIGDG:0031  ProprietaryInformation  "trade secrets, technical documentation"
│   └── SIGDG:0032  RegulatoryInformation   "SEC filings, compliance records"
├── SIGDG:0040  SystemInformation           "infrastructure and configuration data"
│   ├── SIGDG:0041  ConfigurationData       "IPs, cluster membership, file paths"
│   └── SIGDG:0042  AccessControlData       "permissions, LDAP groups, session state"
├── SIGDG:0050  TransactionInformation      "event records: orders, sessions, timestamps"
└── SIGDG:0060  TransformationMetadata      "provenance: encryption, masking, hashing records"

Sensitivity Levels

Sensitivity is modeled as a BFO quality (BFO:0000019) that inheres in an information entity:

CURIELabelDefinition
SIGDG:1010PublicIntended for unrestricted disclosure
SIGDG:1020InternalNot public but no harm if disclosed internally
SIGDG:1030ConfidentialDisclosure causes measurable harm to individuals or enterprise
SIGDG:1040RestrictedDisclosure causes severe harm; regulated by law (GDPR, HIPAA, PCI)

Data Subject Roles

Data subjects are modeled as BFO roles (BFO:0000023), allowing the same information entity to carry different sensitivity depending on whose data it is:

CURIELabelExample
SIGDG:2010CustomerRoleEnd-user, subscriber, app developer
SIGDG:2020EmployeeRoleEmployee, contractor, vendor
SIGDG:2030EnterpriseRoleCorporate entity, partner organization

A column tagged SIGDG:0025 ContactInformation with subject SIGDG:2010 CustomerRole may be Restricted, while the same column with subject SIGDG:2020 EmployeeRole may be Confidential.

Data Lifecycle Processes

Classification itself is a BFO process (BFO:0000015):

CURIELabelDefinition
SIGDG:3010ClassificationProcessAssigning SIGDG categories to an entity
SIGDG:3020MigrationProcessMoving data between storage tiers
SIGDG:3030MaskingProcessApplying de-identification transforms

Training Data

The classification model is trained on synthetic datasets spanning the SIGDG hierarchy:

TableSIGDG CategoryColumnsRows
identity_dataSIGDG:0010 IdentityInformation59100
personal_dataSIGDG:0020 PersonalInformation215100
business_dataSIGDG:0030 BusinessInformation21100
system_dataSIGDG:0040 SystemInformation43100
transaction_dataSIGDG:0050 TransactionInformation17100
metadata_infoSIGDG:0060 TransformationMetadata77100
annotations(ontology reference)1210

Each table’s columns serve as positive examples for their SIGDG category. The column names themselves (e.g., drivers_license_number, payment_card_number, trade_secrets) are the features the model learns to classify.

Configuration

Key Files

FilePurpose
devenv.nixPackages, services, processes, tasks, shell configuration
devenv.yamlNix inputs configuration
.envrcdirenv integration
.envRuntime environment variables (dotenv)
scripts/kdc-init.shIdempotent KDC initialization
pyproject.tomlPython project config (dependencies, dev tools)
TiltfileTilt dev iteration config (image builds, port forwards)

Infrastructure Configuration

FilePurpose
infra/aws/tofu/variables.tfOpenTofu variable definitions
infra/aws/tofu/terraform.tfvarsShared deployment defaults
infra/aws/ansible/group_vars/all.ymlAnsible shared variables
zarf/zarf.yamlAir-gap package definition
policy/tofu/*.regoOPA policies for OpenTofu plans
policy/k8s/*.regoOPA policies for Kubernetes

Environment Variables

Kerberos

VariableDefaultPurpose
KRB5_REALMDEV.VISTA.ZNDX.ORGKerberos realm {ENV}.{LOCATION}.ZNDX.ORG (non-secret)
SIGNALS_KRB_ENVdevEnv segment → DEV in realm
SIGNALS_KRB_LOCATIONvistaLocation segment → VISTA in realm
SIGNALS_KRB_HOSTtinybox.dev.vista.zndx.orgKerberos SPN host FQDN
SIGNALS_KRB_USER_KEYTAB(optional)SecretSpec: path to signals@… keytab
POSTGRES_KRB_KEYTAB(optional)SecretSpec: path to postgres SPN keytab

See Secrets and secretspec.toml for the full declared set. | KRB5_KDC_PORT | 8848 | KDC listener port | | KRB5_CONFIG | .devenv/kdc/krb5.conf | krb5 client config (set by shell) | | KRB5_KDC_PROFILE | .devenv/kdc/kdc.conf | KDC server config (set by shell) | | KRB5CCNAME | .devenv/kdc/krb5cc | Credential cache (set by shell) |

AWS / Cloudflare (set for deployment)

VariablePurpose
AWS_REGIONAWS region for provisioning
CLOUDFLARE_TUNNEL_TOKENTunnel credentials (from tofu output)
CLOUDFLARE_TUNNEL_IDTunnel identifier (from tofu output)

Project Structure

signals-360/
├── components/          # ASF submodules (atlas, ranger, kudu, ...)
├── docs/
│   ├── current/         # mdbook documentation
│   └── scratch/         # Work notes
├── features/            # BDD feature specifications
│   ├── agent/           # S01-S03 scenarios
│   ├── analytics/       # S04-S06 scenarios
│   └── platform/        # Infrastructure scenarios
├── infra/               # Deployment infrastructure
│   └── aws/             # OpenTofu + Ansible
├── policy/              # OPA Rego policies
├── scripts/             # Shell scripts (kdc-init)
├── tilt/                # Tilt dev overlays
├── zarf/                # Air-gap packaging
├── devenv.nix           # Development environment
├── pyproject.toml       # Python project config
└── Tiltfile             # Tilt configuration

Research Roadmap: Evidence Fusion

This roadmap converts the findings from the DST domain-expert readiness audit into a structured research plan. Each work item has defined acceptance criteria, estimated scope, and priority based on impact to external credibility. The goal is to make the evidence fusion layer defensible under scrutiny from domain experts in uncertainty quantification, Dempster-Shafer theory, and applied machine learning.

Priority Classification

PriorityMeaningGate
P0Blocks domain-expert review — will undermine credibility if unaddressedMust resolve before any external presentation
P1Will draw significant questions — defensible answers requiredShould resolve before peer review
P2Strengthens the contribution — good practiceAddress as capacity allows

Work Items

R-01: Source Independence Analysis (P0)

Problem. Dempster’s rule assumes independent evidence sources. The cosine and CatBoost sources both consume the same sentence-transformer embedding vector — they are fundamentally not independent. Name matching partially overlaps with cosine (the column name is part of the embedding text). This violates the theoretical precondition for Dempster combination and causes over-reinforcement of shared signals, producing overconfident belief intervals.

Impact. A DST expert will identify this in the first 5 minutes. It is the single most important issue.

Approach options:

  1. Merge cosine + CatBoost into one source. Treat them as a single “embedding-based” evidence source. CatBoost subsumes cosine when a trained model is available; cosine is the fallback when no model exists. This eliminates the dependence at the cost of reducing from 4 sources to 3 (or 2 when CatBoost is loaded).

  2. Decouple the feature spaces. Train CatBoost on value-only embeddings (stripping column name, table, siblings) while cosine uses the full embedding. This creates genuinely different input representations, though residual correlation from shared value content remains.

  3. Add an architecturally independent source. (Implemented — always-on.) The SVM source (svm_to_mass()) operates on sparse TF-IDF features (character 3–6 grams + word bigrams) with no dependency on the sentence-transformer embedding. The main pipeline trains SVM inline on synthetic data and injects it before classification. Standalone accuracy: 84.6%. A validation script (scripts/svm_pilot.py) measures source correlation and impact on fused belief intervals. See SVM Short-Text Classification in the Evidence Fusion architecture.

  4. Switch to a cautious combination rule. Replace Dempster’s rule with Denoeux’s cautious rule [Denoeux, 2008] or Yager’s modified rule [Yager, 1987], which do not require source independence. The cautious rule uses the least-commitment principle — it produces weaker but more honest intervals.

  5. Quantify and document the dependence. Measure the correlation between cosine and CatBoost mass functions empirically. If correlation is low (which is plausible — CatBoost learns nonlinear patterns cosine cannot capture), document the argument that “weak dependence does not invalidate Dempster combination in practice” with supporting data.

Acceptance criteria:

  • Empirical correlation measurement between cosine and CatBoost mass functions on the evaluation set
  • Selection and implementation of the chosen approach (with rationale document)
  • Updated belief intervals on the evaluation set with comparison to current results
  • Section in evidence-fusion.md documenting the independence assumption and its treatment

Estimated scope: 2-3 sessions


R-02: Constant Calibration Experiment (P0)

Problem. The implementation contains 13 hardcoded constants that control evidence weighting. None have empirical justification:

ConstantLocationCurrent ValueRole
Cosine discountcosine_to_mass()0.30Fraction of cosine mass → \(\Theta\)
CatBoost base discountcatboost_to_mass()0.15Fraction of CatBoost mass → \(\Theta\)
CatBoost max discountcatboost_to_mass()0.50Maximum variance-adjusted discount
Variance scale factorcatboost_to_mass()1.60Maps variance [0,0.25] → discount [0.1,0.5]
Pattern evidence masspattern_to_mass()0.90Mass on matched pattern categories
Pattern \(\Theta\) masspattern_to_mass()0.10Residual mass to \(\Theta\)
Name exact match massname_match_to_mass()0.70Mass for exact column→category match
Name abbrev match massname_match_to_mass()0.50Mass for abbreviation match
Name overlap match massname_match_to_mass()0.30Mass for multi-word overlap match
Uncertainty gap thresholdneeds_clarification0.30\(Pl - Bel\) above which clarification is flagged
Conflict thresholdneeds_clarification0.20\(K\) above which clarification is flagged
Softmax temperaturecosine_to_mass()1.0Temperature for cosine→probability conversion
Variance base discountcatboost_to_mass()0.10Minimum discount with variance data

Impact. An expert will ask “where do these numbers come from?” The answer cannot be “intuition.”

Approach:

  1. Grid search on held-out set. For each constant, sweep a range of values while holding others fixed. Measure accuracy, calibration error (ECE), and mean uncertainty gap on a held-out portion of the evaluation set.

  2. Bayesian optimization. Use Optuna or similar to jointly optimize the constants against a combined objective: accuracy + calibration + uncertainty gap separation (clarification-needed columns should have higher gaps than confident columns).

  3. Sensitivity analysis. For each constant, compute \(\partial(\text{accuracy}) / \partial(\text{constant})\) and \(\partial(\text{ECE}) / \partial(\text{constant})\) to identify which constants matter and which are inert.

Acceptance criteria:

  • Sensitivity analysis showing which constants significantly affect accuracy and calibration
  • Optimized values with confidence intervals from cross-validation
  • Calibration plot (reliability diagram) before and after optimization
  • Constants extracted to a configuration dataclass with documented defaults and rationale
  • Reproducible calibration script in scripts/

Estimated scope: 2-3 sessions


R-03: Conflict Metric Correction (P1) — ✓ Complete

Problem. The conflict metric must use cumulative \(K\) across all Dempster combination steps, not a single pairwise value.

Resolution. dempster_combine() returns (BeliefAssignment, K) (belief.py:105). combine_multiple() computes cumulative \(K = 1 - \prod_i (1-K_i)\) via Smarandache & Dezert (2005) (belief.py:146-156). The cumulative K threads through from_combined_evidence() → HierarchicalClassification.conflict. Tests verify exact K values for known conflict scenarios (test_returns_conflict_value, test_combine_multiple_cumulative_k).

Acceptance criteria:

  • dempster_combine() returns (BeliefAssignment, float) tuple where the float is \(K\)
  • combine_multiple() returns cumulative \(K = 1 - \prod_i (1-K_i)\) (Smarandache & Dezert, 2005)
  • _compute_conflict() removed or replaced
  • All existing tests updated and passing
  • New tests verifying \(K\) for known conflict scenarios

R-04: CatBoost Mass Normalization (P1) — ✓ Complete

Problem. catboost_to_mass() and svm_to_mass() filter proba entries by checking if code in frame.singletons. When the class set diverges from the frame’s leaf set, the filtered probabilities plus the fixed discount no longer sum to 1.0, producing an invalid mass function.

Resolution. After computing singleton masses, residual probability from dropped codes is allocated to \(\Theta\):

assigned = sum(masses.values())
masses[frame.theta] = discount + max(0.0, evidence_mass - assigned)

Applied to both catboost_to_mass() and svm_to_mass() in mass_functions.py. Three new tests verify valid mass functions with mismatched class sets.

Acceptance criteria:

  • catboost_to_mass() always produces a valid mass function (sum = 1.0)
  • svm_to_mass() always produces a valid mass function (sum = 1.0)
  • Test with mismatched class sets (3 new tests)
  • BeliefAssignment.is_valid assertion in all mass function tests

R-05: Cautious Hierarchical Classification (P1)

Problem. The pignistic transform always commits to a leaf singleton. When evidence supports a broader category but is ambiguous at the leaf level, the classifier still picks a specific leaf — even when \(Bel(\text{leaf}) \ll Bel(\text{parent})\). This discards the hierarchy’s value.

Impact. The “belief at every hierarchy level” data is available but not used for decision-making. A domain expert will ask: “why compute hierarchy-level intervals if you always commit to a leaf?”

Approach: Implement cautious_classify() that returns the deepest hierarchy node where \(Bel(A) > \tau\) (configurable threshold). This follows Denoeux & Zouhal (2001):

In this example, cautious classification would return IdentityInformation (0010) — the deepest node where \(Bel > 0.5\) — rather than guessing TaxIdentifier (0085).

Acceptance criteria:

  • cautious_classify() method on HierarchicalClassification
  • Configurable threshold (default 0.5)
  • Returns (code, depth, bel, pl) tuple
  • Evaluation: compare leaf accuracy vs. cautious accuracy at varying thresholds
  • Tests covering: confident→leaf, uncertain→parent, very uncertain→root

Estimated scope: 1-2 sessions


R-06: Pattern Evidence Frequency Weighting (P2)

Problem. pattern_to_mass() treats pattern detection as binary — a pattern is either detected or not. In practice, the pattern detectors fire when ≥1/3 of sample values match. A column where 95/100 values match the email regex provides much stronger evidence than one where 35/100 match.

Approach: Pass match ratios (not just boolean flags) from ColumnFeatures.pattern_signals and scale mass accordingly:

\[m(\{c\}) = \text{base_mass} \times \text{match_ratio}^{0.5}\]

The square root prevents low-ratio matches from contributing negligible mass.

Acceptance criteria:

  • ColumnFeatures provides match ratios alongside pattern names
  • pattern_to_mass() accepts and uses match ratios
  • Test with varying match ratios showing monotonic mass increase

Estimated scope: 1 session


R-07: Name Match Ambiguity Handling (P2)

Problem. name_match_to_mass() uses a greedy best-match strategy. When multiple categories match at the same tier (e.g., “address” matches both BillingAddress and ShippingAddress), only one is selected. The correct DST treatment is to assign mass to the union \(\{BillingAddress, ShippingAddress\}\), representing the evidence that “it’s one of these, but I can’t tell which.”

Approach: When multiple categories match at the same tier, create a composite focal element for their union. This requires the union to be in the restricted focal set — add it as a confusable pair or ad-hoc focal element.

Acceptance criteria:

  • Multi-match detection in name_match_to_mass()
  • Mass assigned to union focal element when multiple matches exist
  • Test cases for ambiguous names (“address”, “identifier”, “data”)

Estimated scope: 1 session


R-08: Confusable Pairs Activation (P2)

Problem. The FrameOfDiscernment accepts confusable_pairs at construction, but the pipeline never passes any. The 16 remaining classification errors cluster in ~6 category pairs (ADID/GUID, BAN/PAN, Under13/Under18, Billing/Shipping address, security flaw subtypes). These are exactly the confusable pairs the mechanism was designed for.

Approach: Define the known confusable pairs from error analysis. When evidence is ambiguous between members of a pair, mass goes to the pair focal element rather than forcing a leaf choice.

Acceptance criteria:

  • Confusable pairs defined from error analysis
  • Passed to FrameOfDiscernment constructor in the pipeline
  • At least one mass function produces mass on pair focal elements
  • Evaluation showing confusable pairs reduce false precision on known error categories

Estimated scope: 1 session


R-09: Uniform Discounting Framework (P2)

Problem. Each mass function converter implements discounting differently — some as a parameter, some hardcoded, some variance-adaptive. There is no uniform discount(ba, alpha) operation.

Approach: Implement a standard discounting operation [Shafer, 1976, §11]:

\[m_\alpha(A) = (1-\alpha) \cdot m(A) \quad \text{for } A \neq \Theta\] \[m_\alpha(\Theta) = \alpha + (1-\alpha) \cdot m(\Theta)\]

Each converter produces a full-confidence mass function, then discounting is applied uniformly as a separate step. This separates the evidence encoding from the reliability weighting.

Acceptance criteria:

  • discount(ba, alpha) utility function in belief.py
  • All 4 converters refactored to use it
  • Discount factors externalized to configuration
  • Existing tests pass with equivalent results

Estimated scope: 1 session


R-10: Virtual Ensembles Integration (P2)

Problem. CatBoost’s virtual ensembles API (get_virtual_ensembles_predictions) provides per-class prediction variance — a direct measure of model uncertainty. The catboost_to_mass() function accepts this variance but it is never provided by the pipeline because classify_dst() doesn’t call the virtual ensembles API.

Approach: When a CatBoost model supports virtual ensembles, call get_virtual_ensembles_predictions() in classify_dst() and pass the variance to catboost_to_mass().

Acceptance criteria:

  • Virtual ensemble predictions extracted when CatBoost model supports it
  • Per-class variance passed through to catboost_to_mass()
  • Test with mock virtual ensemble output
  • Comparison: fixed discount vs. variance-adaptive discount on evaluation set

Estimated scope: 1 session


R-11: Confidence-Gated Adaptive Discounting (P1)

Problem. Cross-benchmark analysis revealed three cosine reliability regimes: high confidence (>0.35, near-perfect), low confidence (<0.05, destructive), and intermediate. The current pipeline uses fixed discounts regardless of regime. On GitTables (all generic names), cosine evidence is near-random (1.6% accuracy) but still receives 70% of its mass, creating systematic conflict \(K = 0.65\) that overwhelms CatBoost’s 81.6% accuracy.

Impact. A 10+ percentage point accuracy drop from evidence fusion (71.4% fused vs. 81.6% CatBoost-only on GitTables) is not acceptable. The regime analysis is documented but not operationalized.

Approach: Implement per-column adaptive discounting based on the cosine similarity score of the top prediction. When the maximum cosine similarity is below a threshold, increase the cosine discount toward 1.0 (vacuous). When it is high, decrease the discount toward 0.0 (full trust). This converts the three-regime observation into an operational improvement.

\[d_{cosine} = \begin{cases} 0.10 & \text{if } \max(sim) > 0.35 \text{ (high confidence)} \\ 0.30 & \text{if } 0.05 \leq \max(sim) \leq 0.35 \text{ (intermediate)} \\ 0.90 & \text{if } \max(sim) < 0.05 \text{ (low confidence)} \end{cases}\]

Acceptance criteria:

  • cosine_to_mass() accepts adaptive discount mode
  • Per-column discount based on max similarity score
  • GitTables accuracy improves (target: CatBoost-only 81.6% maintained under fusion)
  • SIGDG benchmark accuracy maintained ≥ 95% (self-train mode already achieves 99.4%)
  • Regime boundaries tuned on held-out data (not hardcoded)

Estimated scope: 1 session


R-12: GPU-Accelerated Calibration Pipeline (P2)

Problem. The calibration experiment (R-02) requires sweeping 13 constants across a multi-dimensional space. With 6x RTX 4090 GPUs active (driver 570.148.08, CUDA 12.8, PyTorch 2.10.0+cu128), the calibration loop should exploit GPU parallelism for SAGE computation and SentenceTransformer encoding. CatBoost multiclass requires CPU for posterior_sampling and rsm (GPU drops these, losing 2% accuracy).

GPU validation (2026-03-24): Full pipeline completed in 76 min (vs 5+ hours CPU-only). SAGE achieved 27x speedup (489s GPU vs 13,278s CPU). CatBoost forced to CPU for accuracy; GPU used for SentenceTransformer encoding and SAGE.

Approach: Build scripts/calibrate_constants.py using Optuna. Each trial evaluates accuracy + ECE + uncertainty separation. SAGE and embedding encoding on GPU (27x speedup). CatBoost training on CPU with posterior_sampling=True. Multi-trial parallelism via Optuna’s n_jobs.

Acceptance criteria:

  • Optuna-based calibration script with GPU CatBoost
  • Joint optimization of accuracy + ECE + uncertainty gap separation
  • Reliability diagram (calibration plot) before and after optimization
  • Constants extracted to HOCON config with documented rationale
  • Multi-GPU parallelism (1 trial per GPU)

Estimated scope: 2 sessions


Completed Items

ItemStatusKey Result
R-01 option 3 (SVM source)DoneSVM always-on as 5th DST source. 84.6% standalone accuracy. TF-IDF features are architecturally independent from dense embeddings.
R-03 (conflict metric)Donedempster_combine() returns K; combine_multiple() computes cumulative K via Smarandache & Dezert. Tests verify exact values.
R-04 (mass normalization)Donecatboost_to_mass() and svm_to_mass() allocate residual from dropped codes to Theta. 3 new tests.
GPU preflight validationDonepreflight_gpu() validates driver/CUDA compat. 6x RTX 4090 active (driver 570.148.08, CUDA 12.8). SAGE 27x speedup (489s GPU vs 13,278s CPU). CatBoost forced to CPU (posterior_sampling not supported on GPU).
SHAP explanationsDonePer-item CatBoost TreeSHAP with top-3 feature attribution in parquet output.
Single-command pipelineDone--auto-generate trains SVM + CatBoost inline on synthetic data. Zero manual steps.
Self-training modeDone--self-train injects GT-labeled eval data into CatBoost training. 99.4% accuracy (348/350). LLM annotation reproduction workflow. SVM text format fix: 60.6% → 84.6% DST accuracy.
LLM bootstrap agentDoneK-based convergent classification for novel tables. Dual backend (Anthropic + OpenAI-compatible). Tiered sampling, label propagation, LLM revisit with ML context. llm_to_mass() as 6th DST source. Output feeds --self-train.

Dependency Graph

Critical path: R-01 (partial ✓) + R-03 (✓) + R-04 (✓) + R-11 → R-02 (R-12 accelerates) → R-05. Three P0/P1 prerequisites are resolved. The remaining critical path is: implement adaptive discounting (R-11), then run the GPU-accelerated calibration experiment (R-02/R-12). Cautious classification (R-05) builds on calibrated intervals.

Evaluation Protocol

All work items should be evaluated against the same protocol for consistency:

  1. Accuracy: Classification accuracy on the GT-labeled evaluation set (current best: 83.1% CatBoost benchmark, 84.6% SVM, 99.4% with self-training; benchmark target ≥ 95%)
  2. Calibration: Expected Calibration Error (ECE) — do belief intervals track true accuracy?
  3. Uncertainty separation: Do columns flagged needs_clarification genuinely have higher error rates?
  4. Conflict utility: Does high \(K\) correlate with misclassification? (ROC-AUC of \(K\) as a misclassification predictor)
  5. Hierarchy utility: At what belief threshold does cautious classification achieve 99% accuracy? How deep does it commit on average?

References

  • Denoeux, T. (2008). Conjunctive and disjunctive combination of belief functions induced by nondistinct bodies of evidence. Artificial Intelligence, 172(2-3), 234-264.
  • Denoeux, T. & Zouhal, L.M. (2001). Handling possibilistic labels in pattern classification using evidential reasoning. Fuzzy Sets and Systems, 122(3), 409-424.
  • Shafer, G. (1976). A Mathematical Theory of Evidence. Princeton University Press.
  • Smets, P. & Kennes, R. (1994). The Transferable Belief Model. Artificial Intelligence, 66(2), 191-234.
  • Smarandache, F. & Dezert, J. (2005). Information fusion based on new proportional conflict redistribution rules. Proceedings of Fusion 2005.
  • Yager, R.R. (1987). On the Dempster-Shafer framework and new combination rules. Information Sciences, 41(2), 93-137.

Roadmap

Current State

Core Query Stack (complete)

HMS-free Impala + Kudu is operational. Product default is Kudu-only / no-HDFS (no HDFS/YARN as services). Full table lifecycle without Hive Metastore, HDFS, or HBase:

  • Impala built from source (statestore, catalogd, impalad)
  • Kudu master + tablet server running
  • PostgreSQL catalog registry replacing HMS for table metadata
  • DDL: CREATE/DROP DATABASE, CREATE/DROP TABLE
  • DML: INSERT, SELECT with JOINs, aggregations, subqueries
  • Data types: INT, STRING, DOUBLE, BOOLEAN, BIGINT

Atlas Metadata Integration (in progress)

Atlas with the AGE backend is running and the catalog bridge is validated:

  • Atlas AGE backend operational — PostgreSQL + AGE replacing JanusGraph/HBase/Solr
  • Catalog bridge — Python function registers Impala-managed Kudu tables in Atlas via REST API
  • Entity CRUD — Atlas RDBMS model (rdbms_*, same as Aegir); product bridge no longer registers hive_*
  • Classification CRUD — custom classification types, table/column tagging, search by classification
  • BDD coverage — 74 scenarios across 16 features (40 tier-0 classification + 22 tier-1 health + 12 tier-1 integration), all passing

Infrastructure (complete)

  • devenv environment with PostgreSQL (AGE, pg_cron, pg_trgm), Kerberos KDC
  • HMS, Polaris, Kudu processes defined in devenv.nix
  • Atlas with AGE backend (replacing JanusGraph/HBase/Solr)
  • ASF component submodules on shared rch/devenv branches (rch/asf-*)
  • BDD feature specifications across 16 features with tier-0 and tier-1 coverage
  • Air-gap isolation (HF_HUB_OFFLINE=1, model cache, zero external network calls)
  • CI workflow for catalog unit + integration tests
  • mdbook documentation with GitHub Pages deployment

Near-Term Objectives

1. Hot/Warm Data Lifecycle (keystone capability)

The core value proposition of the stack: Kudu for upsert-heavy hot-tier ingest, Iceberg for warm-tier storage as upserts taper off, and Impala providing transparent queries across both tiers.

  • End-to-end Iceberg table creation through Polaris REST catalog
  • SELECT queries on Iceberg tables via Impala
  • Kudu → Iceberg data migration via CTAS
  • Lifecycle validation workload (tests/workload/lifecycle.py) — hot→warm transition
  • Polaris catalog integration with the catalog registry

The IcebergRESTCatalog class already implements createTable, dropTable, and renameTable against the Polaris API. The remaining work is integration testing with a running Polaris instance and validating the full hot→warm lifecycle: data ingested into Kudu, aged via CTAS into Iceberg, queryable transparently through Impala.

2. Entity Type Evolution (rdbms_*, Aegir-aligned)

Stack vocabulary (do not collapse these):

LayerRoleWhat we call a “table”
PostgreSQLFrontend / FDW / RLSForeign table (e.g. atlas_entity_flat)
ImpalaSQL engine only; HMS-free; Kudu (and later Iceberg) onlyImpala name db.table → physical impala::db.table in Kudu
KuduStoragePhysical tablet table (kudu_table FDW option when needed)
AtlasGovernance metadataEntity types — not storage engines

Hive is not a product storage or query tier out of the gate (nothing against Hive; it simply is not in this stack). Atlas still ships Hadoop bootstrap types; signals product registration does not create them.

Atlas type rule (Aegir parity): Use the stock RDBMS model that ships with Atlas (addons/models/2000-RDBMS/):

TypeRole
rdbms_instanceCluster / connection (e.g. Impala+Kudu lab instance)
rdbms_dbDatabase / schema
rdbms_tableTable (governance entity for a signals-managed table)
rdbms_columnColumn
rdbms_index / rdbms_foreign_keyOptional structural metadata

Relationships: rdbms_instance_databases → rdbms_db_tables → rdbms_table_columns (COMPOSITION).

PhaseEntity TypesStatus
Phase 1 (historical)hive_table, hive_column, hive_dbBootstrap proof only — retired for product registration
Phase 2 (current)rdbms_table, rdbms_column, rdbms_db, rdbms_instanceLanded — catalog bridge, BDD steps, sigint.atlas_client (Aegir-aligned; cross-ref Aegir Atlas :21000)
Phase 3 (optional)Storage facets or tags (kudu / iceberg tier) on rdbms_*Future — hot→warm lineage without inventing Hive types

Why this matters: Product path is Postgres front → Impala SQL → Kudu storage. Atlas classifies those assets with the relational RDBMS model, not a Hive warehouse model. Aegir may still retain legacy hive_* entities; signals product code does not create them.

3. Atlas-Impala Catalog Bridge

Evolve the catalog bridge from a test helper to a production integration:

  • Bridge function registers Impala tables in Atlas via REST API
  • Classification CRUD — create types, apply to tables and columns
  • Column-level tagging — individual column classification
  • Automated registration — hook or event-driven (replace manual bridge calls)
  • Lineage tracking for INSERT…SELECT and CTAS operations
  • Search-by-classification in AGE backend (full-text search on classification names)

4. AI/ML Metadata Tagging

Automatic classification of table and column names using the SIGDG ontology (BFO-grounded data governance vocabulary):

  • Classification model trained on the SIGDG information entity hierarchy (SIGDG:0010–0060)
  • Training data: 6 categories (identity, personal, business, system, transaction, metadata)
  • Sensitivity levels assigned as BFO qualities (Public → Internal → Confidential → Restricted)
  • Context engineering pipeline with 12 ablatable features and SAGE importance analysis
  • Multi-stage pipeline (feature extraction → classification → run report + SAGE)
  • Structured run reports (JSON + parquet) for method/feature comparison
  • Tagging service prototype (Python) — Tagger class orchestrates sample → classify → tag
  • Write classifications back to Atlas as SIGDG tags (via AtlasClient.apply_classification())
  • Air-gap isolation — local model cache, HF_HUB_OFFLINE=1, zero external calls
  • BDD validation — 6 meta-tagging + 2 pipeline scenarios, all passing
  • OWL formalization of SIGDG with BFO 2020 imports
  • Event-driven tagging — watch Atlas for new entities (replace manual invocation)

See Metadata Tagging for the architecture and Context Engineering for the SAGE-based feature evaluation methodology. The SIGDG Ontology reference documents the full BFO-grounded vocabulary.

5. Ranger Tag-Based Policies

Connect Atlas classifications to query-time enforcement:

  • Ranger policies reference Atlas classification tags
  • Automatic column masking for PII-tagged columns
  • Row-level filtering based on data subject classification
  • Audit logging of access to classified data

Scenario Tiers

Scenarios are organized by implementation tier:

TierFocusPrerequisites
Tier 0Component healthNone
Tier 1Query stack, metadata catalog, classificationImpala + Kudu + Atlas
Tier 2gRPC engine, extension deployment, self-improvementEngine binary
Tier 3Full visualization pipeline, analytics scenariosEngine + Dask + Datashader

Eliminated Dependencies

The following components are intentionally excluded from the runtime stack:

ComponentReplacementReason
Hive MetastorePG catalog registry + KuduMetaProviderEliminate Thrift dependency, simplify deployment
HDFSKudu (primary); Iceberg/object later; rustfs / Ceph for future enginesNot a storage tier we run; product default is Kudu-only / no-HDFS
HBasePostgreSQL + AGEAtlas backend simplified to single database
SolrPostgreSQL full-text searchAtlas search simplified
ZooKeeperKudu’s built-in Raft consensusNo external coordination service
TrinoImpalaKudu connector removed from Trino 473
YARN / MapReduce—Not part of interactive SQL + Kudu path

Note: Upstream Impala still builds against Hadoop client/native libraries (libhdfs, jars). That is temporary link-time debt until the rch/devenv default is a no-HDFS Impala build — not a commitment to operate Hadoop.

Access slim-down: product default is FDW-only / no-JDBC — apps and agents reach Kudu data through Postgres + impala_fdw, not a first-class Impala JDBC surface. HS2 stays internal to the FDW path until that is the only consumer API we optimize for.

Backlog

The following capabilities are documented as BDD feature specifications in features_archive/ and will be revisited once the core platform is fully validated.

Agent + Visualization (S01–S03)

  • S01: Agent-Mediated Visualization — Interactive data exploration through a WASM terminal with resolution autoscaling and multi-persona views. Requires the gRPC engine, Dask distributed compute, and HoloViews/Datashader rendering.
  • S02: Algorithm Extension — Package, deploy, and invoke custom analysis code as platform extensions through the agent engine.
  • S03: Agent Self-Improvement — Define performance objectives and iterate toward quantitative improvements via self-directed evaluation cycles.

Analytics (S04–S06)

  • S04: OTel Root Cause Analysis — Correlate degradation signals with infrastructure changes for evidence-backed root cause analysis using OpenTelemetry data.
  • S05: Cybersecurity Investigation — Investigate malicious behavior using correlated log sources and detection logic.
  • S06: Streaming Ontology — Ontology-grounded feature discovery from high-velocity streams with human-in-the-loop direction.

gRPC Engine

  • gRPC engine health checks, instruction echo, extension registry. Archived from features/platform/grpc_engine.feature.

These remain valid specifications. When the gRPC engine or streaming pipeline is implemented, features move from features_archive/ back to features/ and are connected to step definitions. See Scenarios Overview for the full backlog listing

DCGM, the warehouse, and agent exchange

One picture of GPU telemetry in a cluster — not only this node — through the Kubernetes DCGM exporter, the Signals federated workspace, and the tiered warehouse. Hot rows live on a Kudu cluster. Settled rows live as Parquet Iceberg data files, and the message-preserving records live as Protobuf objects, on the same object store. Readers use one surface: PostgreSQL impala_fdw over Impala views that union Kudu and Iceberg. Agent exchanges (hx_exchange, hx_reasoning) stay on the same tx_id and the same clock as the device samples, so an interactive session or a reinforcement-learning / fine-tune step can be read back with the watts and faults it caused.

The fence below is plain Mermaid (flowchart). GitHub renders it. Copy the fence, without the mermaid tag line, into any renderer that accepts GitHub’s flowchart syntax. Line breaks inside nodes are <br>. Subgraph titles and node labels that contain punctuation are double-quoted. There is no %%{init}%% directive, no HTML beyond <br>, and no diagram type other than flowchart.

flowchart TB
  subgraph hosts ["1. System: every GPU node"]
    nodeA["GPU node A<br>NVIDIA driver and NVML"]
    nodeB["GPU node B<br>NVIDIA driver and NVML"]
    nodeN["GPU node N<br>NVIDIA driver and NVML"]
  end

  subgraph k8s ["2. Kubernetes cluster: dcgm-exporter"]
    ds["DaemonSet federation-system/dcgm-exporter<br>one pod on every GPU node<br>namespace federation-system"]
    pod["Privileged pod, hostNetwork, hostPort 9400<br>mounts host /dev, /proc, /sys, libnvidia-ml<br>no GPU token, default scheduler, no TSDB"]
    fields["Prometheus GET /metrics<br>power, energy, util, framebuffer,<br>clocks, temperature, XID, NVLink<br>scrape interval 250ms"]
    svc["Service dcgm-exporter port 9400<br>one endpoint per node"]
    attr["Pod attribution<br>DCGM_EXPORTER_KUBERNETES=true adds<br>pod, namespace, and container labels<br>the shipped manifest sets false and uses hostPort"]
    ds --> pod --> fields --> svc
    fields -.-> attr
  end

  nodeA --> ds
  nodeB --> ds
  nodeN --> ds

  subgraph workspace ["3. Signals Federated Workspace"]
    scrape["Scrape every node endpoint, once per request<br>SIGNALS_DCGM_PROM_URL<br>this node defaults to 127.0.0.1:9400"]
    otlp["Live yield. Not a store.<br>GET :9410/v1/metrics<br>OTLP metrics protobuf schema, JSON on this port<br>503 when the exporter is down"]
    surface["Engine surface kind=telemetry<br>Gaius, Aegir, Atelier, Hermes, Metabase pull it<br>signals-protocol Status.surfaces"]
    ingest["Retained ingest at native precision<br>src 0 is DCGM. No float widening.<br>signal_series maps dcgm_field to series_id"]
    scrape --> otlp --> surface
    scrape --> ingest
  end

  svc --> scrape

  subgraph hx ["4. Agent exchanges"]
    work["Application work on a resource-class leaf"]
    interactive["Interactive session<br>root.internal.inference.instruct<br>reasoning, light, or agent-rtc<br>product peer.agent.trajectories"]
    train["Reinforcement learning or fine-tune<br>root.internal.inference.extract<br>medium, or heavy<br>product aegir.models.bespoke and peers"]
    external["External leaf, zero GPU claim<br>root.external.subscription.rate-limited<br>token-metered or rate-metered<br>hx is still written"]
    yk["YuniKorn Application<br>app-id = federation.workload_id<br>queue = the resource-class leaf<br>claim key federation.zndx.org/gpu"]
    run["Agents/Run on zndx.agent.v1<br>tx_id is RFC 9562 UUIDv7<br>coordination Activity agent_run"]
    exchange["hx_exchange<br>agent, actor, message per utterance"]
    reasoning["hx_reasoning<br>quality, lineage, delta, trace per turn"]
    facts["details facts, column t = tx_id<br>yk_app_id, yk_queue, run_id, model, flow_name"]
    work --> interactive --> yk
    work --> train --> yk
    work --> external --> run
    yk --> run
    run --> exchange
    run --> reasoning
    run --> facts
  end

  yk -.->|"placed on a node and a GPU ordinal"| pod

  subgraph kudu ["5. Kudu cluster — tier 0, hot"]
    masters["Masters, Raft, odd count<br>cluster: 3 masters<br>this node: 1 master, port 7051"]
    tservers["Tablet servers, port 7050<br>cluster replication 3<br>this node: 1 tserver, replication 1"]
    tables["Hot tables, database signals_dataproducts<br>signal_tier0, signal_series<br>tx_tier0, details_tier0<br>hx_exchange_tier0, hx_reasoning_tier0<br>latent_tier0, clt_activation_tier0"]
    bounds["RANGE on epoch_hour, HASH buckets<br>writers INSERT only<br>expire with DROP RANGE PARTITION<br>never DELETE"]
    masters --> tservers --> tables --> bounds
  end

  subgraph ice ["6. Iceberg on object storage — tier 1, settled"]
    polaris["Polaris REST catalog :8181<br>warehouse signals"]
    spec["Iceberg spec, not Protobuf<br>table metadata is JSON<br>manifest list and manifests are Avro"]
    parquet["Parquet data files<br>settled signal, tx, details, hx rows<br>write.format.default = parquet"]
    proto["Protobuf message objects<br>OTLP ExportMetricsServiceRequest<br>zndx.agent.v1 trajectory / AgentEvent"]
    bucket["Object storage, S3 API<br>bucket s3://signals-dataproducts/<br>this node: RustFS :9010"]
    settle["Settle a closed range<br>copy, verify the Impala count, then DROP RANGE<br>products: data-product.tier-upkeep<br>signals: signal_settle / GpuMetricsSettle"]
    polaris --> spec
    spec --> parquet
    spec --> proto
    parquet --> bucket
    proto --> bucket
    settle --> spec
  end

  subgraph ir ["7. Retrieval — impala_fdw over Kudu and Iceberg"]
    pg["PostgreSQL foreign tables<br>Signals :5455, peer example Gaius :5444<br>one Kerberos GSSAPI principal"]
    fdw["impala_fdw<br>server impala_kudu_srv"]
    kscan["access = kudu_scan<br>libkudu_client<br>tier0 INSERT and hot scan<br>bypasses Impala"]
    hsql["access = impala_sql<br>HiveServer2 :21050"]
    impala["Impala cluster<br>statestore, catalogd, impalad<br>HMS-free catalog registry in Postgres"]
    views["Single reader surface<br>views signal, tx, details,<br>hx_exchange, hx_reasoning<br>tier0 UNION ALL tier1<br>where epoch_hour is not still in tier0"]
    caller["Caller does not pick a tier<br>peers, signals-ui, agents"]
    pg --> fdw
    fdw --> kscan
    fdw --> hsql
    hsql --> impala
    impala --> views
    views --> caller
  end

  subgraph corr ["8. Correlation — performance stays on the operation"]
    live["Live OTLP attributes, not warehouse columns<br>gpu.index, gpu.uuid, gpu.hostname, dcgm.field<br>service.name = signals-dcgm"]
    stamp["Control-plane merge key, signals-ui<br>openlineage.runId<br>yunikorn.applicationId<br>otel.trace_id<br>signals.workload_id"]
    keys["Read-time join. No stored foreign key.<br>tx_id = details.t = hx_exchange.tx_id = hx_reasoning.tx_id<br>same epoch_hour<br>signal.ts_ns inside min..max hx ts_ns for that tx<br>signal.gpu is the device, signal.inst the instance<br>src 0 device samples beside src 1 trainer or vLLM<br>latent_tier0.ref and clt_activation_tier0.ref = the trace<br>hx_reasoning.lineage holds the OpenLineage run id<br>hx_reasoning.trace holds the brief or the protobuf URI"]
    live --> stamp --> keys
  end

  ingest -->|"INSERT"| kscan
  exchange -->|"INSERT"| kscan
  reasoning -->|"INSERT"| kscan
  facts -->|"INSERT"| kscan
  kscan <-->|"kudu_scan read and INSERT"| tables
  tables --> settle
  reasoning -.->|"trace URI"| proto
  tables -->|"UNION tier0 fragment"| impala
  parquet -->|"UNION tier1 scan"| impala
  polaris --> impala
  views -->|"SQL rows plus object URI"| keys
  kscan -->|"hot tier0 rows"| caller
  otlp -.-> live
  yk -.-> stamp
  external -.->|"zero GPU claim, so no src 0 rows"| keys

  style hosts fill:#e7f6ec,stroke:#1b7a3a
  style k8s fill:#e7f1fb,stroke:#1d4e89
  style workspace fill:#f8f1e3,stroke:#8a5a00
  style hx fill:#fdecea,stroke:#8a2b2b
  style kudu fill:#f3e8f8,stroke:#5c2d91
  style ice fill:#eef6f8,stroke:#0f5f6b
  style ir fill:#eef0f8,stroke:#24356b
  style corr fill:#fff6e8,stroke:#8a5a00

Two ways out of the exporter

The exporter does not write the warehouse. Nothing in this path is a TSDB.

PathWhat happensRetained?
Live yieldSignals engine signals.telemetry.dcgm_otel GETs /metrics once per request and returns OTLP JSON on :9410/v1/metrics. Status.surfaces advertises that URL with kind=telemetry.No. Empty consumer, no work beyond the idle exporter. Down exporter is HTTP 503.
Warehouse landA workspace writer (the Gaius ingest is the reference) parses the same Prometheus text at the field’s native width and INSERTs signal_tier0 through impala_fdw kudu_scan. signal_series.src = 0 and dcgm_field name the DCGM counter.Yes. Hot on Kudu, then settled.

src on a sample is 0 DCGM, 1 vLLM or trainer, 2 engine, 3 host. Device cost and application counters share epoch_hour and ts_ns. They are not two pipelines.

Tier-0 inserts in this picture are impala_fdw kudu_scan (the same foreign tables the hot scan reads). signals.ops.warehouse.ImpalaWarehouse opens HiveServer2 directly; that sidecar is not the pattern drawn here.

How a sample stays on a session

There is no foreign key from signal_tier0 to tx. Association is a read over the views in band 7. For one operation:

  1. The operation is one tx_id (UUIDv7). tx, details.t, hx_exchange, and hx_reasoning use that id.
  2. details for that id names the placement: yk_app_id (this is federation.workload_id and the YuniKorn application id), yk_queue, run_id, model, flow_name.
  3. Interactive work and a fine-tune or RL step differ by queue leaf and product, not by storage. Interactive leaves are root.internal.inference.instruct, reasoning, light, and agent-rtc (Hermes WebRTC, one GPU while the session runs). The trajectory product is {peer}.agent.trajectories. Training-shaped work uses extract, medium, or heavy (for example aegir.models.bespoke). There is no project queue and no root.gaius.
  4. An external leaf (root.external.subscription.rate-limited, token-metered, rate-metered) records hx_* and claims no GPU. It has no src = 0 rows.
  5. YuniKorn places a GPU Application on a node and a device ordinal. The retained sample stores the device as signal.gpu and the instance — the node, when the cluster writer stamps one — as signal.inst. Hostname and GPU UUID stay on the live OTLP point (gpu.hostname, gpu.uuid). They are not columns of signal_tier0.
  6. Device samples for the operation are signal rows with src = 0, that gpu and inst, the same epoch_hour, and ts_ns between the minimum and maximum ts_ns of the hx_* rows for the tx_id. src = 1 rows in that window are the trainer or vLLM counters beside the device.
  7. When the operation recorded model state, latent_tier0.ref and clt_activation_tier0.ref carry the same trace id. hx_reasoning.lineage carries the OpenLineage run id when the review wrote one. hx_reasoning.trace carries the brief, or the object URI of the Protobuf trajectory (zndx.agent.v1, ATIF on the object plane).
  8. The signals-ui merge key openlineage.runId · yunikorn.applicationId · otel.trace_id · signals.workload_id is the control-plane name for the same association. otel.trace_id is not its own warehouse column.

hx_exchange.actor is the speaker (the column is not named role). Kudu string cells used by hx_* are capped at 64KiB; a longer trace is the Protobuf object, and the cell holds the URI.

Parquet, Protobuf, and the Iceberg spec

Tier-1 rows that Impala scans are Parquet data files under the Iceberg table (write.format.default=parquet). Tier-1 messages that must stay in their wire shape are Protobuf objects on the same bucket: an OTLP metrics export (opentelemetry.proto.metrics.v1) and a zndx.agent.v1 trajectory. Iceberg finds the Parquet files through JSON table metadata and Avro manifests. Manifests are not Protobuf.

Polaris (:8181) is the REST catalog. The bucket is s3://signals-dataproducts/. On this node that API is RustFS :9010.

Writers do not DELETE. After a closed range is copied and the Impala count matches, tier upkeep runs DROP RANGE PARTITION on the Kudu tables (impala_fdw_exec for the range DDL). The views hide tier-1 hours that are still present in tier 0, so a settled-but-not-yet-dropped range is not counted twice.

This node and a real cluster

The diagram is the cluster. This workstation is the same graph with the widths turned down.

PieceClusterThis node
DCGMDaemonSet pod on every GPU node; scrape each Service endpointhostPort 9400, scrape 127.0.0.1:9400
Exporter labelsSet DCGM_EXPORTER_KUBERNETES=true so a sample names the podShipped manifest sets false (node-scoped watts)
Kudu3 masters, tablet servers, replication 31 master :7051, 1 tserver, kudu.num_tablet_replicas=1
Object storeS3-compatible bucket signals-dataproductsRustFS :9010
CatalogPolaris RESTPolaris :8181 / admin :8182
SQL front doorPostgres + impala_fdw on each peerSignals :5455, Gaius :5444
IdentityKerberos GSSAPI, one principal through Postgres, Impala, and KuduRealm DEV.VISTA.ZNDX.ORG

The lab settle writer (scripts/signal_settle.py, scripts/gpu_metrics_tier_up.py) still emits HDF5 (.h5, FileFormat.HDF5, write.format.default=hdf5 on signal_tier1) and Impala reads those files with the in-fork HDF5 scanner. That is the workstation analog. It is not the layout drawn above. The cluster blueprint’s scanned files are Parquet; Protobuf is the message objects beside them.

Where the names are defined

Name in the diagramDefined in
dcgm-exporter DaemonSet, counters, :9400zarf/federation/manifests/telemetry/dcgm-exporter.yaml
OTLP yield :9410/v1/metricssrc/signals/telemetry/dcgm_otel.py
Telemetry surfacesrc/signals/engine/s2s.py (kind=telemetry)
signal_tier0 / signal foreign tablesconfig/platform/signal-fdw.sql
tx / details / hx_* viewsconfig/platform/data-products-views.sql
Tier-0 DDL, week ranges, no DELETEconfig/platform/data-products-kudu.sql
Resource-class leavesconfig/scheduler/resource-classes.md
tx_id, hx_*, trajectory productcomponents/signals-protocol/specification/protocol/data_products.md, agent_grpc.md
FDW kudu_scan vs impala_sqlImpala FDW, Query engine

Logical warehouse names and the fact-log shape: Data products history. The exporter’s place on the critical plane: Critical plane.

Nautilus FSM and the Brier ledger

Nautilus is the deterministic supervisor beside each engine. It does not host a model. The project ships a Supervisor instance; the protocol zndx.supervision.v1 is only the grammar. Nautilus loads that instance, stamps a position on every forecast, and scores Brier inside the epoch spec_version + engine_build. A forecast answers to its phase’s gate, not to a later failure in the same run.

This is not the IT-ops method catalog. That ledger (IT-ops FSM and Brier ledger) scores probes in build/state/ops-observations.jsonl by method state and implicit K8s/YuniKorn state. Nautilus writes nautilus_events, nautilus_positions, and nautilus_backlog.

The fence below is plain Mermaid (flowchart). GitHub renders it. Paste the contents into a Mermaid renderer, or keep the fence in a GitHub markdown file. Line breaks inside nodes are <br>. Subgraph titles and node labels that contain punctuation are double-quoted. There is no %%{init}%% directive, no HTML beyond <br>, and no diagram type other than flowchart.

flowchart TB
  subgraph spec ["1. Instance — grammar here, project only in the textproto"]
    textproto["Supervisor textproto<br>config/supervision/project.textproto<br>LoadSpec rejects the whole file on one validation failure"]
    epoch["Scoring epoch<br>spec_version plus engine_build<br>an unrelated source commit does not reset the ledger"]
    tree["Supervision tree<br>parent and depends_on<br>ONE_FOR_ONE, ONE_FOR_ALL, REST_FOR_ONE, or NONE"]
    machine["Machine on one process<br>phases are a total order"]
    objective["Objectives on the surfaced result<br>mechanics paired with intent<br>every gate must pass"]
    budgets["Budgets<br>recycle_k per window, then cooldown<br>exhaustion trips a breaker, never a silent retry"]
    textproto --> epoch
    textproto --> tree
    textproto --> machine
    textproto --> objective
    textproto --> budgets
  end

  subgraph health ["2. Process health — a pure function of the snapshot"]
    snap["One process in the snapshot"]
    starting["STARTING<br>inside warmup_seconds<br>never a miss"]
    serving["SERVING<br>observed and making progress"]
    missing["MISSING<br>past warmup, not observed past the net"]
    stalled["STALLED<br>observed, no progress past the net<br>progress beats wall-clock"]
    unsupervised["UNSUPERVISED<br>reports_positions and never called ReportPosition<br>surfaced, never inferred around"]
    unobs["UNOBSERVABLE<br>the observing engine is MISSING<br>nothing can be said"]
    snap --> starting
    snap --> serving
    snap --> missing
    snap --> stalled
    snap --> unsupervised
    snap --> unobs
  end

  subgraph phases ["3. Phase gate — a hierarchy, not a flat window"]
    phaseA["Phase A<br>named source steps"]
    gateA["Gate A closes phase A<br>GateKind plus concrete params<br>no threshold means no gate"]
    horizonA["Horizon of phase A<br>window_seconds<br>resolve_within_phase_only"]
    phaseB["Phase B<br>resolves the claim that gate A held<br>cannot resolve forecasts stamped in A"]
    gateB["Gate B<br>JUDGE_RUBRIC with judge_final_call is GOLD<br>the supervisor's own check is SILVER at best"]
    intent["ResourceIntent emitted on phase entry<br>occupancy, floor, priority<br>zero floor on exit, ahead of the claim"]
    phaseA --> gateA --> horizonA --> phaseB --> gateB
    phaseA --> intent
  end

  subgraph observe ["4. Observation — Nautilus dials, the engine serves"]
    naut["Nautilus<br>one binary, one instance, one ledger<br>no agent loop"]
    stream["EngineSupervision.Supervise<br>engine death is the stream dropping<br>the engine runs with nobody connected"]
    adapters["SourceKind adapters<br>systemd, Metaflow store, task queue, HTTP<br>Airflow and any foreign store only via the owning engine"]
    report["ReportPosition at each gate boundary<br>REPORTED by the process<br>OBSERVED by an adapter<br>INFERRED carries the least weight"]
    pos["Position<br>process, machine, phase, run_id, step<br>momentum raw here, bucketed at read<br>epoch stamped on the row"]
    textproto -->|"LoadSpec"| naut
    stream --> naut
    adapters --> naut
    report --> naut
    naut --> snap
    naut --> pos
  end

  subgraph act ["5. Directives — Nautilus never kills"]
    trig["evaluate_triggers of the snapshot<br>arming key is trigger plus scope plus bucket<br>never free text"]
    reclaim["ReclaimOrphan<br>silence since last progress, not age since claim<br>a heartbeat after the snapshot voids it<br>a live child refuses it"]
    esc["Escalation is the Overwatch consult<br>the judge stays in the engine<br>judge unavailable is VERDICT_ERROR, not a local fallback"]
    result["DirectiveResult<br>engine dedups on directive_id<br>recycle, reset, hold, and breaker are forecasts"]
    naut --> trig
    budgets --> trig
    trig --> reclaim --> result
    trig --> esc
    esc --> result
  end

  subgraph book ["6. Write-ahead, then the tiered store"]
    wal["SupervisorRecord protobuf first<br>nisshi journal on the object store<br>recording never blocks on the supervised stack<br>buffered_records is the journal lag"]
    drain["Drain into the project warehouse<br>impala_fdw kudu_scan INSERT<br>the project's Postgres, not a peer's"]
    events["nautilus_events_tier0<br>trigger, directive, forecast, resolution<br>p, verdict, outcome_known, outcome, position"]
    positions["nautilus_positions_tier0<br>heartbeat, position, health"]
    cells["nautilus_backlog_tier0<br>one row per workflow, item, Fibonacci slot"]
    views["Logical views nautilus_events, nautilus_positions, nautilus_backlog<br>tier1 Iceberg union is pending<br>until then the views are tier0 only, and no day-drop"]
    wal --> drain
    drain --> events
    drain --> positions
    drain --> cells
    events --> views
    positions --> views
    cells --> views
  end

  subgraph score ["7. Brier — per cell, inside the epoch"]
    cell["Score cell<br>observer, call_site, momentum bucket<br>same spec_version and engine_build<br>position selects which gate may resolve the row"]
    brier["Brier is the mean of (p minus outcome) squared<br>only rows with outcome_known<br>a correction is a new row, latest wins"]
    alpha["alpha = (n/(n+K))*(1-Brier) + (K/(n+K))*prior<br>K = 2, prior = 0.5<br>n = 0 is uncalibrated: risk 0.5, never trusted"]
    promo["Actuation promotion for that scope<br>observe, then propose, then act<br>only as this cell's record earns it<br>the judge of that promotion is a different process"]
    epoch --> cell
    cell --> brier --> alpha --> promo
  end

  subgraph fib ["8. Operations Backlog — Fibonacci hours, not minutes"]
    scales["Two scales, one clock<br>nets, admission, idle: seconds<br>Backlog: the hour"]
    tick["Nautilus.Tick<br>idempotent on the hour<br>a systemd timer is the second hand while the resident runs"]
    slots["Slots 0..9 are Fibonacci hours<br>0, 1, 1, 2, 3, 5, 8, 13, 21, 34<br>computed horizons round DOWN"]
    admit["Slot 0 is first admission<br>time inside the window is the arbiter working, not Backlog time<br>a healthy workflow writes ok here and leaves 1..9 empty"]
    confirm["Slots 1 and 2, both 1 hour<br>miss observed, then confirmed<br>the repeat separates a transient from an open item"]
    chan["The deepest miss selects the channel<br>1 and 2 log, 3 agenda, 5 reminder<br>6 and 8 briefing, 9 discussion"]
    resolved["A later ok resolves the item<br>the history stays"]
    scales --> tick --> slots --> admit --> confirm --> chan
    confirm --> resolved
  end

  machine --> phaseA
  gateA -->|"resolves only forecasts stamped in phase A"| cell
  gateB -->|"resolves the claim that gate A held"| cell
  intent -->|"forecast: share APPLIED within N seconds<br>a dark arbiter makes every peer VERDICT_INCONCLUSIVE"| cell
  pos --> wal
  pos --> cell
  result --> wal
  result -.->|"scored like any other forecast"| cell
  esc -.->|"engine records the judge forecast"| cell
  reclaim -.->|"dry_run records the forecast and does not reset"| wal
  tick --> wal
  views --> reader["Backlog readers<br>Nautilus.Backlog and the engine Backlog RPC<br>the engine still answers when Nautilus is dark<br>a stale last_tick is the signal"]
  peer["A peer Nautilus<br>watches sibling Status<br>writes only its own ledger"]
  peer -.-> wal

  style spec fill:#eef0f8,stroke:#24356b
  style health fill:#e7f6ec,stroke:#1b7a3a
  style phases fill:#f8f1e3,stroke:#8a5a00
  style observe fill:#e7f1fb,stroke:#1d4e89
  style act fill:#fdecea,stroke:#8a2b2b
  style book fill:#f3e8f8,stroke:#5c2d91
  style score fill:#fff6e8,stroke:#8a5a00
  style fib fill:#eef6f8,stroke:#0f5f6b

The phase-gate rule

Forecasts made while a machine is in a phase are resolved against that phase’s gate, inside that phase’s Horizon. With resolve_within_phase_only set, a later phase must not close them. The gate’s own verdict becomes a claim the next phase resolves.

That is why “the file downloaded” is not Brier-penalized for a vLLM failure three phases later, and why the terminal surface verdict can still resolve the terminal gate. Resolving by proposition text inside a time window is the failure this rule replaces: a segment-correct claim was charged for a downstream miss it did not cause.

Positions are the stamp, not a scrape of free text. A process with reports_positions calls ReportPosition on each transition. Unknown process or phase is Ack.accepted=false. An adapter may fill OBSERVED (Metaflow steps, Airflow task instances, pg_cron runs). INFERRED is derived, for example lifecycle from timestamps, and weighs least. Objective.resolves (proposition patterns) is transitional and is retired as the machine starts reporting positions.

A gate with no concrete threshold is not a gate. Where the surface needs a judgment, GATE_KIND_JUDGE_RUBRIC with judge_final_call is tier GOLD and Overwatch renders it. The supervisor does not host that model. Judge unavailable is VERDICT_ERROR. A local model’s rubric score is itself a forecast, resolved by the judge’s call.

Mechanics and intent are paired objectives. Reaching end, a clean task row, or a file on disk is a forecast that the user receives the intended artifact. It is not the objective. The audit case is a publish that stayed green while the surface served three-week-old content.

How a cell is scored

The cell is (observer, call_site, momentum bucket) inside one epoch. Position.momentum is the raw corroboration count. Bucketing happens at read time, so a counter ticking inside a poll does not mint a new cell and does not re-arm a trigger. The arming signature is the same idea: trigger, scope, and bucketed state. Never free text.

For resolved rows in that cell:

Brier = mean( (p - outcome)^2 )
alpha = (n / (n+K)) * (1 - Brier) + (K / (n+K)) * prior

K is 2 and prior is 0.5. n = 0 returns the prior: risk 0.5, not 0. An unresolved group is uncalibrated. It is never treated as trusted. One miss moves the score. It does not change behavior, disable a probe, or retune a threshold. Divergence is surfaced as a trigger and, when determinism cannot settle it, as an Escalation. The judge is scored on the same ledger.

Actuation is promoted per process: observe, then propose, then act, only as that scope’s Brier record earns it. The judge of the promotion is a different process from the supervisor being promoted. Adoption starts at RESTART_STRATEGY_NONE (observe and score, no recycle). The Signals instance is still there: every process in config/supervision/signals.textproto is NONE, and the pinned epoch is spec_version 2026-09-20.1 with engine_build trunk.

Every side effect is a row. A recycle, a reset, a hold, or a breaker trip carries the Position it was rendered at and is resolved inside its horizon. ReclaimOrphan may be dry_run: the forecast is written and the task is not reset. Nautilus measures silence since the last progress signal. It does not measure age since claim, and it does not kill on the clock.

A ResourceIntent is also a forecast: share applied within N seconds, resolved by the arbiter’s QueueShareRecord. The arbiter is Signals (YuniKorn floors), and it is scored like any other observer. Sustained divergence between declared floors and applied config is an objective failure. If the arbiter is dark, every peer’s share forecasts go INCONCLUSIVE together. That agreement is the signal. Nautilus instances do not vote on the placement.

The Backlog clock

Intra-workflow nets stay in seconds (Cadence.net_seconds, warmup, idle). The Operations Backlog reads only the hour, on Fibonacci slots F0..F9 = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34. A horizon computed from a net in seconds rounds down to the nearest slot: surface early, never late.

SlotHoursWhat it means
00First admission. Inside the window the arbiter is still working. A healthy workflow writes ok here.
1 and 21 and 1Miss observed, then confirmed on the next hourly evaluation.
32Agenda event.
55Reminder.
6 and 88 and 21Briefing.
934Discussion. The engine replay window is at least 36 hours so this slot is still in the log.

Slots 4 (3 h) and 7 (13 h) stay on the ladder and do not select a new channel. The deepest slot holding a miss is the escalation level. A later ok resolves the item. The history stays. Nautilus.Tick is idempotent on the hour. Categories (TICK, HOURLY_SETTLED, SLOT, DAILY_DEPENDENT, JUDGED, CHRONIC_AUDIT, OPERATOR_WINDOW, TRANSIENT, COORDINATION) are the contract on the process Expectation. A ceded coordination Activity is not a miss. An Activity past its horizon without release is the item.

BACKLOG_STATE_UNKNOWN means the observing engine was dark at the tick. The engine’s Backlog RPC still answers when Nautilus is down; a stale last_tick is the honest signal.

Where the rows land

Nautilus does not wait on Postgres. A SupervisorRecord (one of BacklogCell, SupervisorEvent, PositionRecord) is appended to the nisshi journal on the object store, then drained. buffered_records on SupervisorStatus is that lag. The drain is impala_fdw kudu_scan into the project’s own Kudu tables. A peer watching Status writes its ledger. No engine writes another engine’s rows.

On this deployment the hot tables are signals_dataproducts.nautilus_events_tier0, nautilus_positions_tier0, and nautilus_backlog_tier0 (config/platform/nautilus-kudu.sql). Day-wide ranges, hash on the workflow, event, or process. The logical views are still SELECT of tier 0 only. Iceberg nautilus_*_tier1 waits on the Impala Iceberg write path. Until that lands there is no day-drop: the Backlog window is 34 hours, not a settled archive. The same foreign-table split as the DCGM appendix applies when the union exists: tier 0 is kudu_scan, the logical view is impala_sql.

Acks go upstream only after the record is durable in the supervisor’s store. Until then the engine’s own tables are the log. Subscribe{since} replays at least 36 hours, then streams live. Events carry the source timestamp so replay and live dedupe the same way. seq detects gaps in one session. It is not the durable cursor.

Where the names are defined

Name in the diagramDefined in
Grammar: tree, phase, gate, horizon, Brier epochcomponents/signals-protocol/specification/operations/nautilus_supervision.md
Messages and enumscomponents/signals-protocol/proto/zndx/supervision/v1/supervision.proto
Signals instance, all NONEconfig/supervision/signals.textproto
Kudu tier 0 and the tier0-only viewsconfig/platform/nautilus-kudu.sql
Foreign tablesconfig/platform/nautilus-fdw.sql
IT-ops method ledger, a different scoreIT-ops FSM and Brier ledger