Run & validate a stack (P3-F4)¶
A stack is a complete, runnable Pipeline config for the whole AI-ready-data spine
(Source → Intake-Screening → Parse → Chunk → Content-Screening → NER → RE → Redaction →
Profiling → Disambiguation → Graph-Assembly → Export), distinct from the per-stage
examples/*.yaml slices. Stacks live in the top-level stacks/ directory. Architecture:
ADR-0036 §3.
The two blessed stacks¶
| Stack | What it is | Providers |
|---|---|---|
stacks/default.yaml |
The blessed default — CPU-first, offline, permissive, deterministic (ADR-0007/0012). What an adopter runs out of the box. | In-core deterministic Providers + the real CPU adapter where it adds value (parser.pdfplumber, chunk.markdown, entity.gazetteer, relation.pattern, redaction.hybrid_rule (comprehensive-by-default, H-C1), profiling.statistical, disambiguation.cascade, graph.canonical, export.knowledge_graph + export.jsonl_parquet, both Screening checkpoints). |
stacks/gpu-sota.yaml |
The GPU / high-accuracy stack. | ONE fused Entity+Relation Stage (relation.gliner_relex — joint NER+RE in one pass), learned redaction.gliner_pii, and the served embedding.endpoint for the RAG export. On a CPU-only host these Stages are skipped-with-flag — never faked. |
stacks/gpu-sota-pod.yaml |
The self-contained GPU-pod variant of gpu-sota.yaml. |
Identical to gpu-sota.yaml except the RAG-corpus embedder is the in-process embedding.sentence_transformers instead of the served embedding.endpoint — so the whole stack runs on a single GPU pod with no external service to stand up. This is the stack the maintainer runs to validate the fused SOTA path end-to-end (LATENCE_CUDA=1 latence stack validate stacks/gpu-sota-pod.yaml). |
The fused extraction Stage (why extraction is ONE Stage, not two)¶
relation.gliner_relex is a Fused Provider: it fulfils both Entity Extraction and Relation
Extraction in one pass (extract_fused), satisfying the FusedEntityRelationExtractor Capability —
not the standalone RelationExtractor.relate seam. So the SOTA stacks declare a single
fused_entity_relation Stage (extract), and the Runner routes its FusedExtraction (mentions +
relations) to both downstream consumers (Profiling, Disambiguation). Pairing it with a separate
entity.gliner Entity Stage would double-run NER, and declaring it under a relation_extraction
Stage raises ContractError at run (it does not satisfy that seam). A stack that wants standalone
learned NER can still wire entity.gliner + a non-fused RE provider; the blessed SOTA path is the
single fused pass. The CPU capability-satisfaction guard (test_stack_capability_guard.py)
statically checks this wiring on every push — see below.
stacks/<client>.yaml is the documented extension point: copy any stack and swap Providers.
Running a stack directly¶
A stack YAML uses two placeholders so it carries no machine-specific path: {STORAGE_URI} (the
run's state/export root) and {CORPUS_DIR} (the Source folder). To run a stack against your own
data with latence run, replace them with a Storage URI and a folder of documents:
sed -e 's#{STORAGE_URI}#file:///tmp/my-run#' -e 's#{CORPUS_DIR}#file:///path/to/my/docs#' \
stacks/default.yaml > /tmp/my-stack.yaml
latence run /tmp/my-stack.yaml
Validating a stack — latence stack validate¶
latence stack validate <stack.yaml> runs a named stack end-to-end on a small bundled corpus
and asserts the G1-style checks in one command — the G1 gate's E2E / reliability lanes generalized
from a one-off gate into a reusable, standing gate ("Goal A: test the default stack, one command").
latence stack validate stacks/default.yaml
# → a pass/fail table per check; exit 0 when green, non-zero on any failure (gates CI/publish).
It is composition over the existing seams — LocalRunner + QualityReportBuilder + the
device/skip seam — not new pipeline logic. The checks:
| Check | Asserts |
|---|---|
contracts |
Every record carries Provenance + Classification; offsets align; zero drift (reuses the QualityReportBuilder completeness check + the ADR-0031 drift roll-up). |
kg |
A non-empty, content-addressed knowledge graph with per-edge Evidence is produced and exported (Parquet/TTL/GraphML). Skip-clean when the stack declares no Graph-Assembly stage. |
rag |
The redacted, chunked corpus is exported (JSONL/Parquet), well-formed, and carries no raw PII (redaction is load-bearing — a bypass FAILs here). |
report |
A schema-valid Quality Report is produced, counts-only (no PII), with per-stage metrics + the device/license/profile columns (P3-F1). |
determinism |
A second run under a fresh run_id produces a byte-identical export (the H-E1 equality discipline). |
resume |
Re-running the same run_id resumes from checkpoints and reproduces a byte-identical export. |
graceful_failure |
The planted malformed inputs are quarantined / PARSE_ERROR'd, never crashing the run. |
device_honesty |
On a CPU-only host a GPU stack's learned Stages are skipped-with-flag (recorded, never faked); the CPU-reachable spine still validates. |
A signed-style summary (like G1-RESULTS.md) is written to Storage under
<storage_root>/_latence/stack-validation/<stack>.md. Pin --storage-root to inspect the run's
artifacts after the fact:
Device honesty on a CPU-only host¶
latence stack validate stacks/gpu-sota.yaml on a machine with no CUDA (or without the heavy
Provider packages installed) reports the learned Stages as skipped-with-flag — the fused
extract (relation.gliner_relex), redact (redaction.gliner_pii), the served-embedder RAG
export, and everything that depends on them — and still validates the CPU-reachable spine
(Source → … → Content Screening). No fabricated quality/throughput numbers ever appear (#64). On a
real GPU host with the packages installed, the learned Stages run (device: cuda) and the full
spine validates.
The capability-satisfaction guard (CPU-visible wiring check)¶
Because a CPU-only stack validate skips every device: cuda Stage, the Runner's dispatch-time
isinstance(provider, <Protocol>) check — the one that raises ContractError for a mis-wired Stage
— never runs for a GPU Stage in CI. That is how the fused/standalone miswiring (a Fused Provider
under a relation_extraction Stage) once hid until a real pod run. check_stack_capabilities
(latence_core.stacks.capability_guard) closes the gap: it loads each Provider class (a
torch-free import — the heavy dep is deferred) and structurally checks it against the Protocol its
Stage's Capability declares, the SAME mapping the Runner enforces. test_stack_capability_guard.py
asserts the committed stacks have zero findings and that the guard bites on the exact miswiring
— so a fused-under-relation slip fails CI on a CPU host, never reaching a pod.
CI¶
CI runs latence stack validate stacks/default.yaml (and stacks/gpu-sota.yaml /
stacks/gpu-sota-pod.yaml) on every push with LATENCE_CUDA=0 — the blessed stacks are proven
end-to-end (the real pipeline, not a config-parse smoke) on a tiny corpus, offline and
deterministic, as a standing gate — plus the CPU capability-satisfaction guard over every stack's
wiring.