Chapter 04
Substrate columns
Substrate columns are the named fields Holonograph captures alongside every call so a later reading can be attributed to what changed about your deployment. Six are mandatory and captured automatically. The rest you declare on the contract and populate yourself, in one of two ways.
What it is
A substrate column is a stable, named slot in the four-layer snapshot's substrate layer. Two categories:
- Mandatory columns — always present, always captured by Holonograph. You don't declare them; they're part of the shape.
- Operator-declared columns — you declare them in the surface contract and populate them either at call time (
deployment-state) or after the outcome is known (diagnostic).
Why it matters
A column you declared is a column Holonograph will hold you accountable for. When drift attribution or variance isolation runs later, they use those columns to distinguish your deployment changed from the vendor moved from your evaluation config drifted. Columns you didn't declare are columns those analyses can't use.
How to use it
Mandatory columns
These six are captured on every event, no declaration required:
| Column | Type | Meaning |
|---|---|---|
lens_version | string | Which lens configuration was in effect. |
light_source_identifier | string | Which model produced the output, as vendor/model/version. Vendor-controlled. |
run_mode | enum | Which run mode the evaluation ran under. See Run modes. |
correlation_id | string | Ties together the calls of one evaluation (e.g. across multiplex vendors). |
provenance | enum | Origin category of the run — typically production-state versus exploratory-state. |
timestamp | ISO 8601 | When the evaluation occurred. |
Operator-declared columns
Each declared column has this shape on the contract:
{
id: "prompt_hash",
control: "operator" | "vendor" | "platform" | "third-party",
visibility: "transparent" | "announced" | "opaque",
semantic: "deployment-state" | "diagnostic",
description?: string,
values?: string[] // enum, when the column is discrete
}
The two fields that decide how you set the column are semantic and values. The rest describe who controls the value, how visible its changes are, and (optionally) a fixed enum you must stay within.
deployment-state — set at call time
These describe the state of your deployment at the moment of the call. Populate them via the operatorColumns map on the SubstrateSnapshot at client construction — the values then attach to every call the client makes until the client is reconstructed with new values.
const client = new HolonographClient({
endpoint: "http://127.0.0.1:8080",
runMode: "production",
lensVersion: "lv_2026_07_01",
substrate: {
lensVersion: "lv_2026_07_01",
lightSourceIdentifier: "anthropic/claude/4",
runMode: "production",
provenance: "production",
operatorColumns: {
prompt_hash: "v2-sha256-9d3f...",
tool_schema_version: "3.1.0",
retrieval_index_rev: "2026-07-01T00:00Z"
}
}
});
Set prompt_hash to a stable hash of your input. When your prompt changes, the hash changes; when it doesn't, the hash doesn't. Downstream analyses read the column expecting exactly that discipline.
diagnostic — set post-hoc via cohort tag
Diagnostic columns are set after the outcome is known — because the value is a category the evaluation itself decided. You set them by passing a cohort tag formatted as <column.id>=<value> to reportOutcome().
await handle.reportOutcome({
dimensions: [{
dimensionId: "correctness",
passed: false,
expected: "a correct answer",
actual: "misidentified the intent"
}],
cohortTags: [
"customer-tier=enterprise",
"fix_tier=tool-input-schema",
"severity=high"
]
});
If the column declares a values[] enum, the value you pass must be one of them. Anything else is rejected at ingest — not silently accepted.
Reading the output
Substrate columns come back on every event via GET /lens/events. Filter queries on them the same way you filter on cohort tags: a slice is a set of column-value pairs, and the values you didn't declare aren't queryable — which is the whole point of declaring them ahead of time.
Reference
Common diagnostic columns
The set below appears often enough in real deployments to document as convention. Declare them if they apply; declare your own if you have a better name.
| Column | Typical values | Notes |
|---|---|---|
fix_tier | llm-judgment, tool-input-schema, tool-handler, code-architecture | Where a corrective artifact would live if this event were part of a lesson cluster. |
failure_category | free-form | Short label for the failure mode, when the event represents one. |
severity | low, medium, high, critical | Operator-assigned severity of the event. |
classification.mode | free-form | Which classifier or mode assigned the outcome, when relevant. |
Column shape
| Field | Values | What it says |
|---|---|---|
id | string | Stable identifier for the column. |
control | operator, vendor, platform, third-party | Who owns the value's changes. |
visibility | transparent, announced, opaque | How visible changes are to the operator. |
semantic | deployment-state, diagnostic | When and how the value gets set (call time vs post-hoc). |
description | string (opt) | Human-readable description. |
values | string[] (opt) | Enum of allowed values. Rejected at ingest if the reported value isn't in the list. |