# Go SDK

Kind

guide

For

developer

Verified against

Go module at veritio@c4100ee · @veritio/core@0.4.7

The Go module implements portable events, edges, record hashing, evidence commits, governed changes, templates, deterministic redaction, and risk scoring. At the verified revision it does not provide the TypeScript `AuditStore` or audit-record-chain verifier.

The protocol semantics are identical to TypeScript and Python. The _ergonomics_ are not. Go has no default arguments, no exceptions, and no way to spell an absent struct field, so three shapes that are invisible in the other SDKs become load-bearing here: required policy arguments, pointer-typed options, and the zero value.

## Add the pinned module

[Section titled “Add the pinned module”](#add-the-pinned-module)

Terminal window

```sh
go get github.com/getveritio/veritio/sdks/go@c4100ee7b678d0c6b227c67ae6ea1d8a1f373967
```

The module declares Go 1.22. Confirm the resolved revision with:

Terminal window

```sh
go list -m -json github.com/getveritio/veritio/sdks/go
```

## Create the shared event

[Section titled “Create the shared event”](#create-the-shared-event)

src/examples/quickstart/go.go

```go
package main

import (
  "fmt"

  veritio "github.com/getveritio/veritio/sdks/go"
)

func main() {
  event, err := veritio.CreateAuditEvent(veritio.AuditEventInput{
    ID:          "evt_member_invited_01",
    OccurredAt:  "2026-08-09T10:00:00.000Z",
    Actor:       veritio.Principal{Type: "user", ID: "usr_123"},
    Action:      "organization.member.invited",
    Target:      veritio.Resource{Type: "organization", ID: "org_acme"},
    Scope:       &veritio.EvidenceScope{TenantID: "org_acme", Environment: "production"},
    Purpose:     "access_management",
    LawfulBasis: "contract",
    Retention:   "security_1y",
    Metadata:    map[string]any{"role": "viewer"},
  })
  if err != nil {
    panic(err)
  }

  hash, err := veritio.HashAuditEvent(event, nil)
  if err != nil {
    panic(err)
  }
  fmt.Printf("event=%s hash=%s\n", event.ID, hash[:12])
}
```

Run this file inside a module that depends on the pinned Veritio module:

Terminal window

```sh
go run .
```

verified output

```text
event=evt_member_invited_01 hash=3c1eb99f7a8f
```

The website verifier formats, compiles, tests, and runs this fixture in a temporary Go module before comparing its event-hash prefix with TypeScript and Python.

## Errors are the whole fail-closed surface

[Section titled “Errors are the whole fail-closed surface”](#errors-are-the-whole-fail-closed-surface)

TypeScript throws and Python raises. Go returns `(T, error)`, and the second return value is where every fail-closed guarantee lives. `CreateAuditEvent`, `CanonicalJSON`, `HashAuditEvent`, `HashAuditRecord`, `NormalizeRiskSignals`, `ScoreRiskSignals`, `RollupEpisodeRisk`, `RiskPolicy`, `DefineEntity`, and `CreateGovernedActionDraft` all return one.

```text
TypeScript   throws        → an ignored error is impossible
Python       raises        → an ignored error is impossible
Go           returns error → an ignored error is a silent zero value
```

An ignored error hands you the zero value of the result type: an `AuditEvent` with an empty ID and no scope, or a `RiskAssessment` with `score=0`. A zero value is not a normalized event. Check every error and never treat the first return as usable until you have.

`VerifyEvidenceCommits` is the exception. It returns `EvidenceCommitVerificationResult` — a value with `OK`, `Index`, and `Reason` — because a failed verification is an expected outcome, not a programming error. Read `result.OK`, not an error.

## The risk policy argument is required

[Section titled “The risk policy argument is required”](#the-risk-policy-argument-is-required)

This is the single largest divergence Go callers hit. The other two SDKs default the policy; Go does not.

SDK

Signature

TypeScript

`scoreRiskSignals(signals: RiskSignals, policy?: RiskScoringPolicy): RiskAssessment`

Python

`score_risk_signals(signals, policy=DEFAULT_RISK_POLICY) -> dict`

Go

`ScoreRiskSignals(signals RiskSignals, policy RiskScoringPolicy) (RiskAssessment, error)`

Go cannot express `policy?`, so the reference policy must be passed by name:

```go
assessment, err := veritio.ScoreRiskSignals(signals, veritio.DefaultRiskPolicy)
if err != nil {
  return err
}
```

`RollupEpisodeRisk(steps, policy)` takes the same required argument for the same reason.

### A zero-value policy scores NaN and bands as critical

[Section titled “A zero-value policy scores NaN and bands as critical”](#a-zero-value-policy-scores-nan-and-bands-as-critical)

Because the argument is required, the compiler will not let you forget it — but it will happily accept a `RiskScoringPolicy` you never populated, from a struct field, a map miss, or an unchecked `RiskPolicy` call whose error you dropped.

The result is not a zero score. It is worse:

```text
var zero veritio.RiskScoringPolicy
ScoreRiskSignals({OperationType: "destructive"}, zero)

  → score         NaN        (sat(0, 0) is 0/0)
  → level         critical   (every band threshold is 0, so no comparison matches)
  → policyVersion ""         (empty)
  → error         nil        (signals were valid; the policy was never checked)
```

`ScoreRiskSignals` fails closed on bad _signals_, not on a bad _policy_. Nothing rejects the empty policy at scoring time.

The net is one step downstream. `CreateSecurityRiskAssertion` runs `validateRiskConclusion`, which rejects a non-finite or out-of-range score before it can reach a canonical hash, and returns `conclusion.score must be a finite number in [0,1]`. That guard exists because a NaN or infinity would otherwise die as an unsanitized `json: unsupported value` inside `HashAssertionRecord`.

So the failure mode is: a NaN score with a `critical` band travels through your own code, your logs, and your dashboards, and only surfaces when you try to turn it into evidence. Construct policies once, at a process boundary, and check the error.

## Optional numbers are pointers, and `Float64` is the helper

[Section titled “Optional numbers are pointers, and Float64 is the helper”](#optional-numbers-are-pointers-and-float64-is-the-helper)

`RiskPolicyOptions.Temperature` is `*float64`, not `float64`. The pointer is deliberate: temperature `0.0` derives the lenient endpoints and is a legitimate value, so an absent knob has to be distinguishable from a supplied zero. The same rule governs every field in `RiskPolicyOverrides` — `MaxBoost`, the per-signal weights and saturation constants, and each band threshold are all `*float64`, where `nil` means “keep the derived value”.

Go does not let you take the address of a literal, so the module ships `Float64`:

```go
policy, err := veritio.RiskPolicy(veritio.RiskPolicyOptions{
  Temperature: veritio.Float64(0.8),
})
// policy.PolicyVersion == "veritio.reference.v1+temp0.80"
```

Temperature is a multiple of `0.01` in `[0,1]`. `0.5` reproduces the reference policy byte-for-byte; lower is lenient, higher is strict. Derivation stamps a deterministic `veritio.reference.v1+tempX.XX` policy version so a hashed conclusion always names the constants that produced it.

Overrides are different. Any override makes the policy hand-tuned, so `Overrides.PolicyVersion` becomes mandatory and `RiskPolicy` fails closed without it — an auto-generated `+tempX.XX` suffix must never misrepresent constants a caller replaced by hand.

## Only `BandOf` is exported

[Section titled “Only BandOf is exported”](#only-bandof-is-exported)

The four determinism primitives are the reason scores agree across languages. Their export status does not agree across languages.

Primitive

TypeScript

Python

Go

Clamp to `[0,1]`

`clamp01`

`clamp01`

`clamp01` (unexported)

Round half-up to 4 decimals

`round4`

`round4`

`round4` (unexported)

Saturating ratio `x/(x+k)`

`sat`

`_sat` (private)

`sat` (unexported)

Score to band

`bandOf`

`band_of`

`BandOf` (exported)

`BandOf(score float64, bands RiskBands) RiskLevel` is the only one you can call. It uses inclusive lower bounds and returns `none` below the low band. Reach for it when you need to re-band a stored score under a different policy without rescoring.

If you were planning to reimplement `round4` or `sat` in your own package to mirror a TypeScript helper: don’t. They are unexported because they are protocol math, not utilities. `round4` uses integer floor math (`math.Floor(v*10000+0.5)/10000`) rather than a language rounding mode, and `RollupEpisodeRisk` decays momentum by repeated multiplication rather than `math.Pow`, precisely so the trailing digits match the other SDKs. A local reimplementation is a byte-divergence waiting to happen.

## `RiskBands` carries no JSON tags

[Section titled “RiskBands carries no JSON tags”](#riskbands-carries-no-json-tags)

`RiskBands` is a plain struct with exported fields and no struct tags:

```go
type RiskBands struct {
  Low      float64
  Medium   float64
  High     float64
  Critical float64
}
```

Passing it straight to `encoding/json` emits `{"Low":0.05,...}` — Go field spellings, not the protocol key names the other SDKs write. When you need the protocol bytes, project the values into a map and run them through `CanonicalJSON`, which sorts keys and renders whole-valued floats as `1` rather than `1.0`. That is what the parity fixture does, and it is why its band line is comparable to the TypeScript and Python outputs instead of manufacturing a difference no hash would ever see.

## Verified cross-language risk parity

[Section titled “Verified cross-language risk parity”](#verified-cross-language-risk-parity)

This fixture scores three pinned signal sets under `DefaultRiskPolicy` and prints every number through `CanonicalJSON`. Its stdout is byte-identical to the Python twin’s expected output.

src/examples/risk/parity.go

```go
// Command parity is the Go cross-language twin of
// src/examples/risk/score-and-explain.ts (and of src/examples/risk/parity.py).
//
// The invariant this fixture proves: risk scoring is language-neutral protocol
// math, not a TypeScript implementation detail. It scores the SAME three pinned
// signal sets as the TypeScript fixture, under the same reference policy
// (veritio.reference.v1), and prints the score, level, policyVersion and the
// ordered factor contributions. Every printed number must match the
// corresponding value in src/examples/risk/expected/score-and-explain.txt
// exactly — if the Go port of round4 / sat / clamp01 ever drifts from the
// TypeScript one, a documented score moves here and the two expected outputs
// stop agreeing. The stdout is deliberately byte-identical to the Python twin's
// expected output, so all three SDKs share one comparable artifact.
//
// Three details make the comparison honest rather than cosmetic:
//
//   - The signal sets are copied verbatim from the TypeScript fixture,
//     including the deliberately sparse first case, so the fail-closed
//     normalization defaults (recoverable / production / magnitude 0) are
//     exercised in every language.
//   - Numbers are rendered through CanonicalJSON, the cross-language byte
//     contract, rather than through Go's %v or strconv, so the printed digits
//     are the digits a hash would see.
//   - ScoreRiskSignals returns an error in Go where TS/Python throw; the
//     fixture aborts instead of printing a degraded score, because a fixture
//     that silently prints score=0 for a rejected signal would document the
//     opposite of the fail-closed contract.
//
// Deterministic by construction: literal inputs, pinned policy constants, no
// clock, no randomness, no filesystem, no map-iteration order in the output
// (CanonicalJSON sorts keys).
//
// Run with the sibling SDK wired in via a replace directive:
//
//  cp src/examples/risk/parity.go <tmpmod>/main.go && go run .
package main

import (
  "fmt"
  "os"
  "strings"

  veritio "github.com/getveritio/veritio/sdks/go"
)

// riskScenario is one pinned, named signal set. The name is part of the printed
// output so a diff points at the scenario that moved rather than at a line
// number.
type riskScenario struct {
  name    string
  signals veritio.RiskSignals
}

// riskScenarios is pinned verbatim from src/examples/risk/score-and-explain.ts.
// Do not "improve" these values: their only job is to be the same inputs the
// TypeScript and Python fixtures score, so the three expected outputs can be
// diffed field by field. Omitted fields stay at Go's zero value, which is
// exactly how this SDK spells "signal not supplied".
var riskScenarios = []riskScenario{
  {
    name:    "read-config-lookup",
    signals: veritio.RiskSignals{OperationType: "read"},
  },
  {
    name: "bulk-export-staging",
    signals: veritio.RiskSignals{
      OperationType:  "bulk",
      Reversibility:  "reversible",
      EnvCriticality: "staging",
      DataVolume:     5000,
      FanOut:         3,
      ReferenceCount: 12,
    },
  },
  {
    name: "destructive-drop-production",
    signals: veritio.RiskSignals{
      OperationType:  "destructive",
      Reversibility:  "irreversible",
      EnvCriticality: "production",
      DataVolume:     250000,
      FanOut:         40,
      ReferenceCount: 180,
    },
  },
}

// fatal aborts the fixture on any error from the SDK.
//
// Risk normalization and scoring fail closed by design, so swallowing an error
// and printing a partial line would turn this fixture into documentation for
// behavior the SDK does not have. Exiting non-zero also makes the CI harness
// treat a regression as a failure instead of recording a new expected output.
func fatal(err error) {
  fmt.Fprintln(os.Stderr, "risk parity fixture failed:", err)
  os.Exit(1)
}

// canonicalJSON renders a value through the cross-language canonical byte
// contract, aborting if it cannot be encoded.
//
// Parity is a claim about bytes, so the fixture must never fall back to Go's
// default formatting: CanonicalJSON sorts object keys and emits whole-valued
// floats as "1" rather than "1.0", which is what makes these lines comparable
// to the TypeScript and Python outputs instead of manufacturing a difference no
// hash would ever see.
func canonicalJSON(value any) string {
  encoded, err := veritio.CanonicalJSON(value)
  if err != nil {
    fatal(err)
  }
  return encoded
}

// bandsJSON renders the policy bands under their protocol JSON key names.
//
// veritio.RiskBands is a plain Go struct with exported fields and no JSON tags,
// so encoding it directly would print "Low"/"Medium"/… and falsely diverge from
// the TypeScript and Python fixtures. The band thresholds — not the Go field
// spelling — are the protocol surface being pinned here.
func bandsJSON(bands veritio.RiskBands) string {
  return canonicalJSON(map[string]any{
    "low":      bands.Low,
    "medium":   bands.Medium,
    "high":     bands.High,
    "critical": bands.Critical,
  })
}

// rawSignalsJSON renders the caller-supplied signals as the sparse literal the
// TypeScript and Python fixtures declare.
//
// Go has no way to spell "field absent" in a struct: the zero value IS absence,
// which is precisely why NormalizeRiskSignals defaults an empty enum and treats
// a zero magnitude as unsupplied. Omitting zero-valued optional fields here
// reproduces the sparse TS/Python input object, so the following "normalized"
// line demonstrably shows defaults being FILLED IN rather than echoed back.
func rawSignalsJSON(signals veritio.RiskSignals) string {
  raw := map[string]any{"operationType": signals.OperationType}
  if signals.Reversibility != "" {
    raw["reversibility"] = signals.Reversibility
  }
  if signals.EnvCriticality != "" {
    raw["envCriticality"] = signals.EnvCriticality
  }
  if signals.DataVolume != 0 {
    raw["dataVolume"] = signals.DataVolume
  }
  if signals.FanOut != 0 {
    raw["fanOut"] = signals.FanOut
  }
  if signals.ReferenceCount != 0 {
    raw["referenceCount"] = signals.ReferenceCount
  }
  return canonicalJSON(raw)
}

// factorLine flattens the ordered factor breakdown into one comparable line.
//
// Factor ORDER is part of the cross-SDK explainability contract, not a
// presentation choice, so the line is emitted in the order ScoreRiskSignals
// returns rather than sorted: a reordered breakdown must show up as a diff here.
func factorLine(assessment veritio.RiskAssessment) string {
  parts := make([]string, len(assessment.Factors))
  for index, factor := range assessment.Factors {
    parts[index] = factor.Key + ":" + canonicalJSON(factor.Contribution)
  }
  return strings.Join(parts, " ")
}

// main prints the parity report for every pinned scenario under the reference
// policy.
//
// Kept free of any ambient input (clock, environment, argv) so the program is
// byte-identical across runs and machines; the documentation pipeline diffs this
// stdout against the checked-in expected output.
func main() {
  fmt.Printf("policyVersion=%s\n", veritio.DefaultRiskPolicy.PolicyVersion)
  fmt.Printf("bands=%s\n", bandsJSON(veritio.DefaultRiskPolicy.Bands))
  for _, scenario := range riskScenarios {
    normalized, err := veritio.NormalizeRiskSignals(scenario.signals)
    if err != nil {
      fatal(err)
    }
    assessment, err := veritio.ScoreRiskSignals(scenario.signals, veritio.DefaultRiskPolicy)
    if err != nil {
      fatal(err)
    }
    fmt.Println("")
    fmt.Printf("scenario=%s\n", scenario.name)
    fmt.Printf("  signals=%s\n", rawSignalsJSON(scenario.signals))
    fmt.Printf("  normalized=%s\n", canonicalJSON(normalized))
    fmt.Printf("  score=%s\n", canonicalJSON(assessment.Score))
    fmt.Printf("  level=%s\n", assessment.Level)
    fmt.Printf("  policyVersion=%s\n", assessment.PolicyVersion)
    fmt.Printf("  factors=%s\n", factorLine(assessment))
  }
}
```

verified output

```text
policyVersion=veritio.reference.v1
bands={"critical":0.75,"high":0.5,"low":0.05,"medium":0.25}

scenario=read-config-lookup
  signals={"operationType":"read"}
  normalized={"dataVolume":0,"envCriticality":"production","fanOut":0,"operationType":"read","referenceCount":0,"reversibility":"recoverable"}
  score=0.05
  level=low
  policyVersion=veritio.reference.v1
  factors=operationType:0.05 dataVolume:0 fanOut:0 referenceCount:0 reversibility:1 envCriticality:1

scenario=bulk-export-staging
  signals={"dataVolume":5000,"envCriticality":"staging","fanOut":3,"operationType":"bulk","referenceCount":12,"reversibility":"reversible"}
  normalized={"dataVolume":5000,"envCriticality":"staging","fanOut":3,"operationType":"bulk","referenceCount":12,"reversibility":"reversible"}
  score=0.3718
  level=medium
  policyVersion=veritio.reference.v1
  factors=operationType:0.55 dataVolume:0.1961 fanOut:0.0129 referenceCount:0.0155 reversibility:0.6 envCriticality:0.8

scenario=destructive-drop-production
  signals={"dataVolume":250000,"envCriticality":"production","fanOut":40,"operationType":"destructive","referenceCount":180,"reversibility":"irreversible"}
  normalized={"dataVolume":250000,"envCriticality":"production","fanOut":40,"operationType":"destructive","referenceCount":180,"reversibility":"irreversible"}
  score=1
  level=critical
  policyVersion=veritio.reference.v1
  factors=operationType:0.85 dataVolume:0.1999 fanOut:0.0738 referenceCount:0.0626 reversibility:1.3 envCriticality:1
```

Two details in that output are worth reading closely. The first scenario supplies only `OperationType`, and the `normalized` line shows the defaults being filled in — `reversibility=recoverable`, `envCriticality=production`, magnitudes `0`. In Go that sparse input is not an option type; it is the struct’s zero value. `RiskSignals` has no way to distinguish “field omitted” from “field set to zero”, which is exactly why `NormalizeRiskSignals` treats an empty enum as unset and a zero magnitude as unsupplied.

The second detail: magnitudes are typed `float64` but normalization rejects anything fractional, negative, `NaN`, or infinite, and coerces IEEE-754 negative zero to positive zero so the metadata bytes render `0` rather than Go’s `-0`. Pass integers.

## Governed action drafts

[Section titled “Governed action drafts”](#governed-action-drafts)

`CreateGovernedActionDraft` derives change and activity IDs, changed paths, revision evidence, event actions, edge relations, and the outbox entry from one pinned input. The Go derivation is byte-identical to the TypeScript and Python ones.

src/examples/governed/action-draft.go

```go
// Package main is the Go half of the governed-action parity fixture.
//
// It runs CreateGovernedActionDraft over the SAME pinned entity definition,
// before/after rows, actor refs, occurredAt, and idempotency key as the
// TypeScript fixture in src/examples/governed/action-draft.ts, and prints the
// derived values in the identical JSON shape. The invariant it proves is
// cross-language determinism of the governed-action derivation:
//
//   - The change id and the activity id come from ONE seed —
//     sha256("<tenantId>:<idempotencyKey>") truncated to 16 hex chars — so a
//     replayed mutation reproduces the same change/activity pair in Go as in
//     TypeScript instead of forking the evidence graph per language.
//   - The revision id is content-addressed by the state digest and scoped by
//     the change id, so both SDKs land on the same rev_... identifier.
//   - The state commitment is built from the entity capture policy alone: an
//     `omit` field never reaches the commitment or the changed paths, and a
//     `content_digest` field is committed as a digest, so the Go outbox is
//     byte-comparable with the TypeScript one rather than a copy of the row.
//   - Changed paths are inferred only from governed fields, and the draft emits
//     the same three event actions and four edge relations in the same order.
//   - The draft is INPUT ONLY in Go too: AuditEventInput carries no
//     store-assigned sequence and no record hash, which this fixture shows by
//     marshalling each event and looking for those keys.
//
// Every id, timestamp, and idempotency key is pinned, so stdout is byte-stable
// across runs and byte-identical to
// src/examples/governed/expected/action-draft.txt (the TypeScript fixture's
// expected output).
package main

import (
  "encoding/json"
  "fmt"
  "sort"

  veritio "github.com/getveritio/veritio/sdks/go"
)

// derivedIDs mirrors the TypeScript fixture's `derivedIds` object field order
// so the two outputs can be compared byte-for-byte instead of semantically.
type derivedIDs struct {
  ChangeID                     string `json:"changeId"`
  ActivityID                   string `json:"activityId"`
  EntityID                     string `json:"entityId"`
  RevisionID                   string `json:"revisionId"`
  IDSeed                       string `json:"idSeed"`
  ChangeAndActivityShareOneSed bool   `json:"changeAndActivityShareOneSeed"`
}

// fieldCommitment is the committed shape of a `content_digest` governed field:
// the capture mode plus the digest of the canonical value, never the value.
type fieldCommitment struct {
  CaptureMode string `json:"captureMode"`
  Digest      string `json:"digest"`
}

// stateCommitmentView projects the draft's state commitment down to the fields
// the parity comparison depends on, in the TypeScript fixture's key order.
type stateCommitmentView struct {
  Algorithm              string          `json:"algorithm"`
  Canonicalization       string          `json:"canonicalization"`
  SchemaRef              string          `json:"schemaRef"`
  FieldSetRef            string          `json:"fieldSetRef"`
  Digest                 string          `json:"digest"`
  CommittedFields        []string        `json:"committedFields"`
  OmittedFieldPresent    bool            `json:"omittedFieldPresent"`
  AccountEmailCommitment fieldCommitment `json:"accountEmailCommitment"`
}

// draftIsInputOnlyView records that a draft is not yet evidence: it reports
// whether any emitted event already carries a store-assigned sequence or a
// record hash (both must be false until a conforming AuditStore appends them).
type draftIsInputOnlyView struct {
  Note                             string `json:"note"`
  EventsCarryStoreAssignedSequence bool   `json:"eventsCarryStoreAssignedSequence"`
  EventsCarryRecordHash            bool   `json:"eventsCarryRecordHash"`
  OutboxMutationBinding            string `json:"outboxMutationBinding"`
  OutboxSchemaVersion              string `json:"outboxSchemaVersion"`
}

// fixtureOutput is the whole printed document, ordered to match the TypeScript
// fixture's console output exactly.
type fixtureOutput struct {
  DerivedIDs      derivedIDs           `json:"derivedIds"`
  ChangedPaths    []string             `json:"changedPaths"`
  StateCommitment stateCommitmentView  `json:"stateCommitment"`
  EventActions    []string             `json:"eventActions"`
  EdgeRelations   []string             `json:"edgeRelations"`
  DraftIsInput    draftIsInputOnlyView `json:"draftIsInputOnly"`
}

// governedScope pins the tenant the idempotency seed is scoped by. The seed is
// sha256("<tenantId>:<idempotencyKey>"), so tenantId is hash-affecting input,
// not decoration.
var governedScope = veritio.EvidenceScope{TenantID: "org_acme", Environment: "production"}

// subscriptionBefore is the pinned pre-mutation row. Go rows are plain maps;
// seatCount stays an int so its canonical JSON encoding ("12") matches the
// TypeScript number literal rather than a float rendering.
var subscriptionBefore = map[string]any{
  "id":            "sub_9f31",
  "accountEmail":  "billing@acme.example",
  "plan":          "team",
  "seatCount":     12,
  "status":        "active",
  "internalNotes": "renewal call scheduled",
}

// subscriptionAfter is the pinned post-mutation row. Only `plan`, `seatCount`,
// and the ungoverned `internalNotes` differ, so exactly two governed changed
// paths must be inferred.
var subscriptionAfter = map[string]any{
  "id":            "sub_9f31",
  "accountEmail":  "billing@acme.example",
  "plan":          "enterprise",
  "seatCount":     25,
  "status":        "active",
  "internalNotes": "upgrade approved on the renewal call",
}

/*
defineSubscriptionEntity declares the governed entity. DefineEntity is the only
place field capture is decided: `internalNotes` is `omit` so operator prose can
never reach the commitment or the changed paths, and `accountEmail` is
`content_digest` so the PII value is committed as a digest. The capture policy
is hash-affecting, so it must be declared identically to the TypeScript fixture
for the digests to agree.
*/
func defineSubscriptionEntity() (veritio.GovernedEntityDefinition, error) {
  return veritio.DefineEntity(veritio.GovernedEntityDefinition{
    Authority:   "acme-billing",
    Type:        "subscription",
    SchemaRef:   "acme://schemas/subscription@3",
    FieldSetRef: "acme://fieldsets/subscription-governed@1",
    Identity: func(row map[string]any) string {
      id, _ := row["id"].(string)
      return id
    },
    Fields: map[string]veritio.EntityFieldPolicy{
      "id":            {Capture: "full"},
      "plan":          {Capture: "full"},
      "seatCount":     {Capture: "full"},
      "status":        {Capture: "full"},
      "accountEmail":  {Capture: "content_digest"},
      "internalNotes": {Capture: "omit"},
    },
  })
}

/*
buildSubscriptionUpgradeDraft builds the pinned upgrade draft. Every input that
feeds an id, a digest, or a changed path is fixed here — no clock, no random
source — so the derivation is reproducible and directly comparable with the
TypeScript and Python SDKs.
*/
func buildSubscriptionUpgradeDraft() (veritio.GovernedChangeDraft, error) {
  entity, err := defineSubscriptionEntity()
  if err != nil {
    return veritio.GovernedChangeDraft{}, err
  }
  return veritio.CreateGovernedActionDraft(veritio.GovernedActionDraftInput{
    Scope:           governedScope,
    Entity:          entity,
    Before:          subscriptionBefore,
    After:           subscriptionAfter,
    ActionType:      "subscription.upgraded",
    ActivityType:    "billing.plan_change",
    InitiatedBy:     veritio.EvidenceRef{Authority: "acme-billing", Kind: "principal", Type: "user", ID: "usr_owner"},
    PerformedBy:     veritio.EvidenceRef{Authority: "acme-billing", Kind: "principal", Type: "service", ID: "svc_billing_api"},
    Producer:        veritio.EvidenceRef{Authority: "acme-billing", Kind: "principal", Type: "service", ID: "svc_billing_api"},
    OccurredAt:      "2026-08-09T10:00:00.000Z",
    IdempotencyKey:  "subscription.upgraded:sub_9f31:req_7c2a",
    MutationBinding: "same_transaction",
  })
}

/*
eventCarriesKey reports whether any drafted event serializes the given key. It
is the Go equivalent of the TypeScript fixture's `'sequence' in event` probe:
AuditEventInput has no sequence or hash member at all, so a draft can never
present itself as an appended record.
*/
func eventCarriesKey(events []veritio.AuditEventInput, key string) (bool, error) {
  for _, event := range events {
    encoded, err := json.Marshal(event)
    if err != nil {
      return false, err
    }
    var decoded map[string]any
    if err := json.Unmarshal(encoded, &decoded); err != nil {
      return false, err
    }
    if _, ok := decoded[key]; ok {
      return true, nil
    }
  }
  return false, nil
}

/*
main prints the parity document. Failures panic rather than printing a partial
document, so a fixture that cannot derive the pinned draft fails the CI byte
comparison loudly instead of silently drifting.
*/
func main() {
  draft, err := buildSubscriptionUpgradeDraft()
  if err != nil {
    panic(err)
  }

  changeID := draft.ChangeRef.ID
  idSeed := changeID[len("chg_subscription_")+len(draft.EntityRef.ID)+1:]

  committedFields := make([]string, 0, len(draft.Revision.StateCommitment.Fields))
  for field := range draft.Revision.StateCommitment.Fields {
    committedFields = append(committedFields, field)
  }
  sort.Strings(committedFields)

  _, omittedFieldPresent := draft.Revision.StateCommitment.Fields["internalNotes"]

  accountEmail, ok := draft.Revision.StateCommitment.Fields["accountEmail"].(map[string]any)
  if !ok {
    panic("accountEmail must be committed as a content digest")
  }
  captureMode, _ := accountEmail["captureMode"].(string)
  accountEmailDigest, _ := accountEmail["digest"].(string)

  eventActions := make([]string, 0, len(draft.Events))
  for _, event := range draft.Events {
    eventActions = append(eventActions, event.Action)
  }
  edgeRelations := make([]string, 0, len(draft.Edges))
  for _, edge := range draft.Edges {
    edgeRelations = append(edgeRelations, edge.Relation)
  }

  carriesSequence, err := eventCarriesKey(draft.Events, "sequence")
  if err != nil {
    panic(err)
  }
  carriesHash, err := eventCarriesKey(draft.Events, "hash")
  if err != nil {
    panic(err)
  }

  output := fixtureOutput{
    DerivedIDs: derivedIDs{
      ChangeID:   changeID,
      ActivityID: draft.ActivityRef.ID,
      EntityID:   draft.EntityRef.ID,
      RevisionID: draft.Revision.Ref.ID,
      IDSeed:     idSeed,
      ChangeAndActivityShareOneSed: changeID == fmt.Sprintf("chg_subscription_%s_%s", draft.EntityRef.ID, idSeed) &&
        draft.ActivityRef.ID == fmt.Sprintf("act_subscription_%s_%s", draft.EntityRef.ID, idSeed),
    },
    ChangedPaths: draft.Revision.ChangedPaths,
    StateCommitment: stateCommitmentView{
      Algorithm:              draft.Revision.StateCommitment.Algorithm,
      Canonicalization:       draft.Revision.StateCommitment.Canonicalization,
      SchemaRef:              draft.Revision.StateCommitment.SchemaRef,
      FieldSetRef:            draft.Revision.StateCommitment.FieldSetRef,
      Digest:                 draft.Revision.StateCommitment.Digest,
      CommittedFields:        committedFields,
      OmittedFieldPresent:    omittedFieldPresent,
      AccountEmailCommitment: fieldCommitment{CaptureMode: captureMode, Digest: accountEmailDigest},
    },
    EventActions:  eventActions,
    EdgeRelations: edgeRelations,
    DraftIsInput: draftIsInputOnlyView{
      Note:                             "CreateGovernedActionDraft returns evidence INPUTS, not persisted records. Append draft.OutboxEntry through a conforming AuditStore inside the same mutation to make it evidence.",
      EventsCarryStoreAssignedSequence: carriesSequence,
      EventsCarryRecordHash:            carriesHash,
      OutboxMutationBinding:            draft.OutboxEntry.MutationBinding,
      OutboxSchemaVersion:              draft.OutboxEntry.SchemaVersion,
    },
  }

  encoded, err := json.MarshalIndent(output, "", "  ")
  if err != nil {
    panic(err)
  }
  fmt.Println(string(encoded))
}
```

verified output

```json
{
  "derivedIds": {
    "changeId": "chg_subscription_sub_9f31_312ca4bde590b545",
    "activityId": "act_subscription_sub_9f31_312ca4bde590b545",
    "entityId": "sub_9f31",
    "revisionId": "rev_subscription_sub_9f31_a71e2768812c_fe34f6d0",
    "idSeed": "312ca4bde590b545",
    "changeAndActivityShareOneSeed": true
  },
  "changedPaths": [
    "/plan",
    "/seatCount"
  ],
  "stateCommitment": {
    "algorithm": "sha256",
    "canonicalization": "veritio-json-v1",
    "schemaRef": "acme://schemas/subscription@3",
    "fieldSetRef": "acme://fieldsets/subscription-governed@1",
    "digest": "sha256:a71e2768812cca4e3609ce7e24460967d253e1ddab13d9a8898f4fd31a3564cb",
    "committedFields": [
      "accountEmail",
      "id",
      "plan",
      "seatCount",
      "status"
    ],
    "omittedFieldPresent": false,
    "accountEmailCommitment": {
      "captureMode": "content_digest",
      "digest": "sha256:aa69b0bc0b21a0ca7b5b5d1beb7556eb7d598f21d638db3eb9770ddd4fb30e45"
    }
  },
  "eventActions": [
    "change.declared",
    "activity.recorded",
    "entity.revision.created"
  ],
  "edgeRelations": [
    "has_activity",
    "has_output",
    "performed_by",
    "generated"
  ],
  "draftIsInputOnly": {
    "note": "CreateGovernedActionDraft returns evidence INPUTS, not persisted records. Append draft.OutboxEntry through a conforming AuditStore inside the same mutation to make it evidence.",
    "eventsCarryStoreAssignedSequence": false,
    "eventsCarryRecordHash": false,
    "outboxMutationBinding": "same_transaction",
    "outboxSchemaVersion": "2026-06-23"
  }
}
```

The `draftIsInputOnly` block is the important part. `eventsCarryStoreAssignedSequence` and `eventsCarryRecordHash` are both `false`, and they are false structurally: `AuditEventInput` has no sequence or hash member, so a draft cannot present itself as an appended record. A draft becomes evidence only when its outbox entry is appended through a conforming store inside the same transaction.

Note what the state commitment does with field capture. `internalNotes` is declared `omit`, so `omittedFieldPresent` is `false` — operator prose never reaches the commitment or the changed paths. `accountEmail` is declared `content_digest`, so it is committed as `sha256:aa69b0bc…` rather than as the address. Capture policy is hash-affecting: declare it identically across SDKs or the digests diverge.

## Trap: a doc comment names two functions that do not exist

[Section titled “Trap: a doc comment names two functions that do not exist”](#trap-a-doc-comment-names-two-functions-that-do-not-exist)

`sdks/go/event.go` documents `VerifyEvidenceCommits` like this:

```text
Per-record integrity comes from
VerifyAuditRecords/VerifyEvidenceEdgeRecords; compose both for end-to-end
evidence verification.
```

Neither function exists in the Go module. Confirm it yourself:

Terminal window

```sh
grep -rn "func VerifyAuditRecords\|func VerifyEvidenceEdgeRecords" sdks/go/
# no matches
```

The only exported verifier in the Go package is `VerifyEvidenceCommits`. The two named functions are TypeScript APIs; the comment was written against the protocol, not against the Go surface, and it will mislead you if you read it as a to-do list of local calls.

The advice inside the comment is still correct as protocol guidance, and the gap it describes is real. `VerifyEvidenceCommits` proves the commit **ledger’s** internal consistency only: sequence and previous-hash linkage per stream, member manifest, Merkle root, and commit hash. It deliberately does not reconcile member record hashes against independently verified records. A fabricated commit chain built over fabricated record hashes verifies `OK` in isolation.

```text
VerifyEvidenceCommits proves      the commit ledger links correctly
VerifyEvidenceCommits cannot prove  the records those commits point at are genuine
```

For per-record integrity in a Go deployment today you either verify records through the TypeScript verifier, or reimplement the record walk against `HashAuditRecord` / `HashEvidenceEdgeRecord`, which the module does export. Treat an `OK` commit result as one half of end-to-end verification, never as the whole of it.

## Supported capabilities

[Section titled “Supported capabilities”](#supported-capabilities)

Area

Public Go APIs

Events and canonical JSON

`CreateAuditEvent`, `CanonicalJSON`, `HashAuditEvent`, `HashAuditRecord`

Edges

`CreateEvidenceEdge`, `HashEvidenceEdge`, `HashEvidenceEdgeRecord`

Evidence commits

`CreateEvidenceCommit`, `HashEvidenceCommit`, `VerifyEvidenceCommits`

Idempotency digest

`HashIdempotencyKey`

Governed changes

`DefineEntity`, `GovernedRevisionID`, `CreateGovernedChangeDraft`, `CreateGovernedActionDraft`

Audit templates

Auth, organization, consent, subject request, retention, agent, review, CI, deployment, and runtime helpers

Deterministic risk

`NormalizeRiskSignals`, `ScoreRiskSignals`, `RollupEpisodeRisk`, `WithRiskSignals`, `BandOf`, `RiskPolicy`, `Float64`, `DefaultRiskPolicy`

Risk assertions

`CreateSecurityRiskAssertion`, `HashAssertionRecord`, `BuildSecurityRiskAssessedEvent`

## Known boundaries

[Section titled “Known boundaries”](#known-boundaries)

The module does **not** expose `AuditStore`, `MemoryAuditStore`, `CreateAuditRecorder`, `VerifyAuditRecords`, or `VerifyEvidenceEdgeRecords` at the verified revision. It also has no provenance recorder, so the `metadata.sessionId` and `metadata.activityEpisodeId` stamps and the agent-capture classifier remain TypeScript-only parity work.

The Go SDK can create, hash, and score the same portable semantics as the other SDKs. Durable ordered persistence and full record-chain verification require a host implementation or the existing TypeScript and storage path until equivalent Go APIs land upstream. Check the [parity matrix](/docs/reference/parity-matrix/) before porting a TypeScript workflow, so a missing API surfaces at design time rather than halfway through the port.

None of this establishes legal compliance. It produces evidence that a reviewer can check independently.

## Verification commands

[Section titled “Verification commands”](#verification-commands)

Inside `sdks/go` at the pinned revision:

Terminal window

```sh
gofmt -d .
go test ./...
```

In this website repository, `bun run verify:examples` repeats the format, compile, run, output, source-revision, content-hash, and cross-language checks for every Go fixture on this page.

Continue with the [parity matrix](/docs/reference/parity-matrix/) to see which capabilities exist per language, the [risk policy reference](/docs/reference/risk-policy/) for the constants `DefaultRiskPolicy` pins, or the [event schema](/docs/reference/event-schema/) for the field names all three SDKs share.

[Edit page](https://github.com/getveritio/veritio-website/edit/main/src/content/docs/docs/sdks/go.mdx)

Last updated: Aug 23, 2026

[Previous  
Python](/docs/sdks/python/)[Next  
CLI](/docs/sdks/cli/)

Veritio provides evidence support, not legal advice or automatic compliance.

This site uses cookieless, anonymous analytics (Umami) by default. With your consent, we also enable Google Analytics, which sets cookies and sends usage data to Google. [Privacy Policy](/legal/privacy/)
