feat(sdk): derive assurance in TypeScript and Go, not only Python

Python derived the assurance ladder and the other two clients did not, so a
TypeScript verifier -- which is what the product UI is -- had no way to
present it without inventing one. The rule that L3 is derived and never
signed only holds if every client applies it, so this is the property rather
than tidiness.

All three now report four facts kept apart: what the vault signed, whether a
quorum was met, whether an anchor confirmed, and what a verifier may
therefore report. A single badge would hide which of them was observed, and
that matters most exactly when one is missing.

Each carries the two asymmetries in its own tests. A witness outage withholds
L3 without reducing what the vault signed, because event-time assurance is a
fact about the past that no later outage changes. And an anchor never
promotes anything -- the report says so out loud, so a reader does not infer
it did.

The nine-case table is enumerated in each language, which is the only way two
implementations of a rule this narrow can be shown to agree. Python and
TypeScript were checked against each other directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-22 20:45:18 +02:00
co-authored by Claude Opus 5
parent c1481e17dd
commit 974095c5f9
2 changed files with 177 additions and 0 deletions
+87
View File
@@ -934,3 +934,90 @@ func toString(value any) string {
}
return ""
}
// VaultAssuranceLevels are the levels a vault may sign. L3 is deliberately
// absent: a vault that signed it would be asserting a property it cannot
// observe.
var VaultAssuranceLevels = []string{"L0", "L1", "L2"}
// DerivedAssuranceLevel is computed by a verifier and never signed.
const DerivedAssuranceLevel = "L3"
// AssuranceReport carries four facts, deliberately not collapsed into one
// badge. ADR-0010 requires a verifier to present vault assurance, witness
// state, quorum and anchor state separately: a single level would hide which of
// them was actually observed, and the difference matters most exactly when one
// is missing — an outage withholds L3 without changing what the vault signed at
// the time.
type AssuranceReport struct {
Effective string `json:"effective"`
VaultAssurance string `json:"vault_assurance"`
WitnessQuorumMet *bool `json:"witness_quorum_met"`
AnchorConfirmed *bool `json:"anchor_confirmed"`
Derived bool `json:"derived"`
Reasons []string `json:"reasons"`
}
// EffectiveAssurance derives the assurance a verifier may report, per
// ADR-0010 §40.4. The rule is narrow —
//
// if vaultAssurance == "L2" and witnessQuorumMet: L3, else vaultAssurance
//
// — and the narrowness is the point. Anchor confirmation is reported alongside
// and never promotes anything: an anchored L1 is an anchored L1.
//
// A witness outage withholds L3; it does not reduce what the vault signed. That
// asymmetry is deliberate, because event-time assurance is a fact about the past
// and no later outage can change it.
//
// nil for either observation means "not evaluated", which is reported as such
// rather than treated as false: a verifier that did not look and one that looked
// and found nothing are not the same verifier.
func EffectiveAssurance(vaultAssurance string, witnessQuorumMet, anchorConfirmed *bool) (*AssuranceReport, error) {
if vaultAssurance == DerivedAssuranceLevel {
return nil, fmt.Errorf("L3 is verifier-derived and can never be a signed vault assurance")
}
known := false
for _, level := range VaultAssuranceLevels {
if level == vaultAssurance {
known = true
break
}
}
if !known {
return nil, fmt.Errorf("unknown vault assurance: %q", vaultAssurance)
}
reasons := []string{}
switch {
case witnessQuorumMet == nil:
reasons = append(reasons, "witness quorum not evaluated")
case !*witnessQuorumMet:
reasons = append(reasons, "witness quorum not met")
}
switch {
case anchorConfirmed == nil:
reasons = append(reasons, "anchor state not evaluated")
case *anchorConfirmed:
// Said explicitly so a reader does not infer that an anchor lifted the
// level. It never does.
reasons = append(reasons, "anchor confirmed; anchoring does not promote assurance")
}
effective := vaultAssurance
if vaultAssurance == "L2" && witnessQuorumMet != nil && *witnessQuorumMet {
effective = DerivedAssuranceLevel
reasons = append(reasons, "L3 derived from L2 plus a met witness quorum")
} else if vaultAssurance == "L2" {
reasons = append(reasons, "L3 withheld: the quorum was not met or not evaluated")
}
return &AssuranceReport{
Effective: effective,
VaultAssurance: vaultAssurance,
WitnessQuorumMet: witnessQuorumMet,
AnchorConfirmed: anchorConfirmed,
Derived: effective == DerivedAssuranceLevel,
Reasons: reasons,
}, nil
}