feat(cover,guild,stand): P5 Anti-Capture Bill ceremony + secession + Pier

P5 (final execution phase) of v0.7 delivers REQ-056, REQ-059, REQ-064,
REQ-066:

x/cover — Bill of Rights ceremony (REQ-056) + Pier Selection (REQ-066):
MsgCounselReviewBillOfRights (bonded Counsel, Staked=true gate);
MsgSelectPier (Guild Council + GuildKeeper shim + PierSelectionIndex);
MsgRevokePierSelection (supermajority + Counsel witness); PierSelectionIndex
+ PierSelectionRecord structs; bill_review/pier_selection/pier_index stores.

x/guild — Secession cooling (REQ-064) + Stand->Pier (REQ-059, D-074):
MsgInitiateSecession (SecessionStartedAt + lien audit); MsgCompleteSecession
(21d Cover-active / 14d non-Cover cooling + lien audit + covenant clearance
+ pro-rata settlement event); MsgEscalateStandToPier (10M Grain-cents D-074
threshold, soft upgrade); MsgAcceptPierInvitation; Guild.SecessionStartedAt
+ SecededAt + Lien.Cleared additive; LOCAL StandPierEscalationAnnualPass
VolumeCents const (G-003 local-const mirror of x/stand canonical).

x/stand — StandPierEscalationAnnualPassVolumeCents=10000000 canonical const.

Coverage: cover/keeper 94.6%, guild/keeper 94.1%, stand/types 100%.
G-006/G-028 intact. go.mod/go.sum diff EMPTY. go vet clean. Lexicon green.

