HOLONOGRAPH / The Guide · Substrate columns v0.23.0 all chapters

Chapter 04

Substrate columns

Reflects Holonograph v0.23.0 · ~5 min read

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.

Practical guidance Declare a column for every knob in your system that could plausibly change a score — prompt, tool schemas, retrieval index revision, feature flags. It's cheaper to declare too many than to reconstruct a slice you didn't foresee.

How to use it

Mandatory columns

These six are captured on every event, no declaration required:

ColumnTypeMeaning
lens_versionstringWhich lens configuration was in effect.
light_source_identifierstringWhich model produced the output, as vendor/model/version. Vendor-controlled.
run_modeenumWhich run mode the evaluation ran under. See Run modes.
correlation_idstringTies together the calls of one evaluation (e.g. across multiplex vendors).
provenanceenumOrigin category of the run — typically production-state versus exploratory-state.
timestampISO 8601When 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.

ColumnTypical valuesNotes
fix_tierllm-judgment, tool-input-schema, tool-handler, code-architectureWhere a corrective artifact would live if this event were part of a lesson cluster.
failure_categoryfree-formShort label for the failure mode, when the event represents one.
severitylow, medium, high, criticalOperator-assigned severity of the event.
classification.modefree-formWhich classifier or mode assigned the outcome, when relevant.

Column shape

FieldValuesWhat it says
idstringStable identifier for the column.
controloperator, vendor, platform, third-partyWho owns the value's changes.
visibilitytransparent, announced, opaqueHow visible changes are to the operator.
semanticdeployment-state, diagnosticWhen and how the value gets set (call time vs post-hoc).
descriptionstring (opt)Human-readable description.
valuesstring[] (opt)Enum of allowed values. Rejected at ingest if the reported value isn't in the list.