Go SDK
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”go get github.com/getveritio/veritio/sdks/go@c4100ee7b678d0c6b227c67ae6ea1d8a1f373967The module declares Go 1.22. Confirm the resolved revision with:
go list -m -json github.com/getveritio/veritio/sdks/goCreate the shared event
Section titled “Create the shared event”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:
go run .event=evt_member_invited_01 hash=3c1eb99f7a8fThe 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”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.
TypeScript throws → an ignored error is impossiblePython raises → an ignored error is impossibleGo returns error → an ignored error is a silent zero valueAn 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”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:
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”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:
var zero veritio.RiskScoringPolicyScoreRiskSignals({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”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:
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”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 is a plain struct with exported fields and no struct tags:
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”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.
// 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)) }}policyVersion=veritio.reference.v1bands={"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:1Two 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”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.
// 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 onlyplace field capture is decided: `internalNotes` is `omit` so operator prose cannever reach the commitment or the changed paths, and `accountEmail` is`content_digest` so the PII value is committed as a digest. The capture policyis hash-affecting, so it must be declared identically to the TypeScript fixturefor 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 thatfeeds an id, a digest, or a changed path is fixed here — no clock, no randomsource — so the derivation is reproducible and directly comparable with theTypeScript 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. Itis 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 neverpresent 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 partialdocument, so a fixture that cannot derive the pinned draft fails the CI bytecomparison 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))}{ "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”sdks/go/event.go documents VerifyEvidenceCommits like this:
Per-record integrity comes fromVerifyAuditRecords/VerifyEvidenceEdgeRecords; compose both for end-to-endevidence verification.Neither function exists in the Go module. Confirm it yourself:
grep -rn "func VerifyAuditRecords\|func VerifyEvidenceEdgeRecords" sdks/go/# no matchesThe 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.
VerifyEvidenceCommits proves the commit ledger links correctlyVerifyEvidenceCommits cannot prove the records those commits point at are genuineFor 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”| 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”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 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”Inside sdks/go at the pinned revision:
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 to see which capabilities exist per language, the risk policy reference for the constants DefaultRiskPolicy pins, or the event schema for the field names all three SDKs share.