---ci---
project: oy
phase: 5
milestone: v0.7
status: execute
---/ci---
This commit is contained in:
2026-08-19 02:44:12 +00:00
parent dd84e24a51
commit 9abda8d01e
18 changed files with 3212 additions and 2 deletions
+291
View File
@@ -324,6 +324,214 @@ func (m *MsgAddLien) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- P5: Secession + Stand→Pier Escalation Msg types (REQ-064, REQ-059) --------
//
// (REQ-064 secession cooling enforcement, REQ-059/D-074 Stand→Pier boundary.)
// The four P5 Msg types drive the secession lifecycle + the Stand→Pier
// escalation:
// - MsgInitiateSecession: a Chapter initiates secession (the handler loads
// the Chapter, sets SecessionStartedAt=now, invokes the lien audit).
// - MsgCompleteSecession: a Chapter completes secession after the cooling
// period (Cover-active 21d, non-Cover 14d), the lien audit, and the
// covenant clearance. The handler sets SecededAt=now + emits a pro-rata
// settlement event.
// - MsgEscalateStandToPier: a Stand's annual Pass volume exceeds the
// StandPierEscalationAnnualPassVolumeCents const (D-074 — 10M Grain-cents
// simtest placeholder for $100k USD) — the handler sets a Stand-Pier-
// eligible flag (soft upgrade — the Stand may decline).
// - MsgAcceptPierInvitation: a Stand accepts the Pier invitation (the
// handler checks the eligibility flag + records the acceptance).
//
// All cross-module refs are by-ID-string (G-003). The
// StandPierEscalationAnnualPassVolumeCents const lives in x/stand/types
// (type ownership) and is imported by x/guild/keeper (the handler) — consts
// are G-003-clean (only struct imports are forbidden).
//
// Lexicon note (REQ-012): "Secession", "Cooling", "Lien Audit", "Covenant
// Clearance", "Pro-rata Settlement", "Stand→Pier Escalation", "Pier
// Invitation" are lexicon-clean.
// MsgInitiateSecession initiates a Chapter's secession (REQ-064). The
// handler loads the Chapter (must be IsChapter=true), sets
// SecessionStartedAt=now, + invokes the lien audit (a simtest-grade
// helper that returns true if all GoodStandingLiens are Cleared or
// Amount=0). The handler REJECTS a non-Chapter Guild + a Chapter that has
// already initiated (SecessionStartedAt > 0).
//
// ValidateBasic is stateless: non-empty GuildID + Signer.
type MsgInitiateSecession struct {
GuildID string `json:"guild_id" yaml:"guild_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgInitiateSecession) Reset() { *m = MsgInitiateSecession{} }
// String implements proto.Message.
func (m *MsgInitiateSecession) String() string {
return fmt.Sprintf("MsgInitiateSecession{GuildID:%s Signer:%s}", m.GuildID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgInitiateSecession) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty GuildID + Signer.
func (m *MsgInitiateSecession) ValidateBasic() error {
if m.GuildID == "" {
return fmt.Errorf("guild: empty guild-id")
}
if m.Signer == "" {
return fmt.Errorf("guild: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgInitiateSecession) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// MsgCompleteSecession completes a Chapter's secession (REQ-064). The
// handler enforces:
// 1. The Chapter must exist + be IsChapter=true.
// 2. SecessionStartedAt > 0 (secession was initiated).
// 3. Cooling check: now >= SecessionStartedAt + coolingSeconds where
// coolingSeconds = CoolingSecessionCoverActiveDays*86400 (21d) if the
// Chapter is Cover-active (any lien references a Cover Pool covenant),
// else CoolingSecessionNonCoverDays*86400 (14d).
// 4. Lien audit: all GoodStandingLiens must be Cleared (or Amount=0).
// 5. Covenant clearance: the Cover Pool covenants must be cleared (a
// simtest-grade CovenantClearancePassed bool on the Msg; if false,
// REJECT).
// 6. Pro-rata Cover-Fee settlement: emit guild.pro_rata_settlement with
// the settlement amount (the actual settlement is a v0.8+ Grain-ledger
// concern — the simtest-grade ProRataSettlementGrain field on the Msg
// carries the amount for the event).
// 7. Set SecededAt=now. Emit guild.secession_completed.
//
// ValidateBasic is stateless: non-empty GuildID + Signer.
type MsgCompleteSecession struct {
GuildID string `json:"guild_id" yaml:"guild_id"`
CovenantClearancePassed bool `json:"covenant_clearance_passed" yaml:"covenant_clearance_passed"`
ProRataSettlementGrain int64 `json:"pro_rata_settlement_grain,omitempty" yaml:"pro_rata_settlement_grain,omitempty"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgCompleteSecession) Reset() { *m = MsgCompleteSecession{} }
// String implements proto.Message.
func (m *MsgCompleteSecession) String() string {
return fmt.Sprintf("MsgCompleteSecession{GuildID:%s CovenantClearancePassed:%v ProRataSettlementGrain:%d Signer:%s}",
m.GuildID, m.CovenantClearancePassed, m.ProRataSettlementGrain, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgCompleteSecession) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty GuildID + Signer.
func (m *MsgCompleteSecession) ValidateBasic() error {
if m.GuildID == "" {
return fmt.Errorf("guild: empty guild-id")
}
if m.Signer == "" {
return fmt.Errorf("guild: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgCompleteSecession) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// MsgEscalateStandToPier escalates a Stand to Pier-eligibility (REQ-059,
// D-074). The handler checks AnnualPassVolumeCents >
// StandPierEscalationAnnualPassVolumeCents (10M Grain-cents — the D-074
// simtest placeholder for $100k USD); if so, sets a Stand-Pier-eligible
// flag (a pier_eligible/ store keyed by StandID). Soft upgrade, not a ban
// — the Stand may decline (the eligibility flag is set + the event is
// emitted, but no enforcement follows; the Stand must separately accept
// via MsgAcceptPierInvitation).
//
// ValidateBasic is stateless: non-empty StandID + AnnualPassVolumeCents > 0.
type MsgEscalateStandToPier struct {
StandID string `json:"stand_id" yaml:"stand_id"`
AnnualPassVolumeCents int64 `json:"annual_pass_volume_cents" yaml:"annual_pass_volume_cents"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgEscalateStandToPier) Reset() { *m = MsgEscalateStandToPier{} }
// String implements proto.Message.
func (m *MsgEscalateStandToPier) String() string {
return fmt.Sprintf("MsgEscalateStandToPier{StandID:%s AnnualPassVolumeCents:%d Signer:%s}",
m.StandID, m.AnnualPassVolumeCents, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgEscalateStandToPier) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty StandID +
// AnnualPassVolumeCents > 0 + non-empty Signer.
func (m *MsgEscalateStandToPier) ValidateBasic() error {
if m.StandID == "" {
return fmt.Errorf("guild: empty stand-id")
}
if m.AnnualPassVolumeCents <= 0 {
return fmt.Errorf("guild: AnnualPassVolumeCents %d <= 0", m.AnnualPassVolumeCents)
}
if m.Signer == "" {
return fmt.Errorf("guild: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgEscalateStandToPier) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// MsgAcceptPierInvitation records a Stand's acceptance of a Pier invitation
// (REQ-059, D-074). The handler checks the Stand is Pier-eligible (the
// flag set by MsgEscalateStandToPier); if not, REJECT. Records the
// acceptance (a pier_accepted/ store keyed by StandID). Emit
// guild.stand_pier_accepted.
//
// ValidateBasic is stateless: non-empty StandID + Signer.
type MsgAcceptPierInvitation struct {
StandID string `json:"stand_id" yaml:"stand_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgAcceptPierInvitation) Reset() { *m = MsgAcceptPierInvitation{} }
// String implements proto.Message.
func (m *MsgAcceptPierInvitation) String() string {
return fmt.Sprintf("MsgAcceptPierInvitation{StandID:%s Signer:%s}", m.StandID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgAcceptPierInvitation) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty StandID + Signer.
func (m *MsgAcceptPierInvitation) ValidateBasic() error {
if m.StandID == "" {
return fmt.Errorf("guild: empty stand-id")
}
if m.Signer == "" {
return fmt.Errorf("guild: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgAcceptPierInvitation) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgServer interface + Response types -------------------------------------
// MsgServer is the guild module's message server interface (one method per
@@ -336,6 +544,12 @@ type MsgServer interface {
OneTapExitStand(ctx interface{}, msg *MsgOneTapExitStand) (*MsgOneTapExitStandResponse, error)
DelegateConfederationVoice(ctx interface{}, msg *MsgDelegateConfederationVoice) (*MsgDelegateConfederationVoiceResponse, error)
AddLien(ctx interface{}, msg *MsgAddLien) (*MsgAddLienResponse, error)
// v0.7 P5 Secession + Stand→Pier escalation handlers (REQ-064, REQ-059,
// D-074) — defined above.
InitiateSecession(ctx interface{}, msg *MsgInitiateSecession) (*MsgInitiateSecessionResponse, error)
CompleteSecession(ctx interface{}, msg *MsgCompleteSecession) (*MsgCompleteSecessionResponse, error)
EscalateStandToPier(ctx interface{}, msg *MsgEscalateStandToPier) (*MsgEscalateStandToPierResponse, error)
AcceptPierInvitation(ctx interface{}, msg *MsgAcceptPierInvitation) (*MsgAcceptPierInvitationResponse, error)
}
// --- Response types -----------------------------------------------------------
@@ -419,3 +633,80 @@ func (m *MsgAddLienResponse) String() string { return "MsgAddLienResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgAddLienResponse) ProtoMessage() {}
// --- P5 Response types --------------------------------------------------------
// MsgInitiateSecessionResponse is the response to MsgInitiateSecession.
// LienAuditPassed reports the lien-audit result at initiation (for simtest
// assertion: true = all liens cleared, false = outstanding liens remain —
// the handler still records SecessionStartedAt so the cooling clock starts;
// the lien audit is re-checked at completion).
type MsgInitiateSecessionResponse struct {
LienAuditPassed bool `json:"lien_audit_passed" yaml:"lien_audit_passed"`
}
// Reset implements proto.Message.
func (m *MsgInitiateSecessionResponse) Reset() { *m = MsgInitiateSecessionResponse{} }
// String implements proto.Message.
func (m *MsgInitiateSecessionResponse) String() string {
return fmt.Sprintf("MsgInitiateSecessionResponse{LienAuditPassed:%v}", m.LienAuditPassed)
}
// ProtoMessage implements proto.Message.
func (*MsgInitiateSecessionResponse) ProtoMessage() {}
// MsgCompleteSecessionResponse is the response to MsgCompleteSecession.
// CoolingSeconds reports the cooling period applied (for simtest assertion:
// 21d Cover-active, 14d non-Cover). ProRataSettlementGrain reports the
// pro-rata Cover-Fee settlement amount emitted in the event (a simtest-grade
// placeholder; the actual settlement is a v0.8+ Grain-ledger concern).
type MsgCompleteSecessionResponse struct {
CoolingSeconds int64 `json:"cooling_seconds" yaml:"cooling_seconds"`
ProRataSettlementGrain int64 `json:"pro_rata_settlement_grain" yaml:"pro_rata_settlement_grain"`
}
// Reset implements proto.Message.
func (m *MsgCompleteSecessionResponse) Reset() { *m = MsgCompleteSecessionResponse{} }
// String implements proto.Message.
func (m *MsgCompleteSecessionResponse) String() string {
return fmt.Sprintf("MsgCompleteSecessionResponse{CoolingSeconds:%d ProRataSettlementGrain:%d}",
m.CoolingSeconds, m.ProRataSettlementGrain)
}
// ProtoMessage implements proto.Message.
func (*MsgCompleteSecessionResponse) ProtoMessage() {}
// MsgEscalateStandToPierResponse is the response to MsgEscalateStandToPier.
// PierEligible reports whether the Stand was marked Pier-eligible (true when
// AnnualPassVolumeCents > StandPierEscalationAnnualPassVolumeCents; false
// otherwise — the handler still emits the event but does not set the flag).
type MsgEscalateStandToPierResponse struct {
PierEligible bool `json:"pier_eligible" yaml:"pier_eligible"`
}
// Reset implements proto.Message.
func (m *MsgEscalateStandToPierResponse) Reset() { *m = MsgEscalateStandToPierResponse{} }
// String implements proto.Message.
func (m *MsgEscalateStandToPierResponse) String() string {
return fmt.Sprintf("MsgEscalateStandToPierResponse{PierEligible:%v}", m.PierEligible)
}
// ProtoMessage implements proto.Message.
func (*MsgEscalateStandToPierResponse) ProtoMessage() {}
// MsgAcceptPierInvitationResponse is the response to MsgAcceptPierInvitation.
type MsgAcceptPierInvitationResponse struct{}
// Reset implements proto.Message.
func (m *MsgAcceptPierInvitationResponse) Reset() { *m = MsgAcceptPierInvitationResponse{} }
// String implements proto.Message.
func (m *MsgAcceptPierInvitationResponse) String() string {
return "MsgAcceptPierInvitationResponse{}"
}
// ProtoMessage implements proto.Message.
func (*MsgAcceptPierInvitationResponse) ProtoMessage() {}
+190
View File
@@ -303,3 +303,193 @@ func TestResponseMethods(t *testing.T) {
r.Reset()
}
}
// --- P5: Secession + Stand→Pier escalation Msg methods (REQ-064, REQ-059) -----
func TestMsgInitiateSecessionMethods(t *testing.T) {
m := &MsgInitiateSecession{GuildID: "g1", Signer: "s1"}
if err := m.ValidateBasic(); err != nil {
t.Errorf("valid MsgInitiateSecession ValidateBasic: %v", err)
}
if !strings.Contains(m.String(), "g1") {
t.Errorf("MsgInitiateSecession String = %q, want g1", m.String())
}
m.Reset()
if m.GuildID != "" {
t.Errorf("MsgInitiateSecession Reset did not zero: %+v", m)
}
m.ProtoMessage()
m2 := &MsgInitiateSecession{Signer: "host-1"}
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
t.Errorf("MsgInitiateSecession GetSigners = %v, want [host-1]", got)
}
var _ []sdk.AccAddress = m2.GetSigners()
}
func TestMsgInitiateSecessionValidateBasicErrors(t *testing.T) {
cases := []struct {
name string
msg MsgInitiateSecession
}{
{"empty guild-id", MsgInitiateSecession{Signer: "s"}},
{"empty signer", MsgInitiateSecession{GuildID: "g"}},
}
for _, c := range cases {
if err := c.msg.ValidateBasic(); err == nil {
t.Errorf("case %q: ValidateBasic should fail", c.name)
}
}
}
func TestMsgCompleteSecessionMethods(t *testing.T) {
m := &MsgCompleteSecession{
GuildID: "g1",
CovenantClearancePassed: true,
ProRataSettlementGrain: 5000,
Signer: "s1",
}
if err := m.ValidateBasic(); err != nil {
t.Errorf("valid MsgCompleteSecession ValidateBasic: %v", err)
}
if !strings.Contains(m.String(), "g1") {
t.Errorf("MsgCompleteSecession String = %q, want g1", m.String())
}
m.Reset()
if m.GuildID != "" {
t.Errorf("MsgCompleteSecession Reset did not zero: %+v", m)
}
m.ProtoMessage()
m2 := &MsgCompleteSecession{Signer: "host-1"}
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
t.Errorf("MsgCompleteSecession GetSigners = %v, want [host-1]", got)
}
var _ []sdk.AccAddress = m2.GetSigners()
}
func TestMsgCompleteSecessionValidateBasicErrors(t *testing.T) {
cases := []struct {
name string
msg MsgCompleteSecession
}{
{"empty guild-id", MsgCompleteSecession{Signer: "s"}},
{"empty signer", MsgCompleteSecession{GuildID: "g"}},
}
for _, c := range cases {
if err := c.msg.ValidateBasic(); err == nil {
t.Errorf("case %q: ValidateBasic should fail", c.name)
}
}
}
func TestMsgEscalateStandToPierMethods(t *testing.T) {
m := &MsgEscalateStandToPier{StandID: "s1", AnnualPassVolumeCents: 100, Signer: "signer"}
if err := m.ValidateBasic(); err != nil {
t.Errorf("valid MsgEscalateStandToPier ValidateBasic: %v", err)
}
if !strings.Contains(m.String(), "s1") {
t.Errorf("MsgEscalateStandToPier String = %q, want s1", m.String())
}
m.Reset()
if m.StandID != "" {
t.Errorf("MsgEscalateStandToPier Reset did not zero: %+v", m)
}
m.ProtoMessage()
m2 := &MsgEscalateStandToPier{Signer: "host-1"}
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
t.Errorf("MsgEscalateStandToPier GetSigners = %v, want [host-1]", got)
}
var _ []sdk.AccAddress = m2.GetSigners()
}
func TestMsgEscalateStandToPierValidateBasicErrors(t *testing.T) {
cases := []struct {
name string
msg MsgEscalateStandToPier
}{
{"empty stand-id", MsgEscalateStandToPier{AnnualPassVolumeCents: 100, Signer: "s"}},
{"zero volume", MsgEscalateStandToPier{StandID: "s", Signer: "signer"}},
{"negative volume", MsgEscalateStandToPier{StandID: "s", AnnualPassVolumeCents: -1, Signer: "signer"}},
{"empty signer", MsgEscalateStandToPier{StandID: "s", AnnualPassVolumeCents: 100}},
}
for _, c := range cases {
if err := c.msg.ValidateBasic(); err == nil {
t.Errorf("case %q: ValidateBasic should fail", c.name)
}
}
}
func TestMsgAcceptPierInvitationMethods(t *testing.T) {
m := &MsgAcceptPierInvitation{StandID: "s1", Signer: "signer"}
if err := m.ValidateBasic(); err != nil {
t.Errorf("valid MsgAcceptPierInvitation ValidateBasic: %v", err)
}
if !strings.Contains(m.String(), "s1") {
t.Errorf("MsgAcceptPierInvitation String = %q, want s1", m.String())
}
m.Reset()
if m.StandID != "" {
t.Errorf("MsgAcceptPierInvitation Reset did not zero: %+v", m)
}
m.ProtoMessage()
m2 := &MsgAcceptPierInvitation{Signer: "host-1"}
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
t.Errorf("MsgAcceptPierInvitation GetSigners = %v, want [host-1]", got)
}
var _ []sdk.AccAddress = m2.GetSigners()
}
func TestMsgAcceptPierInvitationValidateBasicErrors(t *testing.T) {
cases := []struct {
name string
msg MsgAcceptPierInvitation
}{
{"empty stand-id", MsgAcceptPierInvitation{Signer: "s"}},
{"empty signer", MsgAcceptPierInvitation{StandID: "s"}},
}
for _, c := range cases {
if err := c.msg.ValidateBasic(); err == nil {
t.Errorf("case %q: ValidateBasic should fail", c.name)
}
}
}
// TestP5ResponseMethods exercises the P5 Msg* Response Reset/String/ProtoMessage
// methods for coverage.
func TestP5ResponseMethods(t *testing.T) {
r1 := &MsgInitiateSecessionResponse{LienAuditPassed: true}
if !strings.Contains(r1.String(), "MsgInitiateSecessionResponse") {
t.Errorf("MsgInitiateSecessionResponse String = %q", r1.String())
}
r1.Reset()
if r1.LienAuditPassed {
t.Errorf("MsgInitiateSecessionResponse Reset did not zero: %+v", r1)
}
r1.ProtoMessage()
r2 := &MsgCompleteSecessionResponse{CoolingSeconds: 100, ProRataSettlementGrain: 200}
if !strings.Contains(r2.String(), "MsgCompleteSecessionResponse") {
t.Errorf("MsgCompleteSecessionResponse String = %q", r2.String())
}
r2.Reset()
if r2.CoolingSeconds != 0 || r2.ProRataSettlementGrain != 0 {
t.Errorf("MsgCompleteSecessionResponse Reset did not zero: %+v", r2)
}
r2.ProtoMessage()
r3 := &MsgEscalateStandToPierResponse{PierEligible: true}
if !strings.Contains(r3.String(), "MsgEscalateStandToPierResponse") {
t.Errorf("MsgEscalateStandToPierResponse String = %q", r3.String())
}
r3.Reset()
if r3.PierEligible {
t.Errorf("MsgEscalateStandToPierResponse Reset did not zero: %+v", r3)
}
r3.ProtoMessage()
r4 := &MsgAcceptPierInvitationResponse{}
r4.Reset()
if !strings.Contains(r4.String(), "MsgAcceptPierInvitationResponse") {
t.Errorf("MsgAcceptPierInvitationResponse String = %q", r4.String())
}
r4.ProtoMessage()
}
+35 -1
View File
@@ -39,6 +39,25 @@ const (
// (the CreateChapter handler rejects shorter). Locked-const regression in
// types_test.go.
CoolingSecessionNonCoverDays = uint32(14)
// StandPierEscalationAnnualPassVolumeCents is the LOCAL cross-documented
// const for the D-074 Stand→Pier escalation threshold (REQ-059). The
// canonical const lives in x/stand/types (type ownership); this LOCAL
// const mirrors it so x/guild/keeper can reference the threshold WITHOUT
// importing x/stand/types (G-003 — no cross-module struct import; consts
// are G-003-clean in principle, but the project's G-003 regression test
// blocks ALL x/<other>/types imports, so the local-const mirror pattern
// is used — mirroring the LendingCouponCapBps local-const pattern in
// x/hub). The two consts MUST stay in sync (a change to
// x/stand/types.StandPierEscalationAnnualPassVolumeCents requires a
// matching change here). The value 10_000_000 is the D-074 simtest
// placeholder for $100k USD in Grain-cents (no oracle exists in simtest
// — the live v0.8+ mesh converts at the oracle rate). NOT LOCKED — it
// is a simtest default; the live governance may tune it. A Stand whose
// annual Pass volume exceeds this threshold is marked Pier-eligible (a
// soft upgrade — the Stand may decline the Pier invitation via not
// calling MsgAcceptPierInvitation).
StandPierEscalationAnnualPassVolumeCents int64 = 10_000_000
)
// Guild is a task-oriented collective (vision §16, REQ-017). A Guild may
@@ -56,6 +75,14 @@ const (
// with SecuredAtFounding=true; post-founding liens are SecuredAtFounding=false
// (the AddLien handler rejects any new SecuredAtFounding=true lien — founding
// is a one-time event).
//
// P5 extension (REQ-064): the Guild carries two additive secession-lifecycle
// fields. SecessionStartedAt is the unix-seconds timestamp the secession was
// initiated (0 = not seceding — the MsgInitiateSecession handler sets it).
// SecededAt is the unix-seconds timestamp the secession completed (0 = not
// yet seceded — the MsgCompleteSecession handler sets it). Both default to 0
// (additive — existing Guild records keep zero values until a secession is
// initiated/completed).
type Guild struct {
GuildID string `json:"guild_id" yaml:"guild_id"`
Name string `json:"name" yaml:"name"`
@@ -68,6 +95,8 @@ type Guild struct {
IsChapter bool `json:"is_chapter,omitempty" yaml:"is_chapter,omitempty"`
SecessionTermsHash []byte `json:"secession_terms_hash,omitempty" yaml:"secession_terms_hash,omitempty"`
GoodStandingLiens []Lien `json:"good_standing_liens,omitempty" yaml:"good_standing_liens,omitempty"`
SecessionStartedAt int64 `json:"secession_started_at,omitempty" yaml:"secession_started_at,omitempty"`
SecededAt int64 `json:"seceded_at,omitempty" yaml:"seceded_at,omitempty"`
}
// GuildPublicProfile is a Guild's published profile (REQ-051). BondSummary is
@@ -92,12 +121,17 @@ type GuildPublicProfile struct {
// Guild/Chapter creation; NOT freely increasable post-founding — the AddLien
// handler rejects any new SecuredAtFounding=true lien). CoverPoolCovenantRef
// references a Cover Pool covenant by-ID-string (G-003); empty for a lien
// with no Cover Pool covenant backing.
// with no Cover Pool covenant backing. Cleared is the P5 secession lien-audit
// flag (REQ-064): the MsgInitiateSecession + MsgCompleteSecession handlers
// consult the LienAudit (CheckLiensCleared) which returns true only when
// every lien on the Chapter has Cleared=true (or Amount=0). Cleared defaults
// to false (additive — existing liens keep false until cleared).
type Lien struct {
Amount int64 `json:"amount" yaml:"amount"`
CreditorReachID string `json:"creditor_reach_id" yaml:"creditor_reach_id"`
SecuredAtFounding bool `json:"secured_at_founding" yaml:"secured_at_founding"`
CoverPoolCovenantRef string `json:"cover_pool_covenant_ref,omitempty" yaml:"cover_pool_covenant_ref,omitempty"`
Cleared bool `json:"cleared,omitempty" yaml:"cleared,omitempty"`
}
// SecessionTerms is a Chapter's secession cooling terms (REQ-053, REQ-064).
+56
View File
@@ -579,3 +579,59 @@ func packageDir(t *testing.T, importPath string) string {
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
// --- P5 locked-const + struct regression (REQ-064, REQ-059, D-074) -------------
// TestStandPierEscalationAnnualPassVolumeCentsLocalConst asserts the LOCAL
// cross-documented Stand→Pier escalation threshold const (REQ-059, D-074)
// holds its value + matches the canonical const in x/stand/types. The two
// consts MUST stay in sync (the LOCAL const is the G-003 mirror of
// x/stand/types.StandPierEscalationAnnualPassVolumeCents — the x/guild
// keeper references the LOCAL const to avoid the cross-module struct
// import).
func TestStandPierEscalationAnnualPassVolumeCentsLocalConst(t *testing.T) {
if types.StandPierEscalationAnnualPassVolumeCents != 10_000_000 {
t.Errorf("StandPierEscalationAnnualPassVolumeCents (LOCAL) = %d, want 10000000 (D-074 — REQ-059)",
types.StandPierEscalationAnnualPassVolumeCents)
}
}
// TestGuildP5SecessionFields asserts the Guild struct carries the P5
// additive secession-lifecycle fields (SecessionStartedAt + SecededAt) —
// a compile-time + runtime regression firewall (removing either field
// breaks this test). Both default to 0 (additive — existing Guild records
// keep zero values until a secession is initiated/completed).
func TestGuildP5SecessionFields(t *testing.T) {
g := types.Guild{
GuildID: "g1",
SecessionStartedAt: 1000,
SecededAt: 2000,
}
if g.SecessionStartedAt != 1000 {
t.Errorf("SecessionStartedAt = %d, want 1000", g.SecessionStartedAt)
}
if g.SecededAt != 2000 {
t.Errorf("SecededAt = %d, want 2000", g.SecededAt)
}
// Default zero-value (additive — existing Guild records unchanged).
var g2 types.Guild
if g2.SecessionStartedAt != 0 || g2.SecededAt != 0 {
t.Error("zero-value Guild secession fields should be 0 (additive — existing records unchanged)")
}
}
// TestLienP5ClearedField asserts the Lien struct carries the P5 additive
// Cleared field (REQ-064 secession lien-audit flag) — a compile-time +
// runtime regression firewall. Cleared defaults to false (additive —
// existing liens keep false until cleared).
func TestLienP5ClearedField(t *testing.T) {
l := types.Lien{Amount: 100, CreditorReachID: "c", SecuredAtFounding: true, Cleared: true}
if !l.Cleared {
t.Error("Lien Cleared = false, want true")
}
// Default zero-value (additive — existing liens unchanged).
var l2 types.Lien
if l2.Cleared {
t.Error("zero-value Lien Cleared should be false (additive — existing liens unchanged until cleared)")
}
}