ship(P06): merge Identity/Standing into milestone v0.1

---ci---
project: oy
phase: 6
milestone: v0.1
status: complete
ship: v0.0.6
---/ci---
This commit is contained in:
CIAgent
2026-08-17 20:00:21 +00:00
3 changed files with 326 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
package types
import "encoding/json"
const (
ModuleName = "identity"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
)
// Reach is the mesh-native identity layer (§8, §9.6 Layer 1)
// Cryptographic, virtual, no state identity
type Reach struct {
ReachID string `json:"reach_id" yaml:"reach_id"`
HolderID string `json:"holder_id" yaml:"holder_id"`
CreatedAt int64 `json:"created_at" yaml:"created_at"`
PublicKey string `json:"public_key" yaml:"public_key"`
IsNomad bool `json:"is_nomad" yaml:"is_nomad"`
IsFreeholder bool `json:"is_freeholder" yaml:"is_freeholder"`
}
// CitizenshipTier (§9)
type CitizenshipTier string
const (
TierNomad CitizenshipTier = "Nomad" // any Holder (lifestyle tier)
TierFreeholder CitizenshipTier = "Freeholder" // earned full recognition (sovereign tier)
)
// TwoLayerPassport (§9.6)
// Layer 1: Reach (mesh-native, virtual, cryptographic)
// Layer 2: Pier-routed real-world credentials (e-Residency, biometric IDs)
// OY does not issue passports; OY routes to Piers that do.
type TwoLayerPassport struct {
ReachID string `json:"reach_id" yaml:"reach_id"`
Layer1Reach bool `json:"layer1_reach" yaml:"layer1_reach"`
Layer2PierRouted bool `json:"layer2_pier_routed" yaml:"layer2_pier_routed"`
PierCredentialRef string `json:"pier_credential_ref" yaml:"pier_credential_ref"`
}
// WayfarersRecord is an optional Reach-signed attestation (§9.5)
// Credential of belonging, not state identity
type WayfarersRecord struct {
ReachID string `json:"reach_id" yaml:"reach_id"`
FreeholderStatus bool `json:"freeholder_status" yaml:"freeholder_status"`
StandingScore float64 `json:"standing_score" yaml:"standing_score"`
GuildAffiliations []string `json:"guild_affiliations" yaml:"guild_affiliations"`
BloomHistory string `json:"bloom_history" yaml:"bloom_history"`
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
}
type Params struct{}
func DefaultParams() Params { return Params{} }
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Reaches []Reach `json:"reaches" yaml:"reaches"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{Params: DefaultParams(), Reaches: []Reach{}}
}
func ValidateGenesis(bz json.RawMessage) error { return nil }
+160
View File
@@ -0,0 +1,160 @@
package types
import "encoding/json"
const (
ModuleName = "standing"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// Standing formula parameters (§9.2 — LOCKED)
PriorMean = 4.0 // Bayesian prior mean
PriorWeight = 10 // Bayesian prior weight
DecayBucket6mo = 1.0 // 6mo: 100pct
DecayBucket12mo = 0.5 // 12mo: 50pct
DecayBucket24mo = 0.25 // 24mo: 25pct
DecayBucket24plus = 0.0 // 24mo+: 0pct
// Diversity bonus (§9.2)
DiversityBonus3Cats = 0.05
DiversityBonus4Cats = 0.10
DiversityBonus5Cats = 0.15
// Voucher weights (§9.2)
VoucherWeightFreeholder = 1.5
VoucherWeight45Plus = 1.2
VoucherWeight40To45 = 1.0
VoucherWeightBelow40 = 0.5
VoucherWeightBelow10Ratings = 0.3
// Minimum counterparties (§9.2)
MinCounterpartiesValid = 3 // to be valid
MinCounterpartiesFreeholderEligible = 10 // to be Freeholder-eligible
MinCounterpartiesFreeholderStatus = 30 // across 3 categories for Freeholder status
// Freeholder Four Signals (§9.1 — LOCKED)
FreeholderStashMaturityDays = 90 // 90 days Stash activity
FreeholderStashMaxGapDays = 30 // no gap > 30 days
FreeholderMinStandingScore = 4.5 // 4.5+ in at least 3 service categories
FreeholderMinCategories = 3 // at least 3 service categories
)
// Rating is a single rating event (§9.2)
type Rating struct {
RaterID string `json:"rater_id" yaml:"rater_id"`
RateeID string `json:"ratee_id" yaml:"ratee_id"`
Category string `json:"category" yaml:"category"`
Score float64 `json:"score" yaml:"score"` // 0.0-5.0
Weight float64 `json:"weight" yaml:"weight"` // derived from rater Voucher_Weight
TxRef string `json:"tx_ref" yaml:"tx_ref"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
DecayBucket uint8 `json:"decay_bucket" yaml:"decay_bucket"`
}
// Vouch is a Freeholder vouch with skin-in-the-game (§9.1)
type Vouch struct {
VoucherID string `json:"voucher_id" yaml:"voucher_id"`
VoucheeID string `json:"vouchee_id" yaml:"vouchee_id"`
Category string `json:"category" yaml:"category"`
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"` // voucher skin-in-the-game
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
}
// Slash penalizes a Holder (§9.4)
type Slash struct {
ReachID string `json:"reach_id" yaml:"reach_id"`
Amount float64 `json:"amount" yaml:"amount"`
Reason string `json:"reason" yaml:"reason"` // Crack, FraudulentCoverCall, InactivityTimeout
Attester string `json:"attester" yaml:"attester"` // Watcher ID
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
}
// StandingBucket for display (§9.2)
type StandingBucket string
const (
BucketNew StandingBucket = "New" // <10 ratings
BucketTrusted StandingBucket = "Trusted" // 4.0-4.4
BucketPreferred StandingBucket = "Preferred" // 4.5-4.7
BucketTop StandingBucket = "Top" // 4.8+
BucketSlashed StandingBucket = "Slashed" // penalized
)
// FreeholderSignals — all four must be present (§9.1 LOCKED)
type FreeholderSignals struct {
StashMaturity bool `json:"stash_maturity" yaml:"stash_maturity"`
MultiDomainStanding bool `json:"multi_domain_standing" yaml:"multi_domain_standing"`
CommittedCapital bool `json:"committed_capital" yaml:"committed_capital"`
CommunityEndorsement bool `json:"community_endorsement" yaml:"community_endorsement"`
}
// IsFreeholderEligible checks all four signals (§9.1)
// No application, no committee, no form. All four must be present.
func (s FreeholderSignals) IsFreeholderEligible() bool {
return s.StashMaturity && s.MultiDomainStanding && s.CommittedCapital && s.CommunityEndorsement
}
// ComputeDiversityBonus returns the bonus for category count (§9.2)
func ComputeDiversityBonus(categoryCount int) float64 {
switch {
case categoryCount >= 5:
return DiversityBonus5Cats
case categoryCount == 4:
return DiversityBonus4Cats
case categoryCount == 3:
return DiversityBonus3Cats
}
return 0.0
}
// GetVoucherWeight returns the weight for a given rater profile (§9.2)
func GetVoucherWeight(isFreeholder bool, standingScore float64, ratingCount int) float64 {
if isFreeholder {
return VoucherWeightFreeholder
}
if ratingCount < 10 {
return VoucherWeightBelow10Ratings
}
if standingScore >= 4.5 {
return VoucherWeight45Plus
}
if standingScore >= 4.0 {
return VoucherWeight40To45
}
return VoucherWeightBelow40
}
// GetStandingBucket returns the display bucket for a score (§9.2)
func GetStandingBucket(score float64, ratingCount int, isSlashed bool) StandingBucket {
if isSlashed {
return BucketSlashed
}
if ratingCount < 10 {
return BucketNew
}
switch {
case score >= 4.8:
return BucketTop
case score >= 4.5:
return BucketPreferred
case score >= 4.0:
return BucketTrusted
}
return BucketNew
}
type Params struct{}
func DefaultParams() Params { return Params{} }
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Ratings []Rating `json:"ratings" yaml:"ratings"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{Params: DefaultParams(), Ratings: []Rating{}}
}
func ValidateGenesis(bz json.RawMessage) error { return nil }
+100
View File
@@ -0,0 +1,100 @@
package types_test
import (
"testing"
"github.com/oy/openyield/x/standing/types"
)
func TestFreeholderSignalsAllPresent(t *testing.T) {
signals := types.FreeholderSignals{
StashMaturity: true,
MultiDomainStanding: true,
CommittedCapital: true,
CommunityEndorsement: true,
}
if !signals.IsFreeholderEligible() {
t.Error("All four signals present should be Freeholder eligible (§9.1)")
}
}
func TestFreeholderSignalsMissing(t *testing.T) {
signals := types.FreeholderSignals{
StashMaturity: true,
MultiDomainStanding: true,
CommittedCapital: true,
CommunityEndorsement: false, // missing
}
if signals.IsFreeholderEligible() {
t.Error("Missing one signal should NOT be Freeholder eligible (§9.1: all four must be present)")
}
}
func TestDiversityBonus(t *testing.T) {
if types.ComputeDiversityBonus(3) != 0.05 {
t.Error("3 categories should give +0.05 bonus (§9.2)")
}
if types.ComputeDiversityBonus(4) != 0.10 {
t.Error("4 categories should give +0.10 bonus (§9.2)")
}
if types.ComputeDiversityBonus(5) != 0.15 {
t.Error("5+ categories should give +0.15 bonus (§9.2)")
}
if types.ComputeDiversityBonus(2) != 0.0 {
t.Error("2 categories should give 0 bonus (§9.2)")
}
}
func TestVoucherWeights(t *testing.T) {
if types.GetVoucherWeight(true, 4.0, 100) != 1.5 {
t.Error("Freeholder weight should be 1.5x (§9.2)")
}
if types.GetVoucherWeight(false, 4.6, 100) != 1.2 {
t.Error("4.5+ with 1-2 cats should be 1.2x (§9.2)")
}
if types.GetVoucherWeight(false, 4.2, 100) != 1.0 {
t.Error("4.0-4.5 should be 1.0x (§9.2)")
}
if types.GetVoucherWeight(false, 3.5, 100) != 0.5 {
t.Error("Below 4.0 should be 0.5x (§9.2)")
}
if types.GetVoucherWeight(false, 4.0, 5) != 0.3 {
t.Error("Below 10 ratings should be 0.3x (§9.2)")
}
}
func TestStandingBuckets(t *testing.T) {
if types.GetStandingBucket(4.9, 100, false) != types.BucketTop {
t.Error("4.8+ should be Top (§9.2)")
}
if types.GetStandingBucket(4.6, 100, false) != types.BucketPreferred {
t.Error("4.5-4.7 should be Preferred (§9.2)")
}
if types.GetStandingBucket(4.2, 100, false) != types.BucketTrusted {
t.Error("4.0-4.4 should be Trusted (§9.2)")
}
if types.GetStandingBucket(3.5, 100, false) != types.BucketNew {
t.Error("Below 4.0 should be New (§9.2)")
}
if types.GetStandingBucket(4.5, 5, false) != types.BucketNew {
t.Error("Below 10 ratings should be New (§9.2)")
}
if types.GetStandingBucket(4.5, 100, true) != types.BucketSlashed {
t.Error("Slashed should be Slashed bucket (§9.2)")
}
}
func TestLockedConstants(t *testing.T) {
if types.FreeholderStashMaturityDays != 90 {
t.Error("Stash maturity should be 90 days (§9.1 LOCKED)")
}
if types.FreeholderMinStandingScore != 4.5 {
t.Error("Min standing score should be 4.5 (§9.1 LOCKED)")
}
if types.FreeholderMinCategories != 3 {
t.Error("Min categories should be 3 (§9.1 LOCKED)")
}
if types.MinCounterpartiesFreeholderStatus != 30 {
t.Error("Min counterparties for Freeholder status should be 30 (§9.2)")
}
}