docs(P01): complete Orgs+Window foundation phase

---ci---
project: oy
phase: 1
milestone: v0.2
status: complete
phase_role: execution
requirements:
  covered: [REQ-015, REQ-016, REQ-017, REQ-012]
  partial: []
---/ci---

Phase 1 (Orgs+Window foundation) complete. 3 new modules (x/window, x/stand, x/guild)
+ lexicon meta-test scaffolding (G-004). 143 tests total (53 v0.1 baseline + 90 new), 100%
coverage on new packages. Window = fullest primitive (lifecycle Open->Active->Revoked->
Expired, rate-limit, append-only audit log). 9-type Stand enum. Guild Hand-Pass @ 0% fee.
G-003 by-ID-string import invariant test green. G-004/G-009 lexicon meta-test + self-test
table green. Tagged v0.1.1.
This commit is contained in:
2026-08-17 21:18:35 +00:00
parent 3e762f648d
commit 93a8a3b311
14 changed files with 2206 additions and 4 deletions
+117
View File
@@ -0,0 +1,117 @@
package types
import (
"encoding/json"
"fmt"
)
const (
ModuleName = "guild"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// HandPassFeeBps is the LOCKED protocol fee for a Hand-Pass: 0 bps (REQ-017).
// A Guild Hand-Pass is always free at the protocol layer. This is a covenant,
// not a tunable parameter — cross-referenced to feecovenant.WaiverHandPassGuild
// (v0.1 already encodes HandPassGuild as a 0-fee waiver reason). v0.2's Guild
// module references that waiver, doesn't redefine the fee.
HandPassFeeBps = 0
)
// Guild is a task-oriented collective (vision §16, REQ-017). A Guild may
// optionally affiliate with a Stand (stand-affiliation-id references x/stand
// by ID string — G-003 by-ID-string invariant). founder-reach references
// x/identity Reach by string.
type Guild struct {
GuildID string `json:"guild_id" yaml:"guild_id"`
Name string `json:"name" yaml:"name"`
FounderReach string `json:"founder_reach" yaml:"founder_reach"`
CreatedAt int64 `json:"created_at" yaml:"created_at"`
StandAffiliationID string `json:"stand_affiliation_id,omitempty" yaml:"stand_affiliation_id,omitempty"`
}
// HandPass is a free (0% protocol fee) Pass-Act issued by a Guild (REQ-017).
// FeeGrain is always 0 (HandPassFeeBps == 0 is the locked const covenant).
// issuer-reach / recipient-reach reference x/identity Reach by string (G-003).
type HandPass struct {
PassID string `json:"pass_id" yaml:"pass_id"`
GuildID string `json:"guild_id" yaml:"guild_id"`
IssuerReach string `json:"issuer_reach" yaml:"issuer_reach"`
RecipientReach string `json:"recipient_reach" yaml:"recipient_reach"`
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
FeeGrain int64 `json:"fee_grain" yaml:"fee_grain"` // always 0 (HandPassFeeBps == 0)
}
// IssueHandPass is a stub for issuing a Hand-Pass (REQ-017). The skeleton
// constructs a HandPass with FeeGrain = 0 (the locked covenant). Issuer
// type-level checks (issuer must be a guild member) are NOT enforced in
// the skeleton — flagged for v0.3 keeper logic.
func IssueHandPass(passID, guildID, issuerReach, recipientReach string, amountGrain int64, timestamp int64) HandPass {
return HandPass{
PassID: passID,
GuildID: guildID,
IssuerReach: issuerReach,
RecipientReach: recipientReach,
AmountGrain: amountGrain,
Timestamp: timestamp,
FeeGrain: 0, // HandPassFeeBps == 0 (locked covenant)
}
}
// Params for the guild module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the guild module genesis state (REQ-017).
// Guilds + HandPasses are the two top-level sets; ValidateGenesis enforces
// guild-id uniqueness and pass-id uniqueness.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Guilds []Guild `json:"guilds" yaml:"guilds"`
HandPasses []HandPass `json:"hand_passes" yaml:"hand_passes"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Guilds: []Guild{},
HandPasses: []HandPass{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate guild-ids and duplicate pass-ids. Also enforces
// the 0-fee covenant on genesis HandPasses (FeeGrain must be 0).
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("guild: invalid genesis: %w", err)
}
seenGuild := make(map[string]bool, len(gs.Guilds))
for _, g := range gs.Guilds {
if g.GuildID == "" {
return fmt.Errorf("guild: empty guild-id")
}
if seenGuild[g.GuildID] {
return fmt.Errorf("guild: duplicate guild-id %q", g.GuildID)
}
seenGuild[g.GuildID] = true
}
seenPass := make(map[string]bool, len(gs.HandPasses))
for _, p := range gs.HandPasses {
if p.PassID == "" {
return fmt.Errorf("guild: empty pass-id")
}
if seenPass[p.PassID] {
return fmt.Errorf("guild: duplicate pass-id %q", p.PassID)
}
seenPass[p.PassID] = true
if p.FeeGrain != 0 {
return fmt.Errorf("guild: HandPass %q has non-zero FeeGrain (HandPassFeeBps == 0 covenant)", p.PassID)
}
}
return nil
}
+264
View File
@@ -0,0 +1,264 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/guild/types"
)
// TestHandPassFeeBpsLockedConst asserts the LOCKED 0-fee covenant (REQ-017).
// A Guild Hand-Pass is always free at the protocol layer. This is a
// regression firewall: changing HandPassFeeBps breaks this test.
func TestHandPassFeeBpsLockedConst(t *testing.T) {
if types.HandPassFeeBps != 0 {
t.Errorf("HandPassFeeBps = %d, expected 0 (REQ-017 LOCKED 0pct covenant)", types.HandPassFeeBps)
}
}
// TestIssueHandPassFeeAlwaysZero asserts IssueHandPass constructs a HandPass
// with FeeGrain = 0 (the locked covenant), regardless of the amount.
func TestIssueHandPassFeeAlwaysZero(t *testing.T) {
hp := types.IssueHandPass("p1", "g1", "reach:issuer", "reach:recipient", 10000, 1234)
if hp.FeeGrain != 0 {
t.Errorf("IssueHandPass FeeGrain = %d, expected 0 (HandPassFeeBps == 0)", hp.FeeGrain)
}
// Even a large amount has zero fee (0% covenant).
hp2 := types.IssueHandPass("p2", "g1", "reach:i", "reach:r", 1_000_000_000, 1234)
if hp2.FeeGrain != 0 {
t.Errorf("IssueHandPass FeeGrain (large amount) = %d, expected 0", hp2.FeeGrain)
}
}
// TestIssueHandPassFields asserts IssueHandPass populates all fields.
func TestIssueHandPassFields(t *testing.T) {
hp := types.IssueHandPass("p1", "g1", "reach:issuer", "reach:recipient", 5000, 1234)
if hp.PassID != "p1" || hp.GuildID != "g1" || hp.IssuerReach != "reach:issuer" ||
hp.RecipientReach != "reach:recipient" || hp.AmountGrain != 5000 ||
hp.Timestamp != 1234 || hp.FeeGrain != 0 {
t.Error("IssueHandPass fields not set correctly")
}
}
// TestHandPassStructFields asserts HandPass carries all required fields.
func TestHandPassStructFields(t *testing.T) {
hp := types.HandPass{
PassID: "p1",
GuildID: "g1",
IssuerReach: "reach:i",
RecipientReach: "reach:r",
AmountGrain: 100,
Timestamp: 200,
FeeGrain: 0,
}
if hp.PassID != "p1" || hp.GuildID != "g1" || hp.AmountGrain != 100 ||
hp.FeeGrain != 0 {
t.Error("HandPass fields not set correctly")
}
}
// TestGuildWithStandAffiliation asserts a Guild can affiliate with a Stand
// (stand-affiliation-id set).
func TestGuildWithStandAffiliation(t *testing.T) {
g := types.Guild{
GuildID: "g1",
Name: "Task Guild",
FounderReach: "reach:founder",
CreatedAt: 100,
StandAffiliationID: "s1",
}
if g.StandAffiliationID != "s1" {
t.Errorf("StandAffiliationID = %q, want %q", g.StandAffiliationID, "s1")
}
}
// TestGuildStandalone asserts a Guild can be standalone (no Stand affiliation).
func TestGuildStandalone(t *testing.T) {
g := types.Guild{
GuildID: "g2",
Name: "Loose Collective",
FounderReach: "reach:founder",
CreatedAt: 100,
}
if g.StandAffiliationID != "" {
t.Errorf("Standalone Guild StandAffiliationID = %q, want empty", g.StandAffiliationID)
}
}
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
// empty slices for Guilds and HandPasses.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Guilds == nil || len(gs.Guilds) != 0 {
t.Errorf("Default Guilds should be non-nil empty slice")
}
if gs.HandPasses == nil || len(gs.HandPasses) != 0 {
t.Errorf("Default HandPasses should be non-nil empty slice")
}
}
// TestValidateGenesisRejectsDupGuildIDs asserts A-212: duplicate guild-ids
// are rejected.
func TestValidateGenesisRejectsDupGuildIDs(t *testing.T) {
gs := types.GenesisState{
Guilds: []types.Guild{
{GuildID: "g1"},
{GuildID: "g1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate guild-ids")
}
}
// TestValidateGenesisRejectsDupPassIDs asserts A-212: duplicate pass-ids
// are rejected.
func TestValidateGenesisRejectsDupPassIDs(t *testing.T) {
gs := types.GenesisState{
HandPasses: []types.HandPass{
{PassID: "p1"},
{PassID: "p1"}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate pass-ids")
}
}
// TestValidateGenesisRejectsNonZeroFeeGrain asserts the 0-fee covenant is
// enforced at genesis: any HandPass with non-zero FeeGrain is rejected.
func TestValidateGenesisRejectsNonZeroFeeGrain(t *testing.T) {
gs := types.GenesisState{
HandPasses: []types.HandPass{
{PassID: "p1", FeeGrain: 1}, // violates 0-fee covenant
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject non-zero FeeGrain (0pct covenant)")
}
}
// TestValidateGenesisRejectsEmptyGuildID asserts empty guild-id is rejected.
func TestValidateGenesisRejectsEmptyGuildID(t *testing.T) {
gs := types.GenesisState{
Guilds: []types.Guild{{GuildID: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty guild-id")
}
}
// TestValidateGenesisRejectsEmptyPassID asserts empty pass-id is rejected.
func TestValidateGenesisRejectsEmptyPassID(t *testing.T) {
gs := types.GenesisState{
HandPasses: []types.HandPass{{PassID: ""}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty pass-id")
}
}
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
if err := types.ValidateGenesis(json.RawMessage(`{bad`)); err == nil {
t.Error("ValidateGenesis should reject malformed JSON")
}
}
// TestValidateGenesisAcceptsClean asserts a clean genesis validates,
// including a Guild with Stand affiliation and a standalone Guild.
func TestValidateGenesisAcceptsClean(t *testing.T) {
gs := types.GenesisState{
Guilds: []types.Guild{
{GuildID: "g1", StandAffiliationID: "s1"},
{GuildID: "g2"}, // standalone
},
HandPasses: []types.HandPass{
{PassID: "p1", GuildID: "g1", FeeGrain: 0},
{PassID: "p2", GuildID: "g2", FeeGrain: 0},
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err != nil {
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
}
}
// TestModuleConsts asserts the four Cosmos-convention module consts.
func TestModuleConsts(t *testing.T) {
if types.ModuleName != "guild" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "guild" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "guild" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "guild" {
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
}
}
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
func TestDefaultParams(t *testing.T) {
_ = types.DefaultParams() // no panics
}
// --- Lexicon assertion (REQ-012) -------------------------------------------------
// TestLexiconNoBannedTermsInGuildPackage scans every non-test .go file in
// the guild/types package directory for the 9 banned terms (case-insensitive).
// Production files only — the test file contains the banned terms as the list
// of things to forbid (standard lexicon-test bootstrapping pattern).
func TestLexiconNoBannedTermsInGuildPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/guild/types")
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
if err != nil {
t.Fatalf("glob: %v", err)
}
prodFiles := []string{}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
prodFiles = append(prodFiles, f)
}
if len(prodFiles) == 0 {
t.Fatal("no production .go files found in guild/types")
}
for _, f := range prodFiles {
bz, err := os.ReadFile(f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
}
}
}
// packageDir resolves a Go import path to its filesystem directory.
func packageDir(t *testing.T, importPath string) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}