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
| Plane | What it is |
|---|---|
| Wire | signals-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. |
| Schedule | Signals engine on :50551 (capability=scheduler). YuniKorn admits Applications; the queue path is the resource class. |
| Workflows | Airflow holds coordination Activities (including agent-rtc). Metaflow is the production flow path: one metadata service, artifacts on RustFS, events through Knative Eventing. |
| Storage | Transparent 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. |
| Governance | Apache Atlas (AGE on PostgreSQL 16) with an OpenLineage REST API. Marquez-web is the lineage UI. Ranger consumes Atlas tags. |
| Control | signals-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)
| Project | Capability | Port | Primary UI |
|---|---|---|---|
| Signals | scheduler | :50551 | signals-ui :9889 |
| Gaius | cognition | :50051 | board :9890 |
| Ægir | instruct | :50151 | gateway / Vite |
| Atelier | referee | :50251 | workbench :3000 |
| Hermes Agent | agent | :50651 | dashboard :9119 |
| Metabase | dashboard | :50451 | Metabase :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
- Architecture — protocol, queues, Activities, Metaflow, the warehouse, Atlas OpenLineage, the control UI.
- Components — the Apache and sibling trees this repo pins.
- Operations — devenv, Kerberos, secrets, backup, joining as a peer.
- Classification —
sigint, SIGDG, Dempster–Shafer fusion, SAGE. Useful if you tag columns; optional if you are attaching an engine. - 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,grpcurlonPATH - 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
| Plane | Role |
|---|---|
| Protocol | Shared Engine and Scheduler RPCs. Peers keep native gRPC. |
| Schedule | YuniKorn admits Applications. Resource class is the queue leaf. Activities are Airflow DAG runs owned by Signals. |
| Flows | One Metaflow metadata service; tasks as Kubernetes pods; events via Knative Eventing. |
| Storage | Kudu hot, Iceberg on RustFS, Impala views, PostgreSQL via impala_fdw. |
| Governance | Atlas is lineage and classification. OpenLineage REST on the same process. Marquez-web on :21011 is the UI. |
| Control | signals-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.
| Layer | Maturity (honest) | Where it lives |
|---|---|---|
| Native product gRPC | Mature in each core peer | Gaius / Ægir / Atelier / … trees |
| Multi-service + capability engines | In production use; still converging | Core peer engine/ packages |
zndx.engine.v1 shared service | v1 landed; incomplete relative to real peer needs | signals-protocol + per-peer bindings |
| OIP dual-registration / mapping | Partial (stronger in some engines than others) | Protocol spec + peer OIP servicers |
| License-external engines | Isolated adapters (e.g. mbengine) | Separate trees (e.g. AGPL Metabase) |
| Platform process attachment | Lab-usable (ready gate, units, lattice-ci) | This repo (peer-contract, systemd) |
| Cross-engine product flows | Early (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-ready | One foundation readiness gate |
signals.target + peer units | One group lifecycle for co-tenant engines |
peer-contract + lattice-ci | One accept surface (codegen Status + reflection) |
| Platform Metaflow / Airflow / Eventing / YK / Atlas | Shared production path without re-hosting |
signals-protocol pin | One 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.
| Obligation | Meaning |
|---|---|
| Full project stack | Under 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 capacity | The 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 state | start / 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 failure | When 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 upstacks 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
| Layer | Role |
|---|---|
.proto + gRPC services | The specification (OIP-aligned). Codegen is how we implement clients and servers. |
| Generated stubs | First-party path for lattice-ci, unit probes, peer libraries (scripts/zndx_engine_status.py, peer engine/generated/, …). |
| gRPC server reflection | Install 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.
| Requirement | Why |
|---|---|
Codegen from proto/ | Proper protocol implementation for engines and our automation |
| Server reflection on lattice ports | External/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):
- 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?
- 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.
| Prefer | Avoid | Why |
|---|---|---|
| multi-service gRPC | multi-face gRPC | One listen port, several services on one server — gRPC’s own model |
lattice-ci / *-ci | smoke (for elevated gates) | Repeatable accept gates are CI; smoke is one-off first-run feedback |
| protocol / codegen path | proto fallback | .proto is the specification, not a backup |
| peer-scoped stop | soft stop | Full unit stop, scoped to one peer — not a partial or vague halt |
| Domain terms + faithful APIs | Metaphor wrappers for math | Implementations 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 engines | License-external peers | |
|---|---|---|
| Examples | Gaius, Ægir, Atelier; future synth, vigil, … | Metabase (AGPL) today |
| Why separate trees | Product modularity; shared ASL2 (or compatible) line | License incompatibility (AGPL ↛ ASL2 combine) |
| Engine architecture | Shared family (capability engines, multi-service gRPC, co-tenancy) | Isolated product engine (e.g. mbengine); not Gaius-lineage |
| Protocol role | Primary source of organic signals-protocol requirements | Speaks enough wire to join; not the design center for core engine law |
| Platform attach | signals.target, lattice ports, Metaflow/CE/YK as applicable | Same process pattern optional; no source/jar into Signals or core peers |
| SoR | Never — Atlas/lineage/authz stay on Signals | Never — 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.”
| Concern | Authority | Client surface |
|---|---|---|
| Governance types & classifications | Atlas on signals PG + AGE | /api/atlas/* |
| Runtime lineage (Job/Run/Dataset) | Same Atlas process + signals_ol | /api/v1/* (OpenLineage + Marquez-compat) |
| Authorization decisions | Ranger (Atlas tags as input) | Ranger REST / plugins; policy evaluate via discovery |
| Human OL UI | Marquez-web | :21011 (Atlas HTTP + 1) → proxies /api/v1 only |
| Agent memory + compaction | Weathership plugins on Hermes | MemoryProvider + context engine (plugins) |
| Engine-to-engine inference | zndx.engine.v1 | Complete / Status / Remediate / Yield / ServerQuery |
| Heterogeneous model serving | KServe OIP + signals-protocol mapping | ModelInfer / readiness; authz + provenance on the wire |
| GPU co-tenancy | YK Application claim (federation.zndx.org/gpu) + Status.gpu_ids | Queue 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
| Path | Role |
|---|---|
components/signals-protocol/proto/ | Source of truth for protos |
components/signals-protocol/specification/ | Human specs per package |
| Generated stubs | Vendored 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 kind | What is advertised | Typical base |
|---|---|---|
LINEAGE_OL | OpenLineage ingest + Marquez-compat read | http(s)://…:21010/api/v1 |
ATLAS_GOVERNANCE | Atlas REST v2 (entities, types, classifications) | http(s)://…:21010/api/atlas |
RANGER_AUTHZ | Ranger admin / policy evaluate | http(s)://…:6080 (lab) |
MARQUEZ_UI | Optional human UI (not SoR) | http(s)://…:21011 (Atlas HTTP + 1) |
ENGINE | Peer zndx.engine.v1 gRPC | host:port |
OIP | Open Inference Protocol HTTP/gRPC | KServe-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:
- Calling discovery (or reading a Signals-published service map).
- Emitting OpenLineage to
LINEAGE_OL(not a local Marquez). - Registering / classifying assets via
ATLAS_GOVERNANCE(or Signals hooks). - 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 may | They must not |
|---|---|
| Read catalog/lineage via discovered APIs / FDW | Own a parallel lineage DB as product truth |
| Honor Ranger / edge authn for queries | Bypass ZT + Kerberos/data-plane identity |
| Appear as OL Jobs/Runs when they materialize datasets | Replace 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:
| Need | Why OIP + signals-protocol together |
|---|---|
| Heterogeneous serving | Triton/KServe/external clusters speak OIP; zndx engines speak Complete |
| Authorization | Model invoke is a privileged action — identity + Ranger/tag context on the request path |
| Provenance | Every multi-agent inference should leave an OpenLineage Run (and optional reasoning retention) |
Direction:
- Map
zndx.engine.v1.Complete↔ OIPModelInfer(text/tensor payloads) in the protocol spec (expand existing OIP mapping table). - Carry authz context on federation/OIP calls (caller principal, optional Atlas entity / classification scope) — additive proto fields; enforcement at the serving edge and Ranger.
- Emit provenance — engines (or a Signals sidecar) POST RunEvents to
/api/v1/lineagefor model ops: inputs (prompt hashes / dataset refs), outputs, model id fromCompleteResponse.model, capability name. - Optional OIP gateway in Signals or Gaius that dual-registers
zndx.engine.v1and 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
| Piece | Role |
|---|---|
| Marquez-web | Default-stack UI for OL; proxy to Atlas /api/v1 |
| Marquez API contract tests | Freeze the subset Signals implements |
| Stock Marquez server + Flyway | Rejected |
See OpenLineage + Atlas.
Phased delivery (protocol core)
| Phase | Deliverable |
|---|---|
| P0 | components/signals-protocol submodule + this doctrine |
| P0b | Governance scale plane: Atlas/Ranger Kudu projections + RustFS on RAID — required so multi-engine devenv does not crush Postgres/AGE (governance-scale-plane) |
| P1 | Discovery: advertise Atlas + OL + Ranger + OBJECT_STORE on federation Status (or discovery.v1) |
| P2 | Identity map: OL dataset ↔ rdbms_* qualifiedName; dual-path smoke |
| P3 | Producer contract suite (polyglot, Gaius, Flink) → single /api/v1 |
| P4 | OIP mapping + authz/provenance fields in protocol; model-op RunEvents |
| P5 | External peer pack: Metabase (and similar) discovery + lineage participation |
| P6 | Kudu/FDW scale for OL/governance projections; Gaius/Aegir drop private facades |
| P7 | Hermes: 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:
- Implement
MemoryProviderwith prefetch, turn sync, session extract, and agent tools (search/store) — backed by Signals AGE/OL (and optional belief facets), not a greenfield DB. - Implement a context engine that compacts with lineage-aware retention (pre-compression extract into Weathership memory, similar spirit to ByteRover’s pre-compression extraction).
- Use federation discovery for Atlas/Ranger/OL bases; emit OpenLineage for memory writes and model ops (authz + provenance).
- Ship as installable Hermes plugins; eventually qualify as an official reasoning-enabled memory service provider in the Hermes ecosystem.
- 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.
Related
- 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.surfacesand pairwisePEERS
Where it runs
| Mode | Location |
|---|---|
| devenv | Local process, :50551 |
| Lab host | signals.service under signals.target |
| Cluster | Deployment 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):
| Root | Role |
|---|---|
| current | Active policy view + live overlay; regenerable via SyncProjection |
| scratch | Edit/review pending promote |
| archive | Freezes of past applied configs |
API-symmetric clients
| Client | Entry |
|---|---|
| CLI | signals-yk / uv run python -m signals.cli.yk |
| MCP | signals-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:
| Piece | Behavior |
|---|---|
| Side-nav SECTION | archive | current | scratch (same roots as projection) |
| QUEUES / POLICY / OPS | Seeds: queue notes, config/queues, ops/diff, ops/nodes, ops/health, Sync |
| Canvas | Horizontal panels (~33rem), openFrom(i) branch, × closes to parent |
| Trail | sessionStorage per layer; URL holds ?root=&open=&partition= focus only |
| Data | Engine 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:
- Read scratch
queues.yaml - Validate via YK
POST /ws/v1/validate-conf(engine-private) - dry_run: optional
kubectl apply --dry-run=serverof ConfigMap patch; no projection write - live: apply ConfigMap → archive current → write scratch → current → best-effort
SyncProjection
ConfigMap adapter (engine-private)
| Env | Default | Meaning |
|---|---|---|
SIGNALS_YK_APPLY_ENABLED | 1 | Set 0 for local-only promote (no kubectl) |
SIGNALS_YK_CM_NAMESPACE | yunikorn | Namespace |
SIGNALS_YK_CM_NAME | yunikorn-configs | Primary CM (runtime override) |
SIGNALS_YK_CM_FALLBACK | yunikorn-defaults | If primary missing |
SIGNALS_YK_CM_KEY | queues.yaml | Data 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.
| Reason | When |
|---|---|
PREEMPTED | YK / kubelet ended the sentinel |
COMPLETED | work finished |
ORPHAN | missed heartbeat (SIGKILL / node loss fallback) |
UNIT_STOP | peer-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
Yieldbodies (they implement the same RPC; Gaius maps ontoCompleteWorkload/ 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
- Holding is not terminal. YK
Completingstill owns theapp-id. Deploymentspec.replicasstill 1 still owns the ReplicaSet. - Ill-posed claims resolve false. Asserting
yk-apps-completedfromstartorscalingis a demerit even if a GET happens to look empty. - Uncalibrated is 0.5 risk, not 0. No track record → honest ignorance.
- 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.
| Command | Role |
|---|---|
python -m signals.ops methods | List registered methods |
python -m signals.ops method [name] | JSON method (states, holding, well-posed probes) |
python -m signals.ops risk --state apps_completing | Forecast risk for well-posed probes here |
python -m signals.ops report [--axis method|implicit|both] | Ledger Brier table |
python -m signals.ops redeploy | Walk 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 R | First 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
| Piece | Where |
|---|---|
| Brier + α shrink | synth engine/calibration.py (Signals DST discount is the parent) |
| Converge / fail loud | cyberphy zarf/converge CLOSURE |
| This walk | src/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:
- Outerbounds AWS K8s deployment
(
eks_airflowTerraform path preferred for us) - Metaflow Airflow scheduling · Outerbounds Airflow ops
- Outerbounds event triggering (stock stack uses Argo Events; we substitute Knative Eventing)
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:
| Layer | Choice | Why |
|---|---|---|
| Metadata service | Outerbounds metadata service + shared PG (platform DB) | Central run tracking for all engines |
| Artifact store | RustFS (S3 API) under SIGNALS_DATA_ROOT | Already platform object plane |
| Task compute | @kubernetes pods on RKE2 | Only compute layer Airflow integration supports |
| Pod scheduler | YuniKorn queues root.{aegir,atelier,gaius,signals,hermes} | Scarce GPU/CPU governance |
| Production DAG schedule | Apache Airflow (airflow create) | Already in Signals components; no Argo Workflows SoR |
| Event / reactive trigger | Knative Eventing → Airflow DAG trigger | Platform event fabric; matches Gaius need for trigger without Argo |
| UI | signals-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
| Field | Value |
|---|---|
| Path | components/metaflow |
| Remote | git@github.com:weathership/oss-metaflow.git |
| Branch | rch/devenv (created from upstream master pin; platform patches land here) |
| Init | git 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 pain | Platform remedy |
|---|---|
| Competing Tilt/Helm Metaflow on shared node | One Zarf/federation-aligned package + YK queues |
| ImagePull / registry thrash | Platform images in Zarf registry |
| No fair-share with sentinels / other engines | YK multi-tenant queues |
| Cron-only if we only use Airflow stock Metaflow triggers | Knative Eventing for external + inter-flow events |
| Opaque ops UI | signals-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.
| Setting | Lab value | Law |
|---|---|---|
METAFLOW_DEFAULT_DATASTORE | s3 | local is refused |
METAFLOW_DATASTORE_SYSROOT_S3 | s3://metaflow/metaflow | Signals RustFS bucket only |
METAFLOW_S3_ENDPOINT_URL | http://127.0.0.1:9010 | RustFS; 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 (
signalsor dedicatedmetaflowDB 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
~/.metaflowconfigshape):
{
"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.platformfor control-plane services; inference steps useroot.internal.inference.*, not a project parent). - Lab/platform pin: Airflow 3.1.7 (
components/airflowchart);cncf.kubernetesprovider for KPO. - Metaflow steps →
KubernetesPodOperatoronly (@kubernetes, not@batch). - Deploy path:
python flow.py --with retry airflow create flow_dag.pythen ship DAG into Airflowdags/(Git-sync or ConfigMap pipeline). - Time schedules: Metaflow
@scheduleas 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)
| Concern | Stock Outerbounds | Signals |
|---|---|---|
| Event bus | Argo Events + Jetstream | Knative Broker (e.g. MT channel / Kafka later) |
| Ingest | Argo webhook | Knative Broker HTTP + domain sources |
| Filter | Argo sensors | Trigger filters on CloudEvent type / attributes |
| Action | Submit Argo Workflow | Trigger Airflow DAG run (+ optional ksvc) |
Example event types (illustrative):
dev.metaflow.flow.finished— inter-flow chaining (@trigger_on_finishanalogue)dev.signals.sentinel.activated— sentinel woke; start inspection flowdev.gaius.article.curate.requested— product CLI/MCP publishdev.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)
| Mode | When | Metaflow service | Scheduler | Trigger |
|---|---|---|---|---|
| Engine isolation | Single-repo devenv, offline | Local Tilt/PG (Gaius-style) | Local / optional K8s | CLI/MCP |
| Platform | Shared RKE2, multi-engine | Platform metadata + RustFS | Airflow + YK pods | Knative 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)
| Artifact | Path |
|---|---|
| Submodule | components/metaflow (rch/devenv) |
| K8s manifests | config/k8s/metaflow/ |
| Bootstrap | scripts/metaflow_platform_bootstrap.sh · just metaflow-platform |
| Client profile | config/metaflow/platform.json |
| RustFS bucket | metaflow (devenv rustfsBuckets) |
| Lab URL | http://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)
| Artifact | Path |
|---|---|
| Submodule chart | components/airflow/chart (appVersion 3.1.7) |
| Helm values | config/k8s/airflow/values-signals.yaml |
| Host PG bridge | config/k8s/airflow/host-bridge.yaml |
| Cross-ns RBAC | config/k8s/airflow/rbac-metaflow.yaml (KPO → metaflow) |
| CI DAG | config/k8s/airflow/dags/signals_ci_dag.py |
| Bootstrap | scripts/airflow_platform_bootstrap.sh · just airflow-platform |
| CI gate | just airflow-platform-ci |
| Lab URL | http://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)
| Artifact | Path |
|---|---|
| Eventing manifests (v1.23.0) | zarf/federation/manifests/knative/eventing-*.yaml (+ IMC + MT broker) |
| Platform ns | signals-events |
| Broker | signals-events/default (MTChannelBasedBroker / in-memory) |
| Sink | airflow-dag-trigger (CE HTTP → AF3 JWT + /api/v2/dags/{id}/dagRuns) |
| Triggers | smoke, metaflow.finished, gaius.curate, … |
| Bootstrap | scripts/knative_eventing_bootstrap.sh · just knative-eventing |
| Publish helper | scripts/signals_events_publish.sh · just events-publish |
| CI gate | just 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
- Airflow executor: start
LocalExecutor/CeleryExecutorvs fullKubernetesExecutor(DAG volume sync cost on single-node lab). - Event exactly-once: Broker + Airflow dagRun
dag_run_ididempotency keys from CloudEventid. - Namespace layout: one
metaflowns vs per-engine namespaces for pods with YK queue labels only. - UI: Metaflow UI (Outerbounds) optional lab validation vs signals-ui only.
Related
- signals-federation Zarf — YK + Knative Serving
- Airflow component stub
- signals-ui Applications — workloads surface
- Gaius Metaflow service (isolation reference)
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)
| Domain | Service | Role | Lab surface |
|---|---|---|---|
| Identity | Kerberos KDC | Data-plane authn | just bootstrap |
| Data SoR | PostgreSQL 16 | Catalog, AGE, Ranger, Metaflow DB | :5455 |
| Object store | RustFS | S3 for artifacts, Metaflow datastore, Iceberg warehouse | :9010 |
| Iceberg catalog | Polaris | REST catalog; table data on RustFS | :8181 / :8182 |
| Governance | Atlas (+ Marquez-web validator) | Metadata + OL SoR | :21010 / :21011 |
| Authz | Ranger | Tag/resource policies | :6080 |
| Storage / SQL | Kudu + Impala | Analytic tables | Kudu / HS2 :21050 |
| Federation schedule | YuniKorn | Multi-tenant pod admission | REST NodePort :30080 |
| Scale-to-zero / event fabric | Knative Serving + Eventing | Sentinels + CE→Airflow | Serving + signals-events Broker |
| Workflow production | Airflow 3 | Metaflow DAG orchestration | NodePort :30800 |
| Flow metadata | Metaflow service | Platform runs for any engine | NodePort :30180 |
| Control UI | signals-ui | YK ⊇ backplane | :9889 |
| GPU telemetry | DCGM exporter + engine OTel yield | Live 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)
| Item | Why out of critical plane |
|---|---|
| Gaius / Aegir engine processes | Federated consumers, not platform SoR |
| Marquez DB / stock Marquez API | Rejected — Atlas OL is lineage SoR |
| Argo Workflows / Argo Events | Rejected — Airflow + Knative Eventing |
| Engine-local Metaflow Tilt | Isolation mode outside this tree |
Enforcement
| Tool | Behavior |
|---|---|
devenv up -d | Turn-key entry: full process graph + stack-ready before signals-ui |
scripts/signals_stack_preflight.sh | Host (PG/RustFS/Atlas/Kudu/Impala) + RKE2; auto Metaflow/Airflow |
scripts/signals_ready.sh | Check-only Gaius-style PASS/WARN/FAIL oneshot (peers / systemd) |
scripts/data_plane_preflight.sh | Binary gate before Kudu/Impala (no compile) |
scripts/devenv_process_assert.sh | Fail if process graph is a partial up |
just stack-ready | Preflight + optional auto-bootstrap (also automatic on up -d) |
just signals-ready | Check-only readiness; critical includes Kudu + Metaflow |
signals:kerberos-bootstrap | Wait for KDC → keytabs + kinit before Kudu/Impala |
signals:federation-ready | YK + Knative (subset) |
signals:metaflow-platform / airflow-platform | Platform RKE2 services |
just redeploy | Refresh 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
| Surface | Role |
|---|---|
| signals-ready | Foundation readiness (PASS/WARN/FAIL). Exit 0 iff critical set is ready. |
| *-ci | Elevated continuous-integration gates (just airflow-platform-ci, DAG ids signals_ci, CE dev.signals.eventing.ci) |
| smoke | One-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
Related
- Platform Metaflow
- Peer integration — systemd group + external engines
- signals-federation Zarf
- DCGM, the warehouse, and agent exchange — cluster flow; the live yield is not the store
- Development environment
- Sample units:
infra/systemd/· contract:config/platform/peer-contract.json
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:
| Operation | Standard Path | HMS-Free Path |
|---|---|---|
| CREATE TABLE (Kudu) | HMS → Kudu | KuduCatalogOpExecutor → Kudu + PG registry |
| CREATE TABLE (Iceberg) | HMS → Polaris | IcebergCatalogOpExecutor → Polaris — PENDING (see below) |
| DROP TABLE | HMS → storage | SignalsDdlExecutor → storage + PG registry |
| Load metadata | HMS getTable() | TableLoader.loadHmsFree() → Kudu master |
| CREATE/DROP DATABASE | HMS | KuduMetaProvider → 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):
| File | Role |
|---|---|
SignalsDdlExecutor | Routes DDL to Kudu or Iceberg without HMS |
KuduMetaProvider | PostgreSQL-backed catalog registry (databases + tables) |
CatalogOpExecutor | HMS-free DDL block before standard switch statement |
TableLoader | loadHmsFree() constructs metadata from Kudu master |
KuduTable | Handles null msClient in load() |
Frontend | Skips MetaStoreClientPool when HMS-free |
JniCatalog | Zero-size pool in HMS-free mode |
CatalogServiceCatalog | Seeds 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:
| Name | Store | Role |
|---|---|---|
*_tier0 | Kudu on $SIGNALS_DATA_ROOT/kudu | Hot land; expire by DROP RANGE PARTITION |
*_tier1 | Iceberg 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.
| Metaflow | details fact (e = product) | Meaning |
|---|---|---|
current.flow_name / run_id | flow_name, run_id, pathspec | Run identity |
| code package URL / sha | code_package, code_package_sha | Immutable source tarball |
{sysroot}/{flow}/data (CAS) | data_uri | Content-addressed artifacts |
{sysroot}/{flow}/{run} | snapshot_uri | Run prefix (task metadata + pointers) |
@conda / @pypi / default | deps | Environment lock (empty = default) |
YK proxy or @kubernetes | yk_app_id, yk_queue | Admission claim |
| platform profile | datastore, datastore_root, rustfs_endpoint | Must 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)
| Product | Peer | Kind |
|---|---|---|
signals.metaflow.snapshots | Signals | Per-run snapshot (code, data, deps) |
gaius.cognition.outputs | Gaius | Cognition |
gaius.prospects.corpus | Gaius | Prospects SEC/FMP corpus (compact + summary on RustFS) |
aegir.usd-corpora | Ægir | Corpus |
aegir.models.bespoke | Ægir | Fine-tuned / bespoke models |
atelier.classification.embeddings | Atelier | Classification parquet / embeddings |
Events → agent
CloudEvent type: dev.signals.dataproduct.updated.
On create / update / maintain, walk method data-product.history-review:
event_received→reviewing→understood- Activate ACP: Grok-Subscription (
root.external.subscription.rate-limited) or Grok-Local with Qwen3.8capability=[thinking,vision] - Brief must cover quality, lineage, and delta (and
nominal for
signals.metaflow.snapshots) - Persist a
tx+detailsasserts; persist agent work onhx_*keyed totx_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
| Layer | Engine | Access Pattern |
|---|---|---|
| Graph | PostgreSQL + AGE | Relationship queries, ontology traversal |
| Columnar | Kudu | Low-latency random access, time-series (hot tier) |
| SQL | Impala | Distributed analytical queries (HMS-free) |
| Table Format | Iceberg | Large-scale batch analytics, schema evolution (warm tier) |
| Metadata | Atlas | Data 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:
- Dask workers complete computation
- Datashader rasterizes the result
- HoloViews composes the view specification
- Engine streams the update over gRPC
- 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
| Concern | Authority | Notes |
|---|---|---|
| Governance (types, classifications, policies) | Atlas on signals PG + AGE | /api/atlas/* |
| Runtime lineage (Job / Run / Dataset, facets) | Same Atlas/AGE store | OL types + edges |
| OL HTTP ingest | Atlas OL extension | POST /api/v1/lineage |
| Marquez-compatible API | Atlas OL extension | /api/v1/* and /api/v2beta/* |
| OpenLineage UI | Marquez-web | :21011 (Atlas HTTP + 1); proxies /api/v1 and /api/v2beta |
| Heavy lineage analytics | FDW → Impala → Kudu | Atlas 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:
- Marquez-web renders jobs, datasets, events, search, stats, tags, and lineage graphs.
- Marquez API contract tests can point at Atlas.
- 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
| Client | Constraint |
|---|---|
Atlas REST v2 (/api/atlas/*) | No breaking changes |
| rdbms_* / classifications | Additive only |
| AGE labels | New OL labels additive |
| Native Atlas lineage GUI | Continues on existing lineage APIs |
| Marquez-web / OL HTTP clients | Talk only to Atlas /api/v1/* (+ v2beta search) |
Marquez API surface on Atlas
Implemented in components/atlas (OpenLineageServlet + OpenLineageStore):
| Area | Paths (representative) |
|---|---|
| Health | GET /api/v1/health |
| Namespaces | GET/PUT/DELETE /api/v1/namespaces[/{ns}] |
| Jobs | GET /api/v1/jobs, GET/PUT/DELETE .../namespaces/{ns}/jobs[/{job}] |
| Runs | GET .../jobs/{job}/runs, GET /api/v1/jobs/runs/{id}, facets, start/complete/fail/abort |
| Datasets | GET/PUT/DELETE .../namespaces/{ns}/datasets[/{ds}], versions |
| Tags | GET/PUT /api/v1/tags[/{name}], entity/field tag attach/detach |
| Events | GET /api/v1/events/lineage |
| Lineage | GET/POST /api/v1/lineage, GET /api/v1/column-lineage, GET /api/v1/runlineage/upstream |
| Search | GET /api/v1/search, GET /api/v2beta/search/jobs, .../datasets |
| Stats | GET /api/v1/stats/{lineage-events,jobs,datasets,sources} |
| Sources | GET/PUT /api/v1/sources[/{name}] |
| Ingest | POST /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
| Piece | Role |
|---|---|
components/marquez | Source submodule (UI + API contract reference) |
languages.javascript | directory = components/marquez/web; npm.install.enable (enterShell) |
tasks.marquez:build-web | Before devenv:processes:marquez-web — npm + webpack; turn-key devenv up [-d] |
processes.marquez-web | Default stack — always starts with devenv up; :21011 (Atlas + 1), depends on Atlas |
SIGNALS_OL_API_HOST / PORT | Atlas (:21010); UI proxies /api/v1 + /api/v2beta there via setupProxy.js |
MARQUEZ_WEB_PORT | Override 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.
Producer validation: Flink
Upstream Apache Flink is a first-class OpenLineage integration; Cloudera’s stack already proves Flink → Atlas. Dual-path lab check:
| Path | Expectation |
|---|---|
| OpenLineage | Flink OL → Atlas OL ingest → visible in Marquez-web (via Atlas API) |
| Atlas | Entities / processes remain queryable via stock Atlas clients |
BDD @tier-1 (planned): features/platform/lineage_flink_ol.feature.
Phased delivery
| Phase | Deliverable |
|---|---|
| M0 | Submodule + this doctrine |
| M1 | marquez-web default stack process + build-web (UI → Atlas /api/v1) |
| M2 | Atlas webapp complete Marquez-compat /api/v1/* + /api/v2beta/* into signals PG/AGE |
| M3 | Flink dual-path tests green (OL → Atlas /api/v1; Atlas clients still healthy) |
| M4 | Kudu/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.
Related
- 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
| Stream | Partition key | Events |
|---|---|---|
atlas.entity | guid (16-byte UUID or hex string of same) | create, update, soft-delete, QN/type rename |
atlas.edge | stable edge id or sha256(src‖elabel‖dst) | create, delete |
atlas.classification | guid | assign, 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 change | Kudu ops |
|---|---|
| Create/update entity | UPSERT entity_flat; UPSERT entity_by_qn for current digest |
| QN or type_name change | Ordered DELETE old digest + UPSERT new digest + UPSERT flat |
| Soft delete / purge | DELETE/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
| Change | Kudu ops |
|---|---|
| Create edge src -[:L]-> dst | UPSERT edge_out (src,L,dst,dst_type); UPSERT edge_in (dst,L,src,src_type) |
| Delete edge | DELETE both sides with full (src, elabel, dst) |
Delete decoding prerequisite
If using PostgreSQL logical decoding on AGE label tables:
- Default
REPLICA IDENTITY DEFAULTemits only pkey (graphid) on DELETE — insufficient for Kudu. - Required before wiring: either
ALTER TABLE … REPLICA IDENTITY FULLon 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
| Change | Kudu ops |
|---|---|
| Assign tag T to guid G | UPSERT entity_classifications (T, G, …) |
| Remove tag | DELETE 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
| Allowed | Not allowed (v1) |
|---|---|
| Impala HS2 UPSERT/DELETE from outbox worker | FDW INSERT/UPDATE/DELETE as SoR |
| Native Kudu client from worker | Multi-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
| Layer | Role |
|---|---|
| Atlas / Ranger processes | Always talk to Postgres (JDBC) — topology, admin, policy UI, Cypher |
| Postgres 16 + AGE | Thin SoR for AGE graph + Ranger admin; FDW client for scale reads (:5455) |
| impala_fdw | How this Postgres leverages Kudu without relocating Cypher or policy admin |
| Kudu projections | High-volume derived entity/tag/edge/audit tables |
| RustFS | Object/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
| Plane | Store | Holds | Not for |
|---|---|---|---|
| Topology SoR | Postgres + AGE | Vertices/edges, Cypher, hot single-entity paths | Bulk multi-engine scans |
| Policy admin SoR | Postgres ranger | x_policy*, defs, admin UI | High-QPS tag membership heap scans |
| Scale projections | Kudu, exposed as PG foreign tables | entity_flat, by_qn, edges, classifications; Ranger tag denorm | openCypher graph |
| Object / blob | RustFS | artifacts, lineage packages, Weathership memory, backup objects | Relational SoR |
Data products + hx | Kudu tier0 + RustFS Iceberg tier1 (details/tx/hx views) | fact log, tx, hx | Postgres AGE / any extra PG copy |
| Logical packages | DataFusion (signals-df) | portable Parquet backup/verify | Live multi-writer SoR |
Atlas → Kudu (via Postgres FDW)
| Artifact | Path |
|---|---|
| DDL (Kudu) | config/atlas/kudu_projections.sql |
FDW (on Postgres :5455) | config/atlas/kudu_projections_fdw.sql |
| Seed | just atlas-kudu-projections-seed |
| Outbox contract | Atlas → 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”:
- Outbox worker (per-key order, at-least-once)
- True
kudu_scan/ libkudu_client for pk_lookup latency - Atlas/AGE and app SQL prefer foreign tables for bulk entity/tag joins
Ranger → Kudu (via Postgres FDW)
| Artifact | Path |
|---|---|
| DDL (Kudu) | config/ranger/kudu_projections.sql |
FDW (on Postgres :5455) | config/ranger/kudu_projections_fdw.sql |
| Seed | just 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)
| Item | Value |
|---|---|
| Data dir | $SIGNALS_DATA_ROOT/rustfs (default /raid/signals/rustfs) |
| S3 API | http://127.0.0.1:9010 path-style |
| Console | http://127.0.0.1:9011 |
| Lab creds | RUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY (secretspec in shared envs) |
| Client | mc → alias local |
| Buckets | signals-artifacts, signals-lineage, weathership-memory, signals-backup, signals-dataproducts |
| Process | processes.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/
Related
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.
- Superset of stock yunikorn-web — YK REST used in
rch-yunikorn-web, plus Signals views (lineage, sentinels, federated surfaces). - Isolated submodule — developed in
weathership/signals-ui, vendored into Signals ascomponents/signals-ui(same pattern as other components). - 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.
- Federation overwatch — MiNiFi sentinels, OTel, Atlas OL lineage, and engine discovery sit beside YK views in one process.
- Engine required —
/readyziszndx.engine.v1.Engine/Statuswithcapability=schedulerhealthy.SIGNALS_UI_ALLOW_NO_YK=1is 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
| Item | Decision |
|---|---|
| Upstream repo | git@github.com:weathership/signals-ui.git |
| Signals submodule | components/signals-ui |
| Language (service) | Rust (edition 2021+, idiomatic: axum / tower / tokio / reqwest / serde) |
| Presentation | Rust-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. |
| Theme | Keiretsu CSS + Kumo token ramp (vendored from Atelier / cldr-design-template); data-theme="keiretsu" + data-mode="dark|light" |
| Port policy | 9889 default (SIGNALS_UI_BIND); yk-web muscle memory. |
| YuniKorn | Required when stack is up (SIGNALS_YK_API_URL). Primary backplane. |
| Brand logos | Pluggable packs (SIGNALS_UI_BRAND): cloudera (default) · weathership · custom · SIGNALS_UI_BRAND_DIR |
| Theme | Keiretsu dark/light (fixed; not brand-switched) |
| Product identity | Signals (brand-agnostic copy/vars) |
| Auth | Lab: 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 route | Capability | signals-ui requirement |
|---|---|---|
/dashboard | Cluster overview, history charts | Required parity |
/applications | Apps by partition/queue; allocations; state log | Required parity |
/queues, /queues-v2 | Queue tree, resources, properties/templates | Required (both layouts or unified equivalent covering all data) |
/nodes | Capacity, allocated/occupied/available, foreign allocs, search | Required parity |
/status | Scheduler healthchecks | Required parity |
| Charts | Donut/area/bar as used by dashboard/nodes | Required functional equivalent (not pixel clone) |
| Error surface | API failure UX | Required |
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:
| Method | Path | Use |
|---|---|---|
| GET | /ws/v1/clusters | Cluster list |
| GET | /ws/v1/partitions | Partitions |
| GET | /ws/v1/partition/{p}/queues | Queue tree + resources/templates |
| GET | /ws/v1/partition/{p}/queue/{q}/applications | Apps, allocations, stateLog, times |
| GET | /ws/v1/partition/{p}/nodes | Nodes + allocations + foreignAllocations |
| GET | /ws/v1/history/apps | App count time series |
| GET | /ws/v1/history/containers | Container count time series |
| GET | /ws/v1/scheduler/node-utilizations | Utilization buckets |
| GET | /ws/v1/scheduler/healthcheck | Health 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)
| Extension | Source | Notes |
|---|---|---|
| App ↔ sentinel ↔ engine | MiNiFi C2, Knative, tags | Overwatch |
| App ↔ OTel duration/stages | Collector / Tempo-compatible API | Uniform process proxy |
| App ↔ OpenLineage run | Atlas /api/v1 | Semantic I/O lineage |
| History (data products) | Catalog + dev.signals.dataproduct.updated | Agent-facing quality / lineage / delta |
| Lineage browse (table/column) | Atlas OL | Facet inside a product, not the top-line menu |
| Sources / schema / versions | Atlas-enriched OL | Same |
| Federated engine board | Discovery + zndx.engine.v1 | Peers |
| OTel routing views | Collector config + traces | S04 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)
| Stock | signals-ui |
|---|---|
| Angular 13 + Node toolchain as runtime | Rust service; no Node runtime |
| Material / ad-hoc SCSS | Keiretsu + Kumo |
| json-server as product default | Live YK (fixtures only for tests) |
| Scheduler-only product | Control-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
| Concern | Implementation |
|---|---|
| HTTP server | axum + tower-http (trace, cors, compression, static) |
| YK proxy/client | Typed client; optional reverse-proxy path for raw /ws/v1/* |
| BFF merge API | e.g. GET /api/signals/v1/processes/{id} → YK + OTel + OL |
| Config | env + optional HOCON/YAML; SIGNALS_YK_API_URL, SIGNALS_ATLAS_HTTP_*, OTel, C2 |
| Health | /healthz (liveness), /readyz (Engine/Status scheduler healthy) |
| Metrics | Prometheus /metrics (idiomatic for control plane) |
2.3 Presentation options (choose in Phase 0 spike)
| Option | Pros | Cons |
|---|---|---|
| A. Leptos (CSR/SSR) + Trunk | Full Rust, reactive, one language | WASM size; learning curve |
| B. Dioxus | Fullstack Rust story | Ecosystem younger |
| C. Axum + Askama/Maud + HTMX | Simple, tiny, very idiomatic ops UI | Less “app-like” interactivity |
| D. Axum serves prebuilt WASM/static | Clear split | Two-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):
| Asset | Location |
|---|---|
| Keiretsu CSS ramp | Atelier ui/src/styles/theme-keiretsu.css (upstream: cldr-design-template) |
| Kumo ramp values | Atelier ui/src/theme/kumo.ts (restated as CSS vars in signals-ui) |
| Color mode | data-mode="dark" | "light" independent of data-theme="keiretsu" |
Laws
- Elevation is lightness — canvas < base < elevated
- Accents on accent duty only — brand for primary/link/focus
- 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.
| Gap | Atlas work | signals-ui view |
|---|---|---|
| Column lineage | Facet → /api/v1/column-lineage | Lineage module |
| Schema / fields | Facets + rdbms_column | Dataset detail |
| Versions | Event fingerprint / entity version | History tabs |
| Sources | Upsert lineage_sources | Sources catalog |
| Duration / last-N | OL 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
| Signal | Source | UI |
|---|---|---|
| Admission / queues | YuniKorn | YK-superset views |
| Claim / scale-to-zero | Knative + YK app state | Sentinel + app detail |
| C2 health | MiNiFi | Sentinels module |
| Duration / stages | OTel | Merged process timeline |
| Semantic I/O | OpenLineage → Atlas | Lineage 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-ykclient, health, config - Vendor Keiretsu CSS; dark/light shell page
- Add submodule
components/signals-uiin Signals - devenv process +
cargotask; 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.jsonfixtures 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-goal | Why |
|---|---|
| Node.js service or Node-based devenv dependency for signals-ui | Explicit departure |
| Investing in or owning Angular yunikorn-web | Apache 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 plane | Wrong ownership; temporary validator only |
| Reimplementing YuniKorn scheduling | Client of /ws/v1 only |
| Subset “dashboard-only” replacement of yk-web | Must 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
- Create
weathership/signals-uiempty repo + Apache-2.0 (or project) license - Scaffold Axum hello + YK client crate + Keiretsu static shell
- Import yk-web
json-db.jsonas fixtures; first contract test (/ws/v1/partitions) - Submodule add into Signals when skeleton builds
- Presentation spike (Leptos vs HTMX) ≤ 1 week, then freeze
- Parallel in Atlas: column-lineage facet spike (feeds Phase 2–3)
Related
- 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
| Layer | Mechanism | Purpose |
|---|---|---|
| Edge / human & agent access | Cloudflare Zero Trust + IdPs (Okta OIDC, GitHub OAuth) | Reach apps and APIs without “open network + hope” |
| Cluster / data plane | Kerberos (lab/devenv required; FQDN SPNs) | Impala, Kudu, and related clients — no NOSASL |
| Governance metadata | Apache Atlas | Classifications, types, SoR for “what is this data” |
| Authorization | Apache 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
| Capability | Why ZT / robust HTTPS matters |
|---|---|
| Agent observation of synthesizer chains | Browser sensors + web UI must be secure-context; multi-device observers |
| WebRTC A/V | DTLS/SRTP assumes proper HTTPS origin and stable identity for signaling |
| Nvidia-class omni models | Multi-modal streams (A/V + text) across federated sites — cannot run on ad-hoc open ports |
| Signals governance UIs | Atlas/Ranger/Marquez-web and future OL surfaces behind the same edge discipline |
| Federated engines + OIP | Discovery 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:
- Cloudflare Zero Trust (or equivalent) in front of every human/agent web surface.
- Okta / GitHub as IdPs into Access policies.
- Kerberos (or cloud IAM) for service-to-service data plane.
- Atlas classifications → Ranger policies for data authorization.
Signals today (lab)
| Concern | State |
|---|---|
| Bootstrap | just bootstrap — Kerberos KDC, keytabs, FQDN, kinit (required) |
| Impala / Kudu | Kerberos only; GSSAPI clients via signals.impala |
| Atlas / Ranger | Authz spine; portable SoR via just backup / just restore |
| Edge ZT | Not 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
Related
- 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:
synthdocs 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.
| Mode | Compute | K8s | Services |
|---|---|---|---|
| Laptop | Local | k3d (optional) | devenv |
| Workstation | Local | RKE2 (optional) | devenv + GPU |
| Hybrid | Local + AWS | RKE2 (local) + RKE2 (AWS) | Split |
| Full AWS | AWS EC2 | RKE2 | All 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:
| Layer | Tool | Purpose |
|---|---|---|
| Provisioning | OpenTofu | AWS VPC, EC2, S3, Cloudflare |
| Configuration | Ansible | RKE2, Dask, services, tunnels |
| Packaging | Zarf | Air-gap K8s deployment |
| Dev Iteration | Tilt | Live-reload on RKE2/k3d |
| Policy | OPA/Conftest | Guardrails 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
-
Tables created in Impala become visible in Atlas as entities with columns, types, and ownership metadata.
-
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.
-
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 ContactInformationwithSIGDG:1030 Confidential - Column
ssn→SIGDG:0011 GovernmentIdentifierwithSIGDG:1040 Restricted - Column
total_amount→SIGDG:0021 FinancialInformationwithSIGDG:1020 Internal - Table
transactions→SIGDG:0050 TransactionInformation
- Column
-
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 interimhive_*) - 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:
- Ranger policies reference SIGDG classifications (e.g., “mask all columns tagged
SIGDG:0022 PaymentInformation”) - Impala enforces Ranger policies at query time
- 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):
- Atlas catalog bridge — Python function registers Impala-managed Kudu tables in Atlas via REST API. Validated by 4 catalog sync scenarios.
- Tagger service —
Taggerclass (src/sigint/tagger.py) orchestrates sample → classify → tag:ImpalaSamplerreads column names, types, and sample values from ImpalaEmbeddingClassifierclassifies against the SIGDG taxonomy using sentence-transformer embeddingsAtlasClientwrites classifications back as SIGDG tags with confidence scores and evidence
- 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). - 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
- Event-driven tagging: Watch Atlas for new entities and trigger classification automatically (replace manual
just taginvocation) - Evidence fusion calibration: Calibrate DST discount factors and mass constants against held-out data; resolve source independence assumption (see Research Roadmap)
- OWL formalization: Publish SIGDG as OWL ontology with BFO imports, verify via RASE 5-gate pipeline
- Ranger tag-based policies: Column masking and access control driven by Atlas SIGDG classifications
- Feedback loop: Analyst corrections to classifications improve the model over time
- 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:
- Know which features actually contribute to correct predictions
- Measure how much accuracy degrades when each feature is removed
- Systematically reduce reliance on name matching
- Compare multiple classification methods on the same feature set
Feature Decomposition
Each column is represented by 12 named features extracted from its metadata:
| # | Feature | Source | Example |
|---|---|---|---|
| 1 | column_name | Humanized column name | "payment card number" |
| 2 | column_type | Data type (suppressed for string/varchar) | "int" |
| 3 | sample_values | Up to 5 sample values, truncated to 80 chars | "4111..., 5500..." |
| 4 | cardinality | Distinct value count in sample | "cardinality=42" |
| 5 | null_ratio | null_count / total_count | "null_ratio=0.15" |
| 6 | value_entropy | Shannon entropy of value lengths (bits) | "entropy=2.34" |
| 7 | pattern_signals | Detected regex patterns | "patterns: email_pattern, uuid_pattern" |
| 8 | avg_value_length | Mean character length | "avg_len=24.5" |
| 9 | numeric_ratio | Fraction parseable as number | "numeric=0.95" |
| 10 | sibling_context | Humanized names of other columns in same table | "siblings: first name, last name, email" |
| 11 | source_table | Table name | "table=identity_data" |
| 12 | value_description | NL 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:
| Pattern | What it matches |
|---|---|
email_pattern | user@domain.tld |
phone_pattern | +1 (555) 123-4567 |
ssn_pattern | 123-45-6789 |
ipv4_pattern | 192.168.1.1 |
uuid_pattern | 550e8400-e29b-41d4-a716-446655440000 |
date_iso_pattern | 2024-01-15 |
url_pattern | https://example.com |
credit_card_pattern | 4111111111111111 |
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:
-
Feature index matrix:
Xhas shape(N, 12)whereX[i, j] = i— each sample initially uses its own value for every feature. -
Value lookup tables: For each feature
j, a table maps sample index → feature text. When SAGE permutesX[i, j]to indexk, the model uses samplek’s value for featurejinstead. -
Model wrapper (
FeatureMaskModel): Looks up text from the per-feature tables according toX, assembles embedding text, encodes via sentence-transformers, and returns softmax’d cosine similarities as class probabilities. -
SAGE estimation:
MarginalImputer+PermutationEstimatorwith cross-entropy loss estimates each feature’s Shapley value acrossn_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:
| Signal | Method | Semantic Names | Opaque Names | Overall |
|---|---|---|---|---|
| Cosine | Zero-shot embedding similarity + name boost | High | Low | Moderate |
| CatBoost CV | Augmented stratified k-fold CV | Moderate | Low | Moderate |
| CatBoost train→eval | Synthetic training + ordered boosting + column propagation | High | High | High |
| LLM GT | Expert 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 Group | Columns | Description |
|---|---|---|
| Identity | embedding_text, source_table, column_name, sample_values, column_kind | Column metadata and kind |
| Cosine | tag_code, tag_label, tag_abbrev, confidence, boost | Zero-shot cosine predictions |
| Ground Truth | gt_code, correct | LLM GT evaluation (when provided) |
| CatBoost CV | ml_tag_code, ml_tag_label, ml_confidence, ml_correct | Cross-validated ML predictions |
| Features | feat_column_name … feat_value_description (×12) | Transparency: input features as strings |
| SAGE | sage_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:
- 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
- Feature refinement — richer pattern detectors (date formats, currency symbols, statistical distributions), deeper sample analysis (value distributions, min/max/mode)
- Cross-dataset validation — accuracy on datasets with different naming conventions and taxonomies (see GitTables CTA benchmark)
- 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)]
| Component | Dimensions | Description |
|---|---|---|
| Full embedding | 384 | Sentence embedding of all features (name + values + table + siblings) |
| Value-only embedding | 384 | Sentence embedding with name, table, and siblings stripped |
| Discrete features | 12 | Cardinality, null ratio, entropy, 8 pattern flags, value description — scaled by \(\sqrt{384/12}\) |
| Cosine similarities | N | Similarity 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:
| Technique | Overall | Data Cols | Ann Cols |
|---|---|---|---|
| Baseline (k-fold CV) | 48.0% | — | — |
| Cosine-only (5-source DST) | 84.6% | — | — |
| Synthetic train→eval + dual embedding | 83.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
| XGBoost | CatBoost | Notes |
|---|---|---|
objective="multi:softprob" | loss_function="MultiClass" | Both produce probability vectors |
n_estimators | iterations | Number of boosting rounds |
max_depth | depth | Maximum tree depth |
reg_alpha | l2_leaf_reg | L2 regularization |
colsample_bytree | rsm | Random subspace method (feature sampling) |
min_child_weight | min_data_in_leaf | Minimum samples per leaf |
| — | posterior_sampling=True | Ordered 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:
| Column | Feature Group | Description |
|---|---|---|
shap_top1_name | Feature group name | e.g., full_embedding, sample_values, column_name |
shap_top1_value | SHAP magnitude | Summed 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:
- An LLM provides initial column classifications (the “ground truth”)
- ML models learn to reproduce those LLM annotations transparently
- 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:
- Round 1: Inject all GT-labeled eval columns with their LLM labels
- Round 2+: Inject columns whose CatBoost confidence exceeds
--self-train-threshold(default 0.80) using CatBoost’s predicted labels as pseudo-labels - Repeat until no new columns qualify or round limit reached
Embedding encoding happens once before the loop — only CatBoost training repeats per round.
Commands
Single-command workflow (recommended)
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:
-
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.
-
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.
-
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.
-
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.
-
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.
| # | Heuristic | Observation | Implementation | Generalizes? |
|---|---|---|---|---|
| 1 | Dual embedding | Annotation columns have opaque names; the full embedding encodes a misleading name signal | Second embedding with name, table, and siblings stripped (value-only); concatenated as 384 additional dims | Yes |
| 2 | Category reference augmentation | Many classes with few real samples each; CatBoost cannot learn class boundaries | Inject taxonomy reference embeddings as anchor training points | Yes |
| 3 | Cosine similarity features | Cosine similarity to category references is a strong zero-shot signal on semantic names | Encode per-category cosine similarities as CatBoost input features, bridging zero-shot and trained classifiers | Yes |
| 4 | Column propagation | Structurally related columns share the same category; one may have a confident prediction while the other does not | Post-prediction pass: propagate confident predictions to uncertain structurally-related columns | No — dataset-specific |
| 5 | Value description | Generic names (col0, field_1) have no semantic content; cosine similarity on these is random | Substitute NL descriptions of value patterns when the column name is generic (12th SAGE feature) | Yes |
| 6 | Discrete feature scaling | 12 discrete features are ignored by gradient boosting when 384-dim embedding vectors dominate split gain | Scale discrete features by \(\sqrt{384/12}\) so magnitudes compete | Yes |
| 7 | Synthetic data generation | Evaluation data generation process is known or inferable; reverse-engineering it produces unlimited training data | 70+ value generators covering all SIGDG categories, with 50/50 semantic/opaque name split | Partially |
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:
| Property | SIGDG evaluation set | GitTables CTA benchmark |
|---|---|---|
| Column names | Mixed semantic (payment_card_number) and opaque (col_42) | 100% generic (col0, col1, empty) |
| Taxonomy | SIGDG (42 categories, 30 leaves, BFO-grounded) | BFO-grounded (122 types mapped from DBpedia) |
| Source | Synthetic + real enterprise data | Academic benchmark (SemTab 2021) |
Method Accuracy by Benchmark
| Method | SIGDG (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:
-
Cosine dominates (semantic names): Semantic column names directly match category labels. Cosine similarity achieves 99.4% accuracy. CatBoost adds noise.
-
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).
-
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 Confidence | Regime | Observed Behavior |
|---|---|---|
| > 0.35 | High — cosine is reliable | Cosine near-certain; CatBoost adds marginal value |
| 0.05 - 0.35 | Medium — both contribute | Standard DST combination is appropriate |
| < 0.05 | Low — cosine has no signal | Cosine 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 Item | Connection |
|---|---|
| R-01: Source Independence | Confidence-gated fusion is the principled response to shared embedding space between cosine and CatBoost |
| R-02: Calibration Experiment | Calibrating the 13 DST constants requires cross-benchmark data — heuristic validation produces this data |
| R-05: Cautious Classification | When no heuristic resolves a leaf-level ambiguity, cautious classification returns the deepest confident hierarchy node |
| R-08: Confusable Pairs | The 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 Type | Count (SIGDG) | Description |
|---|---|---|
| Singletons | 30 | One per leaf category |
| Internal nodes | 12 | Descendant leaf sets for each parent in the hierarchy |
| Confusable pairs | ~10 | Manually specified pairs from error analysis |
| \(\Theta\) | 1 | Full frame (total ignorance) |
| Total | ~53 | vs. \(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:
| Pattern | Category | Rationale |
|---|---|---|
email_pattern | EmailAddress (0076) | user@domain.tld is unambiguous |
ssn_pattern | TaxIdentifier (0085) | XXX-XX-XXXX format is distinctive |
credit_card_pattern | PaymentCardData (0070) | Luhn-valid 16-digit sequences |
phone_pattern | PhoneNumber (0074) | E.164 / national format detection |
uuid_pattern | DeviceIdentifier (0013) | RFC 4122 hex format |
ipv4_pattern | ConfigurationData (0041) | Dotted quad notation |
url_pattern | ConfigurationData (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 Level | Mass on Singleton | Mass on \(\Theta\) | Example |
|---|---|---|---|
| Exact | 0.70 | 0.30 | "tax identifier" = "TaxIdentifier" |
| Abbreviation | 0.50 | 0.50 | "ssn" = "SSN" (abbrev) |
| Word overlap | 0.30 | 0.70 | "customer email address" ⊃ "email address" |
| No match | 0.00 | 1.00 | Vacuous — 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.
| Property | SVM (5th source) | Cosine (1st) | CatBoost (2nd) |
|---|---|---|---|
| Feature space | Sparse TF-IDF (char + word n-grams) | Dense embedding (384-dim) | Dense embedding + discrete features |
| Inductive bias | Maximum-margin separation | Nearest-neighbor similarity | Gradient-boosted decision boundaries |
| Independence from cosine | High (different feature space) | — | Low (shared embedding) |
| Calibration | Platt scaling (CalibratedClassifierCV) | Softmax + fixed discount | Variance-adaptive discount |
| Explainability | Linear feature weights | Embedding similarity scores | SAGE 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 Confidence | Regime | Observed Behavior |
|---|---|---|
| > 0.35 | High — cosine is reliable | Cosine near-certain; CatBoost adds marginal value |
| 0.05 - 0.35 | Medium — both sources contribute | Standard Dempster combination is appropriate |
| < 0.05 | Low — cosine has no signal | Cosine 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:
| Column | Type | Description |
|---|---|---|
dst_belief | float64 | \(Bel\) for predicted leaf singleton |
dst_plausibility | float64 | \(Pl\) for predicted leaf singleton |
dst_uncertainty_gap | float64 | \(Pl - Bel\): width of the belief interval |
dst_conflict | float64 | Dempster conflict \(K\) across sources |
dst_needs_clarification | bool | \(\text{gap} > 0.3\) or \(K > 0.2\) |
dst_evidence_sources | string | JSON: per-source mass summaries |
dst_belief_path | string | JSON: 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
| File | Purpose |
|---|---|
src/sigint/belief.py | FocalElement, BeliefAssignment, dempster_combine(), FrameOfDiscernment |
src/sigint/mass_functions.py | cosine_to_mass(), catboost_to_mass(), pattern_to_mass(), name_match_to_mass(), svm_to_mass() |
src/sigint/svm_classifier.py | SVMClassifier: TF-IDF + LinearSVC + CalibratedClassifierCV pipeline |
src/sigint/classifier.py | HierarchicalClassification with belief methods and from_combined_evidence() |
src/sigint/embedding_classifier.py | classify() orchestration (up to 5 sources) |
src/sigint/category_set.py | HierarchicalCategorySet with tree navigation |
Test Coverage
86 tests cover the DST layer (400 total across the sigint package):
| Test File | Count | Scope |
|---|---|---|
test_belief.py | 26 | Mass functions, Bel/Pl computation, Dempster combination, frame construction |
test_mass_functions.py | 20 | All 5 converters: high-confidence, uniform, vacuous, discount, edge cases |
test_svm_classifier.py | 9 | SVM train/predict, save/load, config, 5-source DST integration |
test_hierarchical_category_set.py | 16 | Tree navigation, backward compatibility, factory functions |
test_hierarchical_classification.py | 15 | Belief 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:
- 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. - 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.
- 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_clarificationflag (\(K > 0.2\) or gap \(> 0.3\)) routes uncertain columns to human review rather than suppressing disagreement. - 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
| Rule | Property | Trade-off |
|---|---|---|
| Yager (1987) | Conflict mass → \(\Theta\) instead of normalization | Conservative; 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 combination | Simple; handles high conflict but loses sequential diagnostics |
| Denoeux cautious (2008) | Weight-based; does not require independence | Theoretically cleanest for our setting; more complex to implement |
| PCR6 (Smarandache-Dezert) | Proportional conflict redistribution | Addresses 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:
| Parameter | Value |
|---|---|
| Columns per call | 50 (configurable) |
| Max tokens | 65,536 (required for reasoning models) |
| Temperature | 0.0 (deterministic) |
| Retry logic | 3 attempts, exponential backoff (2s → 4s → 8s) on 429/502/503/504 |
| Truncation detection | finish_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:
- The LLM label differs from the ML prediction:
llm_code != ml_code - 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 Element | Domain | Indicators | Min Match |
|---|---|---|---|
| PaymentCard | finance | card_number, pan, cvv, expiry, cardholder | 2 |
| PostalAddress | contact | street, city, state, zip, postal_code, country | 2 |
| PersonName | identity | first_name, last_name, full_name, middle_name | 2 |
| ContactInfo | contact | email, phone, mobile, fax, telephone | 2 |
| BankAccount | finance | account_number, routing_number, iban, swift | 2 |
| GovernmentID | identity | ssn, passport, driver_license, tax_id, ein | 2 |
| BiospecimenCollection | healthcare | sample_location, specimen_type, collection_date | 2 |
| PatientDemographics | healthcare | date_of_birth, gender, ethnicity, blood_type, mrn | 2 |
| GeoLocation | spatial | latitude, longitude, coordinates, altitude | 2 |
| DeviceIdentity | technology | device_id, mac_address, ip_address, imei | 2 |
| Credential | security | password, pin, secret, token, api_key | 2 |
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:
- ML prediction with belief interval — what the evidence sources think, with calibrated uncertainty
- Confusable pair — the specific known ambiguity between categories
- 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:
| Backend | Model | Use Case |
|---|---|---|
cerebras | GLM-4.7 (358B MoE, open-weights) | Default — fast inference, reproducible |
anthropic | Claude Opus 4.6 | Cloud deployment |
openai_compatible | Devstral Small 2 / any | Air-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:
| Source | Discount | Independence |
|---|---|---|
| Cosine | 0.30 | Embedding similarity |
| CatBoost | 0.15 | Gradient boosting on features |
| Pattern | 0.10 | Regex detection |
| Name match | 0.30-0.70 | Lexical matching |
| SVM | 0.20 | TF-IDF sparse features |
| LLM | 0.10 | Full 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):
| Key | Default | Description |
|---|---|---|
bootstrap.max_iterations | 5 | Max convergence loop iterations |
bootstrap.k_threshold | 0.2 | DST K threshold for LLM revisit |
bootstrap.uncertainty_gap_threshold | 0.3 | Pl - Bel gap threshold |
bootstrap.coverage_target | 0.95 | Target label coverage |
bootstrap.confidence_floor | 0.5 | Min ML confidence for fallback labels |
bootstrap.initial_sample_fraction | 0.3 | Initial LLM sample size fraction |
bootstrap.propagation_similarity | 0.85 | Cosine sim for label propagation |
bootstrap.max_llm_calls_per_iteration | 500 | Per-iteration LLM call cap |
bootstrap.max_total_llm_calls | 5000 | Total LLM call budget |
bootstrap.columns_per_call | 50 | Batch size per LLM call |
bootstrap.output | build/bootstrap_gt.json | Ground truth output path |
bootstrap.llm_backend | cerebras | LLM provider |
bootstrap.llm_api_key | null | API key (env var fallback) |
bootstrap.llm_base_url | null | Custom base URL |
bootstrap.llm_model | (from llm.model) | Model name |
bootstrap.llm_max_tokens | 65536 | Max response tokens |
bootstrap.llm_discount | 0.10 | LLM evidence discount in DST |
bootstrap.table_aware_batching | true | Group 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
| File | Purpose |
|---|---|
src/sigint/bootstrap_agent.py | 3-phase agent: LLM sweep → ML validation → targeted revisit |
src/sigint/llm_backend.py | LLM abstraction: Anthropic, OpenAI-compatible, Cerebras factory |
src/sigint/schema_discovery.py | 11 DE patterns, FK hints, category co-occurrence refinement |
src/sigint/data_element.py | DataElement, DataElementMember, DataElementCatalog |
src/sigint/mass_functions.py | llm_to_mass() converter |
scripts/bootstrap_classify.py | CLI entry point |
config/base.conf | HOCON 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:
| Feature | File | Scenarios | Description |
|---|---|---|---|
| Embedding Classification | classification/embedding_classification.feature | 7 | Confidence thresholds, hierarchical output, name-boost |
| Evidence Fusion | classification/evidence_fusion.feature | 6 | DST mass combination, conflict detection, belief intervals |
| Feature Extraction | classification/feature_extraction.feature | 5 | 12 SAGE-ablatable features, pattern signals |
| Taxonomy Management | classification/taxonomy_management.feature | 5 | SIGDG hierarchy, sensitivity mapping |
| Vocabulary Mapping | classification/vocabulary_mapping.feature | 4 | Custom term→category overrides |
| Config Lifecycle | classification/config_lifecycle.feature | 4 | HOCON config load, resolve, materialize, validate |
| GitTables Benchmark | classification/benchmark_gittables.feature | 5 | BFO-grounded 122-type evaluation |
| Bespoke Dataset | classification/bespoke_dataset.feature | 4 | Custom dataset classification |
Platform Health (Tier 1 — 22 scenarios)
Infrastructure health checks that validate each service in the devenv stack:
| Feature | File | Scenarios | Description |
|---|---|---|---|
| PostgreSQL | platform/health_postgres.feature | 3 | Connection, AGE + pg_cron + pg_trgm extensions |
| Kerberos | platform/health_kerberos.feature | 3 | KDC ticket granting, keytab validation |
| Kudu | platform/health_kudu.feature | 3 | Master API, tablet servers, table creation |
| Impala | platform/health_impala.feature | 3 | SQL round-trip, DDL, data types |
| Atlas | platform/health_atlas.feature | 10 | REST 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:
| Feature | File | Scenarios | Description |
|---|---|---|---|
| Catalog Sync | platform/integration_catalog_sync.feature | 4 | Impala DDL → Kudu, Atlas catalog bridge |
| Meta-Tagging | platform/integration_meta_tagging.feature | 6 | SIGDG type setup, column tagging, classification search |
| Pipeline Tagging | tagging/pipeline_tagging.feature | 2 | Full Tagger pipeline: sample → classify → tag |
Planned Enhancement
Data Lifecycle (Tier 1 — 5 scenarios, not yet passing)
| Feature | File | Scenarios | Description |
|---|---|---|---|
| Data Lifecycle | platform/data_lifecycle.feature | 5 | Hot→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)
| Scenario | Description | Prerequisites |
|---|---|---|
| S01: Agent-Mediated Visualization | Interactive exploration through web terminal with resolution autoscaling | gRPC engine, Dask, HoloViews |
| S02: Algorithm Extension | Package, deploy, and invoke custom analysis as platform extensions | gRPC engine, extension registry |
| S03: Agent Self-Improvement | Define performance objectives, iterate toward quantitative improvements | gRPC engine, evaluation harness |
Archived features: features_archive/agent/visualization.feature, extension.feature, evolution.feature
Feature Set B: Analytics (S04–S06)
| Scenario | Description | Prerequisites |
|---|---|---|
| S04: OTel Root Cause Analysis | Correlate degradation signals with infrastructure changes | OTel collector, Dask analytics |
| S05: Cybersecurity Investigation | Investigate malicious behavior using correlated log sources | Security data pipeline |
| S06: Streaming Ontology | Ontology-grounded feature discovery from high-velocity streams | Streaming 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
| Domain | Scenarios | Status |
|---|---|---|
| Classification (tier-0) | 40 | All passing |
| Platform health (tier-1) | 22 | All passing |
| Integration + tagging (tier-1) | 12 | All passing |
| Active total | 74 | 74/74 passing |
| Data lifecycle (planned) | 5 | Not yet passing |
| Backlog (S01–S06 + gRPC) | ~29 | Archived |
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
| Tier | Scope | What It Validates | Infrastructure |
|---|---|---|---|
| Tier 0 | Classification pipeline | Embedding classification, evidence fusion, feature extraction, taxonomy, config lifecycle, benchmarks | None (offline) |
| Tier 1 | Component health + integration | Service health, Impala DDL → Kudu, Atlas catalog bridge, meta-tagging pipeline, end-to-end tagging | Full devenv stack |
| Tier 2 | Engine workflows | gRPC engine, agent-mediated visualization | Engine binary (backlog) |
| Tier 3 | Full stack | End-to-end with Dask/HoloViews visualization pipeline | Engine + 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)
| Feature | Scenarios | What It Tests |
|---|---|---|
embedding_classification.feature | 7 | Embedding classifier: confidence thresholds, hierarchical output, name-boost, batch consistency |
evidence_fusion.feature | 6 | DST evidence fusion: mass combination, conflict detection, belief intervals, needs-clarification |
feature_extraction.feature | 5 | 12 SAGE-ablatable features: pattern signals, entropy, cardinality, value description |
taxonomy_management.feature | 5 | SIGDG taxonomy: hierarchy loading, sensitivity mapping, category resolution |
vocabulary_mapping.feature | 4 | Vocabulary mapping: custom term→category overrides, conflict resolution |
config_lifecycle.feature | 4 | HOCON config lifecycle: load, resolve, materialize, validate |
benchmark_gittables.feature | 5 | GitTables benchmark: BFO-grounded taxonomy, 122-type evaluation, accuracy metrics |
bespoke_dataset.feature | 4 | Bespoke dataset classification: custom data, ground truth comparison |
Tier 1 — Component Health (22 scenarios)
| Feature | Scenarios | What It Tests |
|---|---|---|
health_postgres.feature | 3 | PostgreSQL connection, AGE + pg_cron + pg_trgm extensions |
health_kerberos.feature | 3 | KDC ticket granting, keytab validation, principal listing |
health_kudu.feature | 3 | Kudu master API, tablet servers, table creation |
health_impala.feature | 3 | Impala SQL round-trip, DDL, data types |
health_atlas.feature | 10 | Atlas REST API, bootstrap types, entity CRUD, classifications, FTS, suggestions, glossary, lineage |
Tier 1 — Cross-Component Integration (12 scenarios)
| Feature | Scenarios | What It Tests |
|---|---|---|
integration_catalog_sync.feature | 4 | Impala DDL → Kudu storage, Atlas catalog bridge, entity lifecycle |
integration_meta_tagging.feature | 6 | SIGDG type setup, table/column tagging, classification search, Tagger dry-run |
pipeline_tagging.feature | 2 | Full Tagger pipeline (live + dry-run) against Impala tables |
Catalog sync scenarios:
- Impala CREATE TABLE registers in Kudu
- Impala CRUD operations work end-to-end on Kudu
- Atlas discovers Impala-managed Kudu tables (via catalog bridge)
- 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)
| Feature | Scenarios | What It Tests |
|---|---|---|
data_lifecycle.feature | 5 | Hot→warm Kudu→Iceberg via CTAS, transparent cross-tier queries |
Total
| Tier | Scenarios | Features |
|---|---|---|
| Tier 0 — Classification | 40 | 8 features |
| Tier 1 — Health | 22 | 5 health features |
| Tier 1 — Integration | 12 | 3 integration features |
| Total (passing) | 74 | 16 features |
| Planned (data lifecycle) | 5 | 1 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→ importsfeatures.tagging.steps.tagging_stepsfeatures/tagging/steps/__init__.py→ importsfeatures.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:
- Runs
DESCRIBE {table}on Impala to get column metadata - Builds
rdbms_*entities with temporary GUIDs (Atlas2000-RDBMSmodel, Aegir-aligned) - POSTs to Atlas
POST /v2/entity/bulk— single atomic call - Atlas resolves
rdbms_table_columnsCOMPOSITION 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 Type | Pattern | Example |
|---|---|---|
rdbms_instance | instance@{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 (nothive_*). Physical Kudu names remain separate (kudu_tableFDW 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:
| Persona | Emphasis |
|---|---|
| Data Scientist | Statistical distributions, feature correlations |
| Domain Researcher | Domain-specific metrics, temporal patterns |
| Quantitative Analyst | Numerical precision, confidence intervals |
| Operational Analyst | System health, throughput, latency |
| Business Analyst | KPIs, 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
| Layer | Tool | Directory | Purpose |
|---|---|---|---|
| Provisioning | OpenTofu | infra/aws/tofu/ | AWS VPC, EC2, S3, Cloudflare Tunnel |
| Configuration | Ansible | infra/aws/ansible/ | RKE2 install, Dask, services, ingress |
| Packaging | Zarf | zarf/ | Air-gap Kubernetes deployment |
| Dev Iteration | Tilt | tilt/, Tiltfile | Live-reload development on RKE2/k3d |
| Policy | OPA/Conftest | policy/ | 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
| Role | Purpose |
|---|---|
common | Base node setup: packages, kernel modules, sysctl, data volumes |
rke2-server | Control plane installation with token propagation |
rke2-agent | Worker installation with automatic token fetch |
dask | Helm-based Dask operator + DaskCluster CRD |
jupyterhub | Helm-based JupyterHub with Dask integration |
signals-engine | gRPC engine deployment + service |
cloudflare-tunnel | cloudflared with config-driven ingress routing |
zarf-deploy | 4-phase air-gap orchestrator (stage/init/deploy/verify) |
Playbooks
| Playbook | Description |
|---|---|
site.yml | Full stack: RKE2 + Dask + Engine + JupyterHub + Ingress |
dask-only.yml | Standalone Dask operator deploy |
airgap-deploy.yml | Air-gap deployment via Zarf |
teardown.yml | Remove workloads, preserve RKE2 cluster |
validate.yml | Post-deployment health checks |
Task Reference
Local Cluster (k8s:*)
| Task | Description |
|---|---|
k8s:provision | Create k3d cluster |
k8s:status | Check cluster connectivity |
k8s:deploy-dask | Deploy Dask operator + cluster |
k8s:deploy-jupyter | Deploy JupyterHub |
k8s:forward | Port-forward services |
k8s:destroy | Delete k3d cluster |
AWS Cloud (aws:*)
| Task | Description |
|---|---|
aws:provision | OpenTofu apply (VPC, EC2, S3) |
aws:inventory | Generate Ansible inventory from Tofu state |
aws:deploy | Full stack: RKE2 + Dask + services |
aws:verify | Post-deployment health checks |
aws:ssh | SSH to bastion host |
aws:destroy | Destroy infrastructure (preserves S3) |
aws:teardown | Full teardown including S3 |
OpenTofu (AWS)
AWS infrastructure is provisioned with OpenTofu (open-source Terraform fork) in infra/aws/tofu/.
Resources
| File | Resources |
|---|---|
vpc.tf | VPC, public/private subnets, IGW, conditional NAT, S3 VPC endpoint |
security.tf | Bastion SG (SSH from WARP CGNAT), RKE2 cluster SG |
ec2.tf | Ubuntu 24.04 AMI, SSH key pair, bastion, control plane, workers |
s3.tf | Encrypted data bucket with public access blocked |
outputs.tf | Bastion 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:
| Variable | Default | Description |
|---|---|---|
project | signals-360 | Resource naming prefix |
aws_region | us-east-1 | AWS region |
control_plane_count | 1 | Number of control plane nodes |
worker_count | 4 | Number of worker nodes |
worker_instance_type | r6i.xlarge | Worker EC2 instance type |
data_volume_size | 100 | Worker data volume (GB) |
allowed_ssh_cidrs | 100.96.0.0/12 | WARP CGNAT range for SSH |
airgap_mode | false | Enable 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:
- Stage — copy Zarf binary and packages to control plane
- Init — create PVs, initialize Zarf registry
- Deploy — deploy application package with variables
- 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:
| Component | Required | Description |
|---|---|---|
signals-images | Yes | Core container images (Dask, operator, engine) |
dask-operator | Yes | Dask Kubernetes Operator Helm chart |
dask-cluster | Yes | DaskCluster CRD manifest |
signals-engine | No (default: on) | gRPC engine deployment |
jupyterhub | No (default: on) | JupyterHub Helm chart |
ingress | No (default: on) | Ingress resources |
Variables
| Variable | Default | Description |
|---|---|---|
DASK_WORKER_REPLICAS | 4 | Worker count |
DASK_SPILL_DIR | /tmp/dask-spill | Spill-to-disk path |
S3_ENDPOINT | (empty) | S3-compatible endpoint |
S3_BUCKET | signals-360-data | Data bucket name |
S3_REGION | us-east-1 | AWS 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:
| Variable | Value | Purpose |
|---|---|---|
HF_HUB_OFFLINE | 1 | Prevent HuggingFace Hub API calls |
HF_HOME | e.g. /raid/cache/huggingface | Primary HF cache root |
HF_HUB_CACHE | e.g. /raid/cache/rch/huggingface | Hub package cache (may differ from $HF_HOME/hub) |
SENTENCE_TRANSFORMERS_HOME | e.g. /raid/cache/sentence-transformers | ST model blobs |
SIGINT_EMBEDDING_CACHE_DIR | defaults to $SENTENCE_TRANSFORMERS_HOME | HOCON 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:
- Container image bake-in: Copy
build/models/into the sigint container image at build time - S3 staging: Upload
build/models/to S3, download via VPC endpoint during deployment - Zarf data injection: Add as a Zarf
filescomponent 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.
| Path | zarf/federation/ |
| Package name | signals-federation |
| Version | 0.1.0 |
| Zarf | v0.70.1 |
| Spec | minifi_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
- Knative Serving (CRDs → controllers, Kourier, KPA
enable-scale-to-zero) - YuniKorn 1.9.0 (resource-class queues; policy promoted from
config/scheduler/federation-queues.yaml) - MiNiFi C++ sentinel Knative Service (
federation-signals/minifi-sentinel) - 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:
| Tier | Invariants |
|---|---|
| T0 | node Ready, zarf ns |
| T2 | Knative CRDs, controllers, scale-to-zero CM |
| T3 | YuniKorn Ready, federation queues |
| T4 | ksvc present, STZ posture |
| T5 | C2 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 mode | Example observed |
|---|---|
| Redeploy thrash | Aegir Tilt (infra/tilt/) recreated aegir-metaflow after teardown-precedence.sh |
| Image-pull noise | Metaflow images rewritten by Zarf agent to missing registry digests |
| Queue bypass | Workloads not submitted under root.{aegir,atelier,gaius,signals,hermes} |
| Scheduler conflict | Default kube-scheduler vs YuniKorn admission for capacity |
Direction (federation-owned):
- 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). - No long-running out-of-band Tilt against shared namespaces without an explicit engine queue + resource request/limit that YK can enforce.
- 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. - 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
- zarf.md (legacy notes)
- minifi-cpp component
- Package README:
zarf/federation/README.md
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:
| Tier | Trigger | Action | Speed |
|---|---|---|---|
| Live update | Source file changes (.rs, .py) | Sync into running pod | ~3-5s |
| Full rebuild | Dockerfile or dependency changes | Podman 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.yamloverrides 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:
- Build the signals-engine image using Podman
- Push to the local registry (127.0.0.1:31999)
- Apply the dev overlay to the cluster
- Watch for source changes and live-sync
Dev Overlay
The tilt/engine-dev.yaml configures the engine for development:
- Single replica
RUST_LOG=debugfor verbose logging- Relaxed resource limits (2Gi memory, 2 CPU)
Files
| File | Purpose |
|---|---|
Tiltfile | Tilt configuration (image build, resources, port forwards) |
tilt/engine-dev.yaml | Dev Kubernetes overlay for signals-engine |
tilt/build-and-push.sh | Podman 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
Ownertag 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:
kubectlmust be available in PATHhelmmust 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:
- Set
developer_emailanddeveloper_prefixinterraform.tfvars - All resources are tagged with
Owner = <developer_email> - Provision policies enforce naming conventions
- 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.
| Component | Purpose |
|---|---|
| signals-protocol | Shared zndx.engine.v1 / scheduler.v1 (trunk) |
| Atlas | Governance + OpenLineage REST; AGE on :21010 |
| Marquez | OpenLineage UI; proxies Atlas /api/v1 (:21011) |
| Ranger | Tag-based authz from Atlas classifications |
| Kudu | Hot-tier columnar store |
| Impala | SQL across Kudu and Iceberg |
| Impala FDW | PostgreSQL → Impala HS2 / kudu_scan |
| Iceberg | Warm tier on RustFS |
| Polaris | Iceberg REST catalog (rch/asf-polaris) |
| YuniKorn | Application admission and queues |
| Airflow | Coordination Activities and Metaflow DAGs |
| Metaflow | Platform metadata + @kubernetes tasks |
| Hermes Agent | Agent peer pin (zndx/oss-hermes-agent) |
| MiNiFi C++ | Sentinels (C2 + OTel) on Knative |
| Signals Control UI | Primary backplane, :9889 (weathership/signals-ui) |
| NiFi | Flow 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.
Layout
| Path | Content |
|---|---|
proto/zndx/engine/v1/engine.proto | Complete, Status, Remediate |
specification/protocol/engine_grpc.md | Human 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
| Service | Port | Purpose |
|---|---|---|
| Atlas | 21010 | /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 allgraphdb/apiinterfaces - Backend class:
AtlasAgeGraphDatabaseloaded viaatlas.graphdb.backendconfig 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 Type | qualifiedName Pattern | Source |
|---|---|---|
rdbms_instance | instance@signals | Lab cluster (rdbms_type=Kudu) |
rdbms_db | {db}@signals | CREATE DATABASE |
rdbms_table | {db}.{table}@signals | CREATE TABLE |
rdbms_column | {db}.{table}.{col}@signals | Column 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:
DESCRIBE {table}on Impala → column names, types, comments- Build
rdbms_instance+rdbms_db(referred),rdbms_table(main),rdbms_column(referred) entities with negative temp GUIDs (data_typeon columns,type=TABLEon tables) POST /v2/entity/bulk— single atomic call, idempotent via qualifiedName matching- Extract real GUIDs from
guidAssignments(create) ormutatedEntities(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 Name | SIGDG Class | Sensitivity |
|---|---|---|
customer_id | SIGDG:0012 PlatformIdentifier | Internal |
email | SIGDG:0025 ContactInformation | Confidential |
ssn | SIGDG:0011 GovernmentIdentifier | Restricted |
total_amount | SIGDG:0021 FinancialInformation | Confidential |
order_status | SIGDG:0050 TransactionInformation | Internal |
BDD Validation
The tier-1 integration tests validate the full classification lifecycle:
- Catalog bridge — Impala table registered in Atlas with correct column metadata
- Classification CRUD — Create PII classification type, apply to tables and columns
- Column-level tagging — Apply classification to individual columns (e.g.,
email→ PII) - 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.
| Service | Default port | Env override |
|---|---|---|
| Atlas | 21010 | SIGNALS_ATLAS_HTTP_PORT (process uses fixed -port 21010 today; keep in sync) |
| Marquez-web | 21011 | MARQUEZ_WEB_PORT (default ATLAS + 1) |
| OL proxy target | Atlas host/port | SIGNALS_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
- Policy admin — define allow/deny/mask rules on resources and tags.
- TagSync — pull classifications from Atlas (
http://localhost:21010) into Ranger’s tag store. - Impala plugin — enforce policies when users query Kudu/Iceberg tables via Impala.
See Metadata Tagging for the Atlas → Ranger flow.
Local devenv (target)
| Service | Port / endpoint | Notes |
|---|---|---|
| Ranger Admin | 6080 | SIMPLE auth for local dev |
| TagSync | background process | REST source → Atlas :21010 |
| Postgres | localhost:5455/ranger | admin + 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
| Service | Port | Purpose |
|---|---|---|
| kudu-master | 7051 (RPC), 8051 (HTTP) | Schema management, tablet locations |
| kudu-tserver | 7050 (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-clientviadevenv tasks run kudu:install-javainto.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
| Service | Port | Purpose |
|---|---|---|
| statestore | 24000 | Cluster membership, metadata broadcast |
| catalogd | 26000 | Metadata management, DDL execution |
| impalad | 21050 (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.
| Layer | Still wants Hadoop | Product default |
|---|---|---|
| BE link | libhdfs | Should not; link tax until no-HDFS default lands |
| Package check | libhadoop.so | Distro packaging leftover |
| FE Maven | hadoop-hdfs / client APIs | Gate/remove for Kudu-only profile |
| Runtime services | HDFS/YARN | None |
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 storage | Kudu (STORED AS KUDU); kudu_scan via libkudu_client |
| Warm storage | Iceberg and Impala UNION ALL views via impala_sql (HS2) |
| Identity | Kerberos: Postgres GSSAPI role is the Impala/Kudu principal |
| Access control | Postgres 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.
| Service | Port | Purpose |
|---|---|---|
| Polaris | 8181 | Iceberg 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.
Lab surfaces
| Surface | Port |
|---|---|
| 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.
| Concern | Authority |
|---|---|
| Sentinel agent, C2, overwatch | MiNiFi C++ (components/minifi-cpp) |
| Scale sentinel pods to zero | Knative Serving (KPA) |
| Multi-tenant queues, fair-share, preemption of claims | YuniKorn (this submodule) |
| Actual GPU/engine work | Host 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.
| Piece | Role |
|---|---|
signals-ui | Primary backplane — strict superset of yk-web capabilities |
| Repo | git@github.com:weathership/signals-ui.git |
| Submodule | components/signals-ui |
| Stack | Rust / Axum (no Node); Keiretsu + Cloudera logo (Atelier continuity) |
| YK | Required once the stack lands (SIGNALS_YK_API_URL) |
| YK API | Full /ws/v1/* surface used by yk-web (contract-tested) |
| Value-add | Sentinels, OTel, Atlas OL lineage, engine discovery |
| Stock YK web | Optional 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)
| Path | URL |
|---|---|
LAN classic (just yk-ui-forward) | http://192.168.1.55:9889/ |
| LAN NodePort (durable) | http://192.168.1.55:30889/ |
| REST | http://127.0.0.1:30080/ (NodePort; also LAN :30080) |
| Recipes | just yk-ui-forward / just yk-ui-forward-stop |
| signals-ui YK URL | SIGNALS_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:
- Zero Trust private network route for
192.168.1.0/24(or the node IP) via a connector on this host, or - cloudflared named tunnel + Access application →
http://127.0.0.1:9889(cloudflared tunnel loginfirst).
Until either is configured, use the LAN URLs on the same Wi‑Fi/subnet.
- Plan: Signals Control Plane UI
- Capability reference only:
~/local/src/asf/rch-yunikorn-web/
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).
| Concern | Choice |
|---|---|
| Chart | components/airflow/chart (1.20.0-dev / AF 3.1.7) |
| Executor | LocalExecutor (lab baseline; no Redis/Celery) |
| Metadata DB | Host devenv PostgreSQL (airflow DB, bridged into ns) |
| UI / REST | API server NodePort 30800 |
| Metaflow deploy | python flow.py airflow create dag.py |
| Task runtime | KubernetesPodOperator → RKE2 pods (RBAC → metaflow) |
| Scheduler of pods | YuniKorn (federation queues) |
| Time triggers | Metaflow @schedule |
| Event triggers | Knative Eventing → Airflow DAG runs (M3) |
Full architecture: Platform Metaflow · Critical plane.
Status
| Phase | State |
|---|---|
| Component submodule | components/airflow |
| Platform deploy on RKE2 | M2 — just airflow-platform |
| CI DAG | signals_ci (ConfigMap) · just airflow-platform-ci |
| Knative Eventing → DAG trigger | M3 — 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
| Surface | Value |
|---|---|
| UI / API | http://127.0.0.1:30800 (NodePort 30800) |
| Admin | admin / admin (lab only) |
| API auth | AF3 JWT: POST /auth/token → Authorization: Bearer … for /api/v2 |
| Namespace | airflow |
| Secrets file | build/config/airflow-secrets.env (gitignored; fernet/jwt/api) |
| Values | config/k8s/airflow/values-signals.yaml |
| Host PG bridge | signals-postgres-proxy (hostNetwork socat → 127.0.0.1:5455) |
| Zarf | zarf.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.
Related
- Outerbounds Airflow + Metaflow ops
- Metaflow scheduling with Airflow
Metaflow
Platform Metaflow for Signals — ground-up capability for any federated engine via signals-protocol. Not Gaius-owned; not Marquez.
| Submodule | components/metaflow |
| Remote | git@github.com:weathership/oss-metaflow.git |
| Branch | rch/devenv |
| Architecture | Platform Metaflow |
Role
| Concern | Authority |
|---|---|
| Flow authoring / run metadata | Metaflow (this submodule + metadata service) |
| Production DAG schedule | Airflow (components/airflow) |
| Event / reactive trigger | Knative Eventing → Airflow DAG runs |
| Task pods | RKE2 + YuniKorn (@kubernetes) |
| Artifacts | RustFS (S3) |
| Lineage SoR | Atlas OL (not Marquez DB, not Metaflow cards alone) |
| Operator UI | signals-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
| Phase | State |
|---|---|
| Submodule pin | Added (rch/devenv) |
| Platform metadata service on RKE2 | M1 — just metaflow-platform |
| Airflow + YK | Planned (M2) |
| Knative Eventing bridge | M3 — 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
Related
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 surface | Signals / Weathership contribution | Official 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 listing | Memory providers · Memory provider plugins |
Context engine (plugins/context_engine/<name>/) | Compaction that preserves lineage, tags, and authz-scoped context | Plugins · Context engine plugins |
| General plugins / tools | Atlas/OL/Ranger tools via discovery; OIP model ops with provenance | Plugins overview |
| Signal (messenger) | signal-cli in devenv packages; phone via secretspec SIGNAL_CLI_PHONE | Hermes 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 class | Where it lives (examples) | Relation to Hermes |
|---|---|---|
Multi-engine federation (zndx.engine.v1, OIP, co-tenancy) | signals-protocol + Ægir / Atelier / Gaius | Hermes can call engines; it is not the federation SoR |
| Governance + lineage SoR | Atlas /api/atlas/*, OL /api/v1/*, Ranger | Hermes consumes via Weathership plugins / tools |
| DST / classification method | Atelier (healthy); sigint outcomes in Signals | Beyond 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 labs | Structural 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>/, subclassMemoryProvider; one active external provider viamemory.providerin config. - Context engines:
plugins/context_engine/<name>/,ctx.register_context_engine(...); one active viacontext.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.
Related
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:
| Component | Role |
|---|---|
| Knative Serving | Scale 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
Related
- signals-protocol
- Kerberos / Secrets
- Upstream: C2.md, METRICS.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.
- Development Environment — devenv shell,
just up, tasks - Peer integration — join the lattice
- Secrets — SecretSpec, dotenv, keytabs
- Services — PostgreSQL (AGE, pg_cron)
- Kerberos — project-local KDC (
DEV.VISTA.ZNDX.ORG) - Storage and backup —
SIGNALS_DATA_ROOT, stamps - Peer data products — warehouse facts
- Peer unit acceptance —
signals.targetand 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:
- Ensures data layout under
SIGNALS_DATA_ROOT - Starts KDC, then kerberos-bootstrap (keytabs + kinit) before Kudu/Impala
- Starts the full host graph (Postgres, RustFS, Atlas, Marquez-web, Ranger, Kudu, Impala, signals-ui)
- Runs stack-ready before signals-ui: data-plane health, YuniKorn/Knative, Metaflow, Airflow (auto-deploy when missing). Peers use
just signals-readyfor 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:
| Port | Project |
|---|---|
| 5432 | System/apt PostgreSQL (leave alone) |
| 5438 | cybersec / cyberphy |
| 5444 | gaius |
| 5455 | signals (this tree) |
| 5533 | atelier |
| 5555 | aegir |
| 5566 | synth |
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 / platform | Role | Default port(s) |
|---|---|---|
postgres | devenv PostgreSQL 16 + AGE (Atlas graph, Ranger, catalog). Not pglite. | 5455 |
kdc | Kerberos | 8848 |
kudu-master / kudu-tserver | Columnar store | 7051/7050 (web 8051/8050) |
impala-statestore / catalogd / impalad | SQL + kudu_scan | HS2 21050 |
atlas | Governance + OL API | 21010 |
marquez-web | OL UI → Atlas /api/v1 | 21011 |
ranger-admin | Authz | 6080 |
rustfs | S3 objects (critical object plane) | 9010 |
polaris | Iceberg REST catalog (warehouse on RustFS) | 8181 / admin 8182 |
signals-ui | Control plane UI (requires stack-ready) | 9889 |
| RKE2 YuniKorn | Federation scheduler | REST 30080 |
| RKE2 Knative | Serving (+ Eventing M3) | Serving ns |
| RKE2 Metaflow | Platform metadata service | 30180 |
| RKE2 Airflow 3 | Platform 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):
languages.javascriptpoints atcomponents/marquez/webwithnpm.install.enable = true(checksummednpm clean-installon shell enter).- Task
marquez:build-webhasbefore = [ "devenv:processes:marquez-web" ], so process-compose runs submodule init + npm + webpack before the UI process. processes.marquez-webonly 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.
| Layer | Responsibility |
|---|---|
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
| Language | Version | Purpose |
|---|---|---|
| Rust | stable | Primary application language |
| Python | 3.12 | Tooling, Airflow; uv for package management |
| Java | 21 | ASF component builds (Maven enabled) |
| TypeScript/JS | latest | Frontend |
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:
| Category | Tools |
|---|---|
| Core | git, gh, jq |
| Security | krb5, cyrus_sasl, openssl |
| ASF Build | cmake, ninja, gcc, protobuf, flatbuffers |
| Kubernetes | kubectl, helm, tilt, k9s, k3d |
| Cloud | awscli2, opentofu, ansible |
| Air-Gap | zarf, conftest, podman |
| Ingress | cloudflared |
| WASM | wasmtime, wasm-pack, wasm-bindgen-cli, binaryen |
| Data | grpcurl, dbmate |
| Docs | mdbook, 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
| Layer | What | How peers use it |
|---|---|---|
| Process / group | signals.target + foundation ready gate | systemd After=signals-ready.service |
| Wire / product | signals-protocol + platform endpoints | gRPC zndx.engine.v1.Engine, Metaflow/Airflow/CE, Atlas |
| Artifact | Path |
|---|---|
| Machine-readable contract | config/platform/peer-contract.json |
| Resource-class queues | config/scheduler/resource-classes.md |
| Unit samples | infra/systemd/ |
| Peer-repo acceptance templates | Peer unit acceptance spec |
| Group control README | infra/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):
| Tool | Used by |
|---|---|
just | Foundation + peer wrappers |
kubectl | signals-ready (Eventing Broker) |
grpcurl | lattice-ci + peer accept probes |
What peers consume (not re-host)
| Concern | Use platform | Avoid |
|---|---|---|
| Workflow metadata | Metaflow :30180 + config/metaflow/platform.json | Engine-local Tilt Metaflow as SoR when in platform mode |
| DAG production | Airflow :30800 | Argo Workflows for Metaflow prod |
| Events | Knative Broker signals-events/default | Argo Events |
| Schedule / STZ | YuniKorn + Knative Serving | Unscheduled free-for-all pods |
| Lineage / governance | Atlas OL + tags (:21010) | Peer Marquez DB |
| Analytic tables | Impala HS2 + Kudu (Kerberos) | Parallel warehouses on :5455 |
| Object store | RustFS :9010 | Competing S3 on same ports |
| Data product inventory | Signals warehouse (details / tx / hx) + RustFS URIs | Peer 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):
| Port | Owner |
|---|---|
| 5432 | system/apt (Metabase app DB may use) |
| 5438 | cybersec / cyberphy |
| 5444 | Gaius |
| 5455 | Signals foundation |
| 5533 | Atelier |
| 5555 | Ægir |
| 5566 | Synth |
| 5577 | Metabase 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.
| Work | Queue |
|---|---|
| Host GPU: reasoning / CoT chat | root.internal.inference.reasoning |
| Host GPU: code model | root.internal.inference.coding |
| Host GPU: planner / tool-router | root.internal.inference.orchestration |
| Host GPU: instruct / short chat | root.internal.inference.instruct |
| Host GPU: embeddings | root.internal.inference.embedding |
| Host GPU: OCR / docling / extract | root.internal.inference.extract |
| CPU-only burst / idle sentinel | root.internal.compute |
| Metaflow UI / Airflow / Eventing | root.platform |
| Pay-per-token API | root.external.token-metered |
| RPM/TPM-limited API | root.external.rate-metered |
| Grok ACP / xAI subscription | root.external.subscription.rate-limited |
Required stamps:
yunikorn.apache.org/queueannotation (placement rule isprovided)yunikorn.apache.org/app-id=federation.workload_idfederation.projectfor C2 → Yield- Host GPU: request
federation.zndx.org/gpuonly (nevernvidia.com/gpuon 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:
| Peer | Port | Capability (Status) | Unit sample | Notes |
|---|---|---|---|---|
| Gaius | 50051 | cognition | gaius.service | Product engine / federation mesh |
| Ægir | 50151 | instruct | aegir.service | Capability engine (+ native face) |
| Atelier | 50251 | referee | atelier.service | Capability engine; native servicer may be :50071 on co-tenant hosts |
| Synth | 50351 | synthesis | synth.service | Optional peer |
| Metabase | 50451 | dashboard | metabase.service | License-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
-
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). -
PartOf=/WantedBy=signals.target
Group stop/restart and opt-in membership viasystemctl enable/disable. -
Wrappers in the peer tree (not signals, not multiline shell in the unit):
scripts/systemd_start.sh—just up/ fulldevenv upfor this project (capability engine + product UI/gateway/DB); block until lattice Status and product surfaces answerscripts/systemd_stop.sh— peer-scoped full-stack stop of this unit’s devenv graph (and vLLM children); lattice + product ports free
UnitExecStart=/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).
-
Accept = full stack ready, not “Status alone” or “engine-only”:
- Codegen Status (preferred in-peer): generated
zndx.engine.v1stubs,projectmatches contract (see Signalsscripts/zndx_engine_status.py/ Gaiusscripts/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
grpcurlworks (installgrpcio-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)
- Codegen Status (preferred in-peer): generated
-
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. -
Single owner of the lattice port
Avoid dual listeners (orphan devenv + unit). Reclaim or refuse multi-bind (Gaius lesson on:50051). Interactivedevenv upmust not fight the unit. -
PATH
System-widejust/grpcurl/kubectlon systemdPATH;bash -lcinside wrappers when devenv/direnv is required. -
Never bind Signals
:5455or RustFS:9010. -
Product doc in the peer tree (Gaius pattern):
docs/current/src/operations/peer-unit.md— unit SoR for that project; not mesh/FEDERATION.mdhistory.
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.
| Must | Must not |
|---|---|
| Fully stop this peer’s lattice unit — engine process, process group, and vLLM/worker children it owns | Leave the lattice port listening “for convenience” |
TERM → grace → KILL if needed; call engine mgr.shutdown() / process-group teardown | Host-wide just teardown, gpu-deep-cleanup, or kill-by-pattern that matches sibling peers |
| Release this peer’s GPU leases / free this peer’s GPUs | Wipe /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>
| Command | Starts |
|---|---|
systemctl start signals | Foundation only |
systemctl start signals.target | Foundation + ready + all enabled peers (does not restart live members) |
systemctl restart signals.target | Complete refresh — stop every PartOf= member, start all enabled members, signals-refresh.service verifies Status |
just signals-restart | Same recycle + fail if any enabled member is not active |
just signals-cold-start-ci | Elevated 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.
| Peer | Status |
|---|---|
| Metabase | Complete (license-external AGPL; isolated engine) |
| Gaius | Full-stack under signals.target (Status + lattice-ci) |
| Ægir | Full-stack remediation 2026-08-13 (just up; was engine-only) — re-validate UI ports |
| Atelier | Full-stack remediation 2026-08-13 (just up; was engine-only) — re-validate UI ports |
Tick accept in peer-unit-spec.
Gaius
| Fact | Value |
|---|---|
| Role | Cognition / product engine; federation mesh participant |
| Checkout (lab) | ~/local/src/zndx/gaius |
| Unit sample | infra/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 |
| Capability | cognition (Status.project=gaius) |
| Product lifecycle | just up / just down (wrappers); process gaius-engine |
| Platform Metaflow | METAFLOW_SERVICE_URL=http://127.0.0.1:30180 + platform profile when federated |
| Events | e.g. dev.gaius.article.curate.requested → platform Broker |
| Signals checklist | peer-unit-spec — gaius |
| Product ops (SoR for unit) | Gaius tree docs/current/src/operations/peer-unit.md |
| Not the accept gate | src/gaius/engine/FEDERATION.md (older KServe/GPU mesh) |
Landed in Gaius tree (peer session):
scripts/systemd_start.sh/systemd_stop.sh— wait for Status (not only TCP).GaiusZndxEngineServiceron the existing gRPC server (project=gaius, capabilitycognition).- Product doc
docs/current/src/operations/peer-unit.md. - Unit installed:
just install-systemd --peers gaius --enable(lab host).
Lessons for other peers (from Gaius):
| Lesson | Detail |
|---|---|
| TCP ≠ Status | Native service already on :50051 did not imply zndx.engine.v1.Engine/Status |
| Third servicer | Register lattice face beside native (+ OIP); do not replace product gRPC |
| Status early | Status is live at gRPC bind (~phase GRPC); do not wait for vLLM/endpoint load |
| Stop is peer-scoped, not weak | Fully stop this peer’s engine (+ children); never host-wide teardown / gpu-deep-cleanup that kills sibling leases (see Unit stop) |
| One engine process | Two devenv daemons can both claim :50051; recycle this checkout’s engine for accept |
| FEDERATION.md vs peer-unit.md | Mesh write-up ≠ lattice accept gate |
| lattice-ci | Elevated 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@aegirper/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 undersignals.target: unit =just up(capability-engine + gateway + vite). Accept = Status + product UI health.
| Fact | Value |
|---|---|
| Role | Ontology / 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 class | core_federated_engine (Gaius-lineage capability engine) |
| Checkout (lab) | ~/local/src/zndx/aegir |
| Unit sample | infra/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 / Status | Status.project=aegir; endpoints empty (nothing hosted — honest); no capability_hint in the contract |
| Unit process model | Full devenv (just up) — process capability-engine + gateway + vite |
| Accept | Status :50151 + gateway health + vite |
| Session checklist | peer-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
| Fact | Value |
|---|---|
| Role | Referee / CAI; capability engine on lattice |
| Architecture class | core_federated_engine |
| Checkout (lab) | ~/local/src/zndx/atelier |
| Unit sample | infra/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 / Status | project=atelier; referee (+ configured caps) |
| Unit process model | Full devenv — capability-engine + grpc-server + gateway + vite |
| Accept | Status :50251 + :50071 + gateway + vite |
| Product SoR | Atelier docs/current/src/operations/peer-unit.md |
| Session checklist | peer-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.
| Fact | Value |
|---|---|
| Role | agent engine (Status.project=hermes) |
| gRPC | :50651 — zndx.engine.v1.Engine |
| Dashboard | :9119 |
| Sample unit | infra/systemd/hermes.service |
| Wrappers | Hermes tree scripts/systemd_start.sh / systemd_stop.sh |
| Peer contract | peers[] id hermes |
| Product SoR | Hermes 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.
| Fact | Value |
|---|---|
| License | AGPL-3.0 (Signals is Apache-2.0) — isolation is required |
| Architecture | Isolated product engine; not Gaius-lineage / core peer family |
| Role | License-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 unit | infra/systemd/metabase.service |
| Wrappers | AGPL tree scripts/systemd_start.sh / systemd_stop.sh |
| Peer contract | peers[] id metabase |
| Product docs | AGPL tree README.engine.md |
| Accept template | peer-unit-spec — metabase (landed) |
License boundary
| Do | Don’t |
|---|---|
Separate checkout (e.g. ~/local/src/agpl/metabase) | Submodule / jar-vendor into signals |
Unit WorkingDirectory / Exec* → AGPL tree only | Ship AGPL inside ASL2 images without a legal plan |
Vendor only signals-protocol in Metabase | Copy FE/BE into weathership/signals |
| Process + network integration | Secrets 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)
- Foundation ready before start.
- Optional platform Metaflow profile for flows that feed dashboards.
- CloudEvents via platform Broker when DAGs exist.
- YK queues for any RKE2 work.
- No second critical plane (engine/app DBs stay product-local).
Synth (stub)
| Fact | Value |
|---|---|
| gRPC | :50351 |
| Postgres | :5566 |
| Unit | synth.service |
| Status | Same 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
- Own lattice Postgres + gRPC ports (document in
peer-contract.json). - Vendor
signals-protocol; implementzndx.engine.v1.Engine/Status(+ OIP path). - Peer-tree
scripts/systemd_{start,stop}.sh; unit points at them. After=signals-ready.service; never bind:5455/:9010.- Start blocks until lattice Status (and product health if required).
- Platform Metaflow / CE / YK when joining production federation.
- If license ≠ ASL2 → external tree only (Metabase pattern).
just lattice-ci --require <id>green; theninstall-systemd --peers <id> --enable.
Related
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 does | Peer does not |
|---|---|
Own {peer}.{domain}.{name} | Create details / tx / hx tables |
| Write bytes to RustFS | Copy product rows into Gaius/Ægir/Atelier Postgres |
| Point Metaflow at platform profile | Use METAFLOW_DEFAULT_DATASTORE=local on the shared host |
Emit dev.signals.dataproduct.updated + UUIDv7 tx_id | Mint 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 queue | Invent 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 / prefix | Use |
|---|---|
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_focus | Catalog identity |
spec / spec_id | Product specification (signals.spec.warehouse_product) |
aspect.<aspect_id> | SHACL binding (declared / conformant / failed / holding) |
flow_name, run_id, pathspec | Metaflow identity |
snapshot_uri | {sysroot}/{flow}/{run} |
data_uri | {sysroot}/{flow}/data (CAS) |
code_package, code_package_sha | Immutable source |
deps | @conda / @pypi / default |
yk_app_id, yk_queue | Admission claim |
object_store, datastore_root, rustfs_endpoint | Must 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:
- Quality — is the retained object complete and on RustFS?
- Lineage — pathspec, code sha, YK app, Atlas OL job
- Delta — this run vs prior
latest_run_id/ last corpus pin - 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
| Work | Queue |
|---|---|
| Prospects GPU extract / OCR | root.internal.inference.extract |
| Instruct / short generation | root.internal.inference.instruct |
| Embeddings | root.internal.inference.embedding |
| Platform Metaflow/Airflow itself | root.platform |
| Grok ACP review | root.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)
git submodule update --remoteonsignals-protocol(trunk afterTX_ID_NOT_UUIDV7+ data-products spec). Regen engine stubs.- Implement
RemediateforTX_ID_NOT_UUIDV7(remint v7). - Flow uses platform Metaflow + RustFS profile — no local datastore.
- Choose
gaius.prospects.*(or keepgaius.cognition.outputsif that is the product). Seed once via Signals. endstep: map run → facts; recordtx+details+hx; include the method walk so nominal can be scored.- Objects live at
s3://metaflow/metaflow/…and/ors3://signals-dataproducts/gaius/…. Facts store those URIs. - Do not write prospects rows to Gaius PG
:5444as SoR. - Do not DROP warehouse partitions. Do not bind
:9010or:5455.
8. Where to look in this tree
| Piece | Path |
|---|---|
| Contract | components/signals-protocol/specification/protocol/data_products.md |
| UUIDv7 + Remediate | …/protocol/tx_id.md, SignalKind.TX_ID_NOT_UUIDV7 |
| Seed catalog | config/platform/data-products.json |
| Writer | src/signals/ops/history.py, src/signals/ops/warehouse.py |
| Snapshot mapping | src/signals/ops/metaflow_store.py |
| Platform Metaflow | config/metaflow/platform.json |
| Endpoints | config/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: Gaiusdocs/current/src/operations/peer-unit.md— notFEDERATION.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 downexist for the wrappers;restart-cleanis a full product recycle, not the unit start path.- Two devenv process-compose daemons can both hold
:50051because gRPC defaults to SO_REUSEPORT (kernel load-balances Status). Engine now setsgrpc.so_reuseport=0andgaius-engine.shrefuses start if the port is taken (#EN.00000014.DUALBIND).devenv processes restartmay no-op or spawn a second daemon (/tmp/devenv-<hash>vs$XDG_RUNTIME_DIR). - Gaius-local Metabase
:3100is not capabilitydashboard(AGPL peer:3200/:50451). - Live 2026-08-13: one listener on
:50051.grpcurl listshowszndx.engine.v1.Engine.just lattice-ci --require gaiusPASS (codegen + reflection). Pingrpcio-reflection<1.82— 1.83+ needs protobuf 7, whichxai-sdkforbids.
Filled: aegir
Ops: Peer integration — Aegir
Copy into Aegir session as the work order.
Agent one-liner (full paths):
Implement
peer-unit@aegirper/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.mdand/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 undersignals.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-engineindevenv.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)
| id | port | path_hint | capability_hint |
|---|---|---|---|
| synth | 50351 | ~/local/src/zndx/synth | synthesis |
Copy blank template when scheduled.
Signals-side responsibilities (not in peer session)
| Task | Recipe / path |
|---|---|
| Foundation ready | just signals-ready · signals-ready.service |
| Group control samples | infra/systemd/ |
| Contract | config/platform/peer-contract.json |
| Probe lattice | just lattice-ci |
| Ops for all peers | peer-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
| Service | Portable artifact |
|---|---|
Atlas (signals PG + AGE) | postgres/signals.dump |
| Ranger | postgres/ranger.dump (+ conf if installed) |
| Catalog | postgres/signals_catalog.dump |
| Polaris | dump if DB exists, else polaris.ABSENT |
| Kudu tables | logical/kudu/*.parquet via DataFusion (not FS copy) |
| rustfs / flink | filesystem/*.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
| Command | Needs |
|---|---|
just backup | Kerberos ticket + GSSAPI Impala HS2 on FQDN; Postgres; Kudu; signals-df |
just restore | Postgres 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
| File | Role |
|---|---|
secretspec.toml | Declares 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 env | Non-secret identity: realm, host, ports (KRB5_REALM, SIGNALS_KRB_ENV, …) |
Kerberos + env segment
| Concern | Where |
|---|---|
Realm DEV.VISTA.ZNDX.ORG | devenv.nix / KRB5_REALM (not a secret) |
PG role signals | Principal primary via pg_ident |
| Keytab paths | SecretSpec: SIGNALS_KRB_USER_KEYTAB, POSTGRES_KRB_KEYTAB, … |
| Live TGTs | Runtime (kinit / KRB5CCNAME) — not long-lived SecretSpec values |
Align SecretSpec profile with SIGNALS_KRB_ENV when using multi-env:
| Profile | Typical realm |
|---|---|
default / dev | DEV.VISTA.ZNDX.ORG |
stage | STAGE.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
| Provider | Use |
|---|---|
dotenv | Default local (.env) |
env | CI: secrets already in the environment |
keyring | Per-machine OS keyring |
sops / others | Shared 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
| Layer | Value |
|---|---|
| Org TLD | zndx.org |
| Location | vista |
| Environment / posture | dev → realm segment DEV |
| Host short name | tinybox |
| Cloudflare / FQDN | tinybox.dev.vista.zndx.org |
| Kerberos realm | DEV.VISTA.ZNDX.ORG = {ENV}.{LOCATION}.ZNDX.ORG |
| User principal | signals@DEV.VISTA.ZNDX.ORG |
| PostgreSQL role | signals (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:
| Principal | PG role |
|---|---|
signals@DEV.VISTA.ZNDX.ORG | signals |
signals@STAGE.VISTA.ZNDX.ORG | signals (different realm / KDC) |
analyst@DEV.VISTA.ZNDX.ORG | analyst |
Configuration
| Setting | Value |
|---|---|
| Realm | DEV.VISTA.ZNDX.ORG |
| Principal instance (FQDN) | tinybox.dev.vista.zndx.org |
| KDC port | 8848 (127.0.0.1) |
| Data directory | .devenv/kdc/ (gitignored) |
| Env overrides | KRB5_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
| Principal | Type | Credentials |
|---|---|---|
postgres/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORG | Service (PG GSSAPI) | .devenv/kdc/postgres.keytab |
impala/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORG | Service (Impala HS2) | .devenv/kdc/impala.keytab |
HTTP/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORG | Service (Atlas/Ranger SPNEGO) | .devenv/kdc/http.keytab |
kudu/tinybox.dev.vista.zndx.org@DEV.VISTA.ZNDX.ORG | Service (Kudu) | .devenv/kdc/kudu.keytab |
signals@DEV.VISTA.ZNDX.ORG | Service principal for the Signals project (PG role + FDW outbound) | Password: signals; .devenv/kdc/signals.keytab |
signals/admin@DEV.VISTA.ZNDX.ORG | Admin (optional) | Password: signals |
zndx@DEV.VISTA.ZNDX.ORG | User principal. Semantica web login; the username is zndx | Password: zndx |
semantica@DEV.VISTA.ZNDX.ORG | Service 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
| Piece | Source |
|---|---|
| Python API | playwright (uv dev group + optional extra browser) |
| Launcher | signals.browser — resolves Chromium, fixes devenv/nix driver env |
| Chromium | devenv package chromium on PATH, or SIGNALS_CHROMIUM_PATH, or Playwright-bundled after just browser-install |
| Node for driver | devenv 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
| Variable | Role |
|---|---|
SIGNALS_UI_BASE | default http://127.0.0.1:9889 |
SIGNALS_CHROMIUM_PATH | force Chromium binary |
SIGNALS_BROWSER_HEADED=1 | show window |
SIGNALS_BROWSER_SLOW_MO_MS | Playwright slow_mo |
SIGNALS_UI_BROWSER_SKIP=1 | skip browser fixtures |
SIGNALS_BROWSER_KEEP_LD_PATH=1 | do 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):
| Tool | Purpose |
|---|---|
wasmtime | WASM runtime for testing |
wasm-pack | Build Rust → WASM packages |
wasm-bindgen-cli | Generate JS bindings |
binaryen | WASM 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
| Component | Role |
|---|---|
| HoloViews | Declarative data visualization — the agent composes views from the HoloViews gallery |
| Datashader | Server-side rasterization for large datasets at viewport resolution |
| Dask | Distributed 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:
- Interprets the instruction and selects a visualization strategy
- Directs Dask to perform distributed computation
- Datashader rasterizes the result at the current viewport resolution
- 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:
| Persona | Emphasis |
|---|---|
| Data Scientist | Statistical distributions, feature correlations |
| Domain Researcher | Domain-specific metrics, temporal patterns |
| Quantitative Analyst | Numerical precision, confidence intervals |
| Operational Analyst | System health, throughput, latency |
| Business Analyst | KPIs, 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:
| CURIE | Label | Definition |
|---|---|---|
SIGDG:1010 | Public | Intended for unrestricted disclosure |
SIGDG:1020 | Internal | Not public but no harm if disclosed internally |
SIGDG:1030 | Confidential | Disclosure causes measurable harm to individuals or enterprise |
SIGDG:1040 | Restricted | Disclosure 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:
| CURIE | Label | Example |
|---|---|---|
SIGDG:2010 | CustomerRole | End-user, subscriber, app developer |
SIGDG:2020 | EmployeeRole | Employee, contractor, vendor |
SIGDG:2030 | EnterpriseRole | Corporate 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):
| CURIE | Label | Definition |
|---|---|---|
SIGDG:3010 | ClassificationProcess | Assigning SIGDG categories to an entity |
SIGDG:3020 | MigrationProcess | Moving data between storage tiers |
SIGDG:3030 | MaskingProcess | Applying de-identification transforms |
Training Data
The classification model is trained on synthetic datasets spanning the SIGDG hierarchy:
| Table | SIGDG Category | Columns | Rows |
|---|---|---|---|
identity_data | SIGDG:0010 IdentityInformation | 59 | 100 |
personal_data | SIGDG:0020 PersonalInformation | 215 | 100 |
business_data | SIGDG:0030 BusinessInformation | 21 | 100 |
system_data | SIGDG:0040 SystemInformation | 43 | 100 |
transaction_data | SIGDG:0050 TransactionInformation | 17 | 100 |
metadata_info | SIGDG:0060 TransformationMetadata | 77 | 100 |
annotations | (ontology reference) | 12 | 10 |
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
| File | Purpose |
|---|---|
devenv.nix | Packages, services, processes, tasks, shell configuration |
devenv.yaml | Nix inputs configuration |
.envrc | direnv integration |
.env | Runtime environment variables (dotenv) |
scripts/kdc-init.sh | Idempotent KDC initialization |
pyproject.toml | Python project config (dependencies, dev tools) |
Tiltfile | Tilt dev iteration config (image builds, port forwards) |
Infrastructure Configuration
| File | Purpose |
|---|---|
infra/aws/tofu/variables.tf | OpenTofu variable definitions |
infra/aws/tofu/terraform.tfvars | Shared deployment defaults |
infra/aws/ansible/group_vars/all.yml | Ansible shared variables |
zarf/zarf.yaml | Air-gap package definition |
policy/tofu/*.rego | OPA policies for OpenTofu plans |
policy/k8s/*.rego | OPA policies for Kubernetes |
Environment Variables
Kerberos
| Variable | Default | Purpose |
|---|---|---|
KRB5_REALM | DEV.VISTA.ZNDX.ORG | Kerberos realm {ENV}.{LOCATION}.ZNDX.ORG (non-secret) |
SIGNALS_KRB_ENV | dev | Env segment → DEV in realm |
SIGNALS_KRB_LOCATION | vista | Location segment → VISTA in realm |
SIGNALS_KRB_HOST | tinybox.dev.vista.zndx.org | Kerberos 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)
| Variable | Purpose |
|---|---|
AWS_REGION | AWS region for provisioning |
CLOUDFLARE_TUNNEL_TOKEN | Tunnel credentials (from tofu output) |
CLOUDFLARE_TUNNEL_ID | Tunnel 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
| Priority | Meaning | Gate |
|---|---|---|
| P0 | Blocks domain-expert review — will undermine credibility if unaddressed | Must resolve before any external presentation |
| P1 | Will draw significant questions — defensible answers required | Should resolve before peer review |
| P2 | Strengthens the contribution — good practice | Address 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:
-
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).
-
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.
-
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. -
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.
-
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:
| Constant | Location | Current Value | Role |
|---|---|---|---|
| Cosine discount | cosine_to_mass() | 0.30 | Fraction of cosine mass → \(\Theta\) |
| CatBoost base discount | catboost_to_mass() | 0.15 | Fraction of CatBoost mass → \(\Theta\) |
| CatBoost max discount | catboost_to_mass() | 0.50 | Maximum variance-adjusted discount |
| Variance scale factor | catboost_to_mass() | 1.60 | Maps variance [0,0.25] → discount [0.1,0.5] |
| Pattern evidence mass | pattern_to_mass() | 0.90 | Mass on matched pattern categories |
| Pattern \(\Theta\) mass | pattern_to_mass() | 0.10 | Residual mass to \(\Theta\) |
| Name exact match mass | name_match_to_mass() | 0.70 | Mass for exact column→category match |
| Name abbrev match mass | name_match_to_mass() | 0.50 | Mass for abbreviation match |
| Name overlap match mass | name_match_to_mass() | 0.30 | Mass for multi-word overlap match |
| Uncertainty gap threshold | needs_clarification | 0.30 | \(Pl - Bel\) above which clarification is flagged |
| Conflict threshold | needs_clarification | 0.20 | \(K\) above which clarification is flagged |
| Softmax temperature | cosine_to_mass() | 1.0 | Temperature for cosine→probability conversion |
| Variance base discount | catboost_to_mass() | 0.10 | Minimum discount with variance data |
Impact. An expert will ask “where do these numbers come from?” The answer cannot be “intuition.”
Approach:
-
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.
-
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).
-
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_validassertion 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 onHierarchicalClassification - 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:
-
ColumnFeaturesprovides 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
FrameOfDiscernmentconstructor 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 inbelief.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
| Item | Status | Key Result |
|---|---|---|
| R-01 option 3 (SVM source) | Done | SVM always-on as 5th DST source. 84.6% standalone accuracy. TF-IDF features are architecturally independent from dense embeddings. |
| R-03 (conflict metric) | Done | dempster_combine() returns K; combine_multiple() computes cumulative K via Smarandache & Dezert. Tests verify exact values. |
| R-04 (mass normalization) | Done | catboost_to_mass() and svm_to_mass() allocate residual from dropped codes to Theta. 3 new tests. |
| GPU preflight validation | Done | preflight_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 explanations | Done | Per-item CatBoost TreeSHAP with top-3 feature attribution in parquet output. |
| Single-command pipeline | Done | --auto-generate trains SVM + CatBoost inline on synthetic data. Zero manual steps. |
| Self-training mode | Done | --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 agent | Done | K-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:
- 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%)
- Calibration: Expected Calibration Error (ECE) — do belief intervals track true accuracy?
- Uncertainty separation: Do columns flagged
needs_clarificationgenuinely have higher error rates? - Conflict utility: Does high \(K\) correlate with misclassification? (ROC-AUC of \(K\) as a misclassification predictor)
- 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 registershive_* - 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/devenvbranches (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):
| Layer | Role | What we call a “table” |
|---|---|---|
| PostgreSQL | Frontend / FDW / RLS | Foreign table (e.g. atlas_entity_flat) |
| Impala | SQL engine only; HMS-free; Kudu (and later Iceberg) only | Impala name db.table → physical impala::db.table in Kudu |
| Kudu | Storage | Physical tablet table (kudu_table FDW option when needed) |
| Atlas | Governance metadata | Entity 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/):
| Type | Role |
|---|---|
rdbms_instance | Cluster / connection (e.g. Impala+Kudu lab instance) |
rdbms_db | Database / schema |
rdbms_table | Table (governance entity for a signals-managed table) |
rdbms_column | Column |
rdbms_index / rdbms_foreign_key | Optional structural metadata |
Relationships: rdbms_instance_databases → rdbms_db_tables → rdbms_table_columns (COMPOSITION).
| Phase | Entity Types | Status |
|---|---|---|
| Phase 1 (historical) | hive_table, hive_column, hive_db | Bootstrap proof only — retired for product registration |
| Phase 2 (current) | rdbms_table, rdbms_column, rdbms_db, rdbms_instance | Landed — 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) —
Taggerclass 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:
| Tier | Focus | Prerequisites |
|---|---|---|
| Tier 0 | Component health | None |
| Tier 1 | Query stack, metadata catalog, classification | Impala + Kudu + Atlas |
| Tier 2 | gRPC engine, extension deployment, self-improvement | Engine binary |
| Tier 3 | Full visualization pipeline, analytics scenarios | Engine + Dask + Datashader |
Eliminated Dependencies
The following components are intentionally excluded from the runtime stack:
| Component | Replacement | Reason |
|---|---|---|
| Hive Metastore | PG catalog registry + KuduMetaProvider | Eliminate Thrift dependency, simplify deployment |
| HDFS | Kudu (primary); Iceberg/object later; rustfs / Ceph for future engines | Not a storage tier we run; product default is Kudu-only / no-HDFS |
| HBase | PostgreSQL + AGE | Atlas backend simplified to single database |
| Solr | PostgreSQL full-text search | Atlas search simplified |
| ZooKeeper | Kudu’s built-in Raft consensus | No external coordination service |
| Trino | Impala | Kudu 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.
| Path | What happens | Retained? |
|---|---|---|
| Live yield | Signals 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 land | A 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:
- The operation is one
tx_id(UUIDv7).tx,details.t,hx_exchange, andhx_reasoninguse that id. detailsfor that id names the placement:yk_app_id(this isfederation.workload_idand the YuniKorn application id),yk_queue,run_id,model,flow_name.- 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, andagent-rtc(Hermes WebRTC, one GPU while the session runs). The trajectory product is{peer}.agent.trajectories. Training-shaped work usesextract,medium, orheavy(for exampleaegir.models.bespoke). There is no project queue and noroot.gaius. - An external leaf (
root.external.subscription.rate-limited,token-metered,rate-metered) recordshx_*and claims no GPU. It has nosrc = 0rows. - YuniKorn places a GPU Application on a node and a device ordinal. The retained sample stores the device as
signal.gpuand the instance — the node, when the cluster writer stamps one — assignal.inst. Hostname and GPU UUID stay on the live OTLP point (gpu.hostname,gpu.uuid). They are not columns ofsignal_tier0. - Device samples for the operation are
signalrows withsrc = 0, thatgpuandinst, the sameepoch_hour, andts_nsbetween the minimum and maximumts_nsof thehx_*rows for thetx_id.src = 1rows in that window are the trainer or vLLM counters beside the device. - When the operation recorded model state,
latent_tier0.refandclt_activation_tier0.refcarry the same trace id.hx_reasoning.lineagecarries the OpenLineage run id when the review wrote one.hx_reasoning.tracecarries the brief, or the object URI of the Protobuf trajectory (zndx.agent.v1, ATIF on the object plane). - The signals-ui merge key
openlineage.runId · yunikorn.applicationId · otel.trace_id · signals.workload_idis the control-plane name for the same association.otel.trace_idis 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.
| Piece | Cluster | This node |
|---|---|---|
| DCGM | DaemonSet pod on every GPU node; scrape each Service endpoint | hostPort 9400, scrape 127.0.0.1:9400 |
| Exporter labels | Set DCGM_EXPORTER_KUBERNETES=true so a sample names the pod | Shipped manifest sets false (node-scoped watts) |
| Kudu | 3 masters, tablet servers, replication 3 | 1 master :7051, 1 tserver, kudu.num_tablet_replicas=1 |
| Object store | S3-compatible bucket signals-dataproducts | RustFS :9010 |
| Catalog | Polaris REST | Polaris :8181 / admin :8182 |
| SQL front door | Postgres + impala_fdw on each peer | Signals :5455, Gaius :5444 |
| Identity | Kerberos GSSAPI, one principal through Postgres, Impala, and Kudu | Realm 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 diagram | Defined in |
|---|---|
dcgm-exporter DaemonSet, counters, :9400 | zarf/federation/manifests/telemetry/dcgm-exporter.yaml |
OTLP yield :9410/v1/metrics | src/signals/telemetry/dcgm_otel.py |
| Telemetry surface | src/signals/engine/s2s.py (kind=telemetry) |
signal_tier0 / signal foreign tables | config/platform/signal-fdw.sql |
tx / details / hx_* views | config/platform/data-products-views.sql |
Tier-0 DDL, week ranges, no DELETE | config/platform/data-products-kudu.sql |
| Resource-class leaves | config/scheduler/resource-classes.md |
tx_id, hx_*, trajectory product | components/signals-protocol/specification/protocol/data_products.md, agent_grpc.md |
FDW kudu_scan vs impala_sql | Impala 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.
| Slot | Hours | What it means |
|---|---|---|
| 0 | 0 | First admission. Inside the window the arbiter is still working. A healthy workflow writes ok here. |
| 1 and 2 | 1 and 1 | Miss observed, then confirmed on the next hourly evaluation. |
| 3 | 2 | Agenda event. |
| 5 | 5 | Reminder. |
| 6 and 8 | 8 and 21 | Briefing. |
| 9 | 34 | Discussion. 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 diagram | Defined in |
|---|---|
| Grammar: tree, phase, gate, horizon, Brier epoch | components/signals-protocol/specification/operations/nautilus_supervision.md |
| Messages and enums | components/signals-protocol/proto/zndx/supervision/v1/supervision.proto |
Signals instance, all NONE | config/supervision/signals.textproto |
| Kudu tier 0 and the tier0-only views | config/platform/nautilus-kudu.sql |
| Foreign tables | config/platform/nautilus-fdw.sql |
| IT-ops method ledger, a different score | IT-ops FSM and Brier ledger |