Compare commits

...

8 Commits

Author SHA1 Message Date
cloudinit-bot a6d33a58b3 verify(P2): lifecycle divergence docs + regression guard — 4 layers green
Structural: go vet clean. Behavioral: TestSignalKindShapeIntentional PASS;
existing council tests PASS. Security: no locked-const change
(SignalKindCount==4 preserved). Quality: x/council/types coverage 96.4%
(improved from v0.3; new intent test adds coverage). NFR purity: zero
feat: commits in P2 (only docs + test).

---ci---
project: oy
phase: 2
milestone: v0.4
status: verify
tag_base: v0.3.x
milestone_type: nfr
reqs: [REQ-031]
---/ci---
2026-08-17 23:32:02 +00:00
cloudinit-bot 7d1468b442 docs(arch): council lifecycle divergence decisions (REQ-031, AUDIT §193)
Add ARCHITECTURE.md §"Council Voice/Council Interface — Lifecycle Type
Divergence Decisions (v0.4)": documents P1-1 (Proposal/VoteOption absent,
deferred to v0.5+ governance runtime — feat: rejected by D-001), P1-2
(SignalKind 4-vs-5: the 4-source shape is intentional per AUDIT rationale;
Freeholder is eligibility, Guild is council tier, Capital is committed-
capital per vision §9.1), P2 (bearers ValidateGenesis no-op correct per spec).

test(council): TestSignalKindShapeIntentional regression guard (REQ-031)

Add intent-assertion test locking the 4-source SignalKind shape with the
AUDIT §193 P1-2 rationale in the doc comment. A future agent changing
SignalKindCount 4→5 must update this test, surfacing the rationale. No
locked-const change, no production .go files modified (D-050).

Verification: go test ./... green; only types_test.go modified in x/**.

---ci---
project: oy
phase: 2
milestone: v0.4
status: execute
tag_base: v0.3.x
milestone_type: nfr
reqs: [REQ-031]
---/ci---
2026-08-17 23:31:55 +00:00
cloudinit-bot 92966cb9c5 checkpoint(p1): v0.4 phase 1 complete → v0.3.1
P1 shipped: tag v0.3.1, release id 749. REQ-029, REQ-030 covered.
Branch oy/phase/01 deleted. Next: P2 lifecycle divergence docs+guard.

---ci---
project: oy
phase: 1
milestone: v0.4
status: complete
tag_base: v0.3.x
milestone_type: nfr
phase_release_tag: v0.3.1
---/ci---
2026-08-17 23:31:16 +00:00
cloudinit-bot 838bd06a9d Merge phase/01 into milestone/v0.4-refinement (P1 complete → v0.3.1)
---ci---
project: oy
phase: 1
milestone: v0.4
status: complete
tag_base: v0.3.x
milestone_type: nfr
---/ci---
2026-08-17 23:30:50 +00:00
cloudinit-bot 57c7dc5ff5 verify(P1): lexicon+const hardening — 4 layers green
Structural: go build ./... + go vet clean. Behavioral: both meta-tests
PASS consuming SyntheticBannedStrings(); 3 new cross-const tests PASS.
Security: lexicon firewalls green (x/ + docs/); G-003 production firewall
intact (no production import of x/bond/types in x/hub/types). Quality:
x/hub/types coverage 93.3% (v0.3 floor preserved; new test adds coverage).

---ci---
project: oy
phase: 1
milestone: v0.4
status: verify
tag_base: v0.3.x
milestone_type: nfr
reqs: [REQ-029, REQ-030]
---/ci---
2026-08-17 23:30:46 +00:00
cloudinit-bot 2ca0e1aa4b refactor(lexicon): shared SyntheticBannedStrings() helper (REQ-029, G-014)
Add lexicon.SyntheticBannedStrings() — single source for the synthetic
self-test table consumed by BOTH meta-tests. Refactor
TestLexiconMetaSelfTestTable (lexicon_meta) and TestLexiconMetaDocsSelfTestTable
(lexicon_meta_docs) to consume the helper; remove the byte-identical
duplicated 10-string table from both. Closes the G-014 drift risk.

test(hub): cross-package const-equality test (REQ-030, A-304, G-015)

Add x/hub/types/cross_const_test.go: test-only import of x/bond/types
(G-003 test-exempt). TestLendingCouponCapMatchesBondCap + TestLendingCoupon-
FloorMatchesBondFloor assert cross-equality; TestConstsAreMissionLocked800And0
(G-015) asserts absolute 800/0 values — catches paired drift. Closes A-304.

Verification: go test ./... green; old synthetic table gone (grep 0);
go.mod unchanged; G-003 production firewall intact.

---ci---
project: oy
phase: 1
milestone: v0.4
status: execute
tag_base: v0.3.x
milestone_type: nfr
reqs: [REQ-029, REQ-030]
---/ci---
2026-08-17 23:30:30 +00:00
cloudinit-bot cc0940d9f8 checkpoint(p0): v0.4 phase 0 complete → v0.3.0
Phase 0 shipped: tag v0.3.0, release id 748. Branch oy/phase/00 deleted.
Next: P1 lexicon+const hardening (oy/phase/01-lexicon-const-hardening).

---ci---
project: oy
phase: 0
milestone: v0.4
status: complete
tag_base: v0.3.x
milestone_type: nfr
phase_release_tag: v0.3.0
release_id: 748
---/ci---
2026-08-17 23:29:20 +00:00
cloudinit-bot 0bb14bd1a1 Merge phase/00 into milestone/v0.4-refinement (P0 complete → v0.3.0)
Phase 0 (pre-execution) complete. Tags v0.3.0. Next: P1 lexicon+const
hardening (oy/phase/01-lexicon-const-hardening).

---ci---
project: oy
phase: 0
milestone: v0.4
status: complete
tag_base: v0.3.x
milestone_type: nfr
---/ci---
2026-08-17 23:28:49 +00:00
7 changed files with 220 additions and 55 deletions
+7 -4
View File
@@ -1,11 +1,14 @@
{
"phase": 0,
"stage": "mvp_ux_check",
"phase": 1,
"stage": "complete",
"milestone": "v0.4",
"milestone_type": "nfr",
"tag_base": "v0.3.x",
"phase_role": "pre_execution",
"phase_role": "execution",
"project": "oy",
"attempts": 0,
"updated_at": "2026-08-17T22:50:00Z"
"updated_at": "2026-08-17T23:35:00Z",
"phase_release_tag": "v0.3.1",
"release_id": 749,
"reqs_covered": ["REQ-029", "REQ-030"]
}
+33 -1
View File
@@ -227,4 +227,36 @@ v0.4 touches NO new `x/*` modules. The touched files are:
### v0.4 Interface Contracts (unchanged from v0.3)
v0.4 does not change any cross-component interface. The 6 cross-component interfaces (Standing, Forge/Fold, Mirror, Window, Fee Covenant, Voice/Council) are unchanged. REQ-031 documents a divergence in the Voice/Council interface surface (SignalKind shape) but does not change it.
v0.4 does not change any cross-component interface. The 6 cross-component interfaces (Standing, Forge/Fold, Mirror, Window, Fee Covenant, Voice/Council) are unchanged. REQ-031 documents a divergence in the Voice/Council interface surface (SignalKind shape) but does not change it.
### Council Voice/Council Interface — Lifecycle Type Divergence Decisions (v0.4, REQ-031)
This section documents the lifecycle type shape-divergences flagged by AUDIT.md §193 for the Council/Voice interface surface. v0.4 is a refinement-only NFR milestone (D-047): the D-001 filter REJECTS `feat:`-class enum additions and locked-const shape changes, so these divergences are DOCUMENTED here, not fixed in code. A regression-guard test (`TestSignalKindShapeIntentional` in `x/council/types/types_test.go`) locks the current shape so a future agent does not silently "fix" a divergence by changing a locked const.
**Divergence P1-1 (AUDIT §193): `Proposal`/`ProposalStatus`/`VoteOption` enums absent from `x/council/types`.**
- **Spec source**: P3-01-01 deliverable recommended `Proposal`, `ProposalStatus` (5 states), `VoteOption` (3 options) enums mirroring OZ Governor / `x/gov`.
- **Implemented**: `Council`, `CouncilMember`, `Voice`, `SignalKind`, `TallyResult` — no `Proposal`/`ProposalStatus`/`VoteOption` lifecycle types.
- **Must-have impact**: NONE. The v0.2 P3 must-haves (3 councils, Mission Lock, `TallyResult` x/gov shape, no veto) are all met without the Proposal lifecycle.
- **Decision (v0.4, D-050)**: ADDING `Proposal`/`ProposalStatus`/`VoteOption` is a `feat:`-class addition (new enum types). REJECTED by the D-001 refinement-only filter. **Deferred to v0.5+** when the council keeper is wired to a live governance runtime (the AUDIT's own recommendation: "add in v0.3 when wiring the council keeper to a live governance runtime"). The skeleton council keeper in v0.2 does not consume a Proposal lifecycle; adding the types without the runtime would be dead code.
- **Severity (AUDIT)**: P1 (spec drift from deliverable text, not a must-have, not blocking).
- **v0.4 action**: DOCUMENT only (this section). No code change.
**Divergence P1-2 (AUDIT §193): `SignalKind` 4 sources vs spec `VoiceSource` 5 sources.**
- **Spec source**: P3-01-01 deliverable specified `VoiceSource` with 5 sources (Stash/Standing/Vouch/Freeholder/Guild).
- **Implemented**: `SignalKind` with 4 sources: `SignalStash`, `SignalStanding`, `SignalVouch`, `SignalCapital` (`SignalKindCount = 4`, locked const).
- **Code rationale (AUDIT §193 P1-2)**: the 4-source shape is a defensible design refinement, not a defect:
- `Freeholder` is an ELIGIBILITY property (upstream in `x/standing`), not a voice signal. A Freeholder-eligible Reach is a precondition for voting, not a signal that feeds a vote's weight.
- `Guild` is a COUNCIL TIER (one of the three councils is the Guild Council), not a voice signal. Including Guild as a signal kind would conflate the council tier with the signal source.
- `Capital` is committed-capital (vision §9.1, one of the four Freeholder signals), which the spec's `VoiceSource` list omitted. Adding `Capital` corrects the spec list to match vision §9.1's four-signal definition (REQ-005: "Four Freeholder signals locked").
- **Must-have impact**: NONE. The v0.2 P3 must-haves did not enumerate `VoiceSource` coverage; the 4-signal shape matches REQ-005's "Four Freeholder signals locked" exactly.
- **Decision (v0.4, D-050)**: changing `SignalKindCount` 4→5 (to restore the spec's 5-source `VoiceSource`) is a LOCKED-CONST CHANGE. REJECTED by the D-001 refinement-only filter (changing a locked const is a behavioral change, not a refinement). The 4-source shape is the CORRECT shape per vision §9.1 and REQ-005; the spec deliverable text was wrong, not the implementation.
- **Severity (AUDIT)**: P1 (design-choice divergence, tested and self-consistent, not blocking).
- **v0.4 action**: DOCUMENT the rationale here + add `TestSignalKindShapeIntentional` (regression guard) so a future agent changing `SignalKindCount` from 4 to 5 must also update the intent-assertion test, surfacing the AUDIT rationale for review. No locked-const change.
**Divergence P2 (AUDIT §193): `x/bearers/types` `ValidateGenesis` no-op.**
- **Spec source**: P4-02-01 said "DefaultParams/GenesisState unchanged" (bearers is an EXTENSION in v0.2, not a new module; the A-212 `ValidateGenesis` upgrade was scoped to NEW modules only).
- **Implemented**: `ValidateGenesis` remains a no-op (`x/bearers/types/types.go:108` returns `nil` unconditionally).
- **Decision (v0.4)**: CORRECT per spec — no action (AUDIT explicitly notes "no action"). The A-212 upgrade applies to NEW modules (v0.2's `x/window`, `x/stand`, etc.), not to EXTENDED modules like `x/bearers`. Listed here for completeness; no code change, no test change.
+38
View File
@@ -85,3 +85,41 @@ func FindBannedTerm(s string) (string, bool) {
func ContainsBannedTerm(s string) (string, bool) {
return FindBannedTerm(s)
}
// SyntheticBannedStrings returns one synthetic string per banned term, each
// embedding exactly one banned term in a plausible sentence context. This
// is the single source of truth (REQ-029, GRILL G-014) for the synthetic
// self-test table consumed by BOTH project-wide meta-tests:
//
// lexicon_meta_test.go :: TestLexiconMetaSelfTestTable (package lexicon_meta, scans x/**/*.go)
// lexicon_meta_docs_test.go :: TestLexiconMetaDocsSelfTestTable (package lexicon_meta_docs, scans README.md + docs/**/*.md)
//
// Before REQ-029, both meta-tests DUPLICATED their own 10-string synthetic
// table (byte-identical), creating a drift risk: a future banned-term
// addition updating one table but not the other would silently drop coverage
// in the unmaintained firewall. SyntheticBannedStrings() eliminates the
// duplication — both meta-tests now consume this helper, so a future addition
// updates both firewalls from one place. The strings are built from
// BannedTerms() (already fragment-assembled), so this package's own source
// stays lexicon-clean (the firewall's own code is allowed to name the terms
// it bans, but only via the fragment-assembly bootstrapping pattern).
//
// The returned slice is indexed positionally against BannedTerms(): the i-th
// synthetic string embeds the i-th banned term. Both meta-tests assert
// len(SyntheticBannedStrings()) == len(BannedTerms()) and that each string
// triggers FindBannedTerm with the matching term.
func SyntheticBannedStrings() []string {
terms := BannedTerms()
return []string{
"open a " + terms[0] + " here", // bank
"make a " + terms[1] + " now", // deposit
"compounding " + terms[2] + " rate", // interest
"the " + terms[3] + " is 5pct", // yield
"foreign " + terms[4] + " pair", // currency
"price in " + terms[5], // dollar
"price in " + terms[6], // euro
"freeze the " + terms[7], // account
"move to " + terms[8] + " now", // savings
"the " + terms[9] + " lost money", // depositor
}
}
+11 -31
View File
@@ -128,22 +128,14 @@ func TestLexiconMetaDocsNoBannedTermsInDocs(t *testing.T) {
// breaks, this test fails before the firewall silently passes a real
// violation in a docs page.
//
// G-014 self-test drift: this table is the docs mirror of the
// TestLexiconMetaSelfTestTable in lexicon_meta_test.go (package lexicon_meta).
// Both reuse lexicon.BannedTerms() as the single source for the 10 terms, so
// a future addition updates both firewalls from one place. The synthetic
// strings are assembled from lexicon.BannedTerms() fragments so this file
// does not contain any banned term as a literal substring (it would otherwise
// trip its own scan; the meta-test file is also excluded from its own scan,
// but the self-test keeps the source clean for readability/searchability).
//
// CROSS-REFERENCE: keep this table aligned with
//
// lexicon_meta_test.go :: TestLexiconMetaSelfTestTable
//
// Any change to the synthetic-string construction must be mirrored in both
// files (or, preferably, add a shared helper in the lexicon package — see
// G-014 minimum-viable: cross-reference comment + shared BannedTerms()).
// REQ-029 (GRILL G-014): the synthetic strings are sourced from
// lexicon.SyntheticBannedStrings(), the single source of truth shared with
// lexicon_meta_test.go :: TestLexiconMetaSelfTestTable. Before REQ-029, this
// file DUPLICATED its own 10-string table (byte-identical to the x/ meta-
// test), creating a drift risk; the shared helper closes it. This file no
// longer builds its own synthetic table — both meta-tests consume the same
// helper, so a future banned-term addition updates both firewalls from one
// place.
func TestLexiconMetaDocsSelfTestTable(t *testing.T) {
terms := lexicon.BannedTerms()
// The spec lists 10 banned terms (plan docs say "9", counting dollar/euro
@@ -152,22 +144,10 @@ func TestLexiconMetaDocsSelfTestTable(t *testing.T) {
if len(terms) != 10 {
t.Fatalf("BannedTerms() len = %d, want 10", len(terms))
}
// Each synthetic string embeds exactly one banned term in a plausible
// sentence context. Each must be detected.
synthetic := []string{
"open a " + terms[0] + " here", // bank
"make a " + terms[1] + " now", // deposit
"compounding " + terms[2] + " rate", // interest
"the " + terms[3] + " is 5pct", // yield
"foreign " + terms[4] + " pair", // currency
"price in " + terms[5], // dollar
"price in " + terms[6], // euro
"freeze the " + terms[7], // account
"move to " + terms[8] + " now", // savings
"the " + terms[9] + " lost money", // depositor
}
// REQ-029: consume the shared synthetic-string helper (G-014 single source).
synthetic := lexicon.SyntheticBannedStrings()
if len(synthetic) != len(terms) {
t.Fatalf("synthetic table len = %d, want %d", len(synthetic), len(terms))
t.Fatalf("SyntheticBannedStrings() len = %d, want %d (must match BannedTerms())", len(synthetic), len(terms))
}
for i, s := range synthetic {
found, ok := lexicon.FindBannedTerm(s)
+9 -19
View File
@@ -76,10 +76,12 @@ func TestLexiconMetaNoBannedTermsInX(t *testing.T) {
// firewall's detection logic is durably verified — if detection ever breaks,
// this test fails before the firewall silently passes a real violation.
//
// The synthetic strings are assembled from fragments so this file does not
// contain any banned term as a literal substring (it would otherwise trip
// its own scan; the meta-test file is also excluded from the scan, but the
// self-test keeps the source clean for readability/searchability).
// REQ-029 (GRILL G-014): the synthetic strings are sourced from
// lexicon.SyntheticBannedStrings(), the single source of truth shared with
// lexicon_meta_docs_test.go :: TestLexiconMetaDocsSelfTestTable. Before
// REQ-029, both meta-tests DUPLICATED their own 10-string table, creating a
// drift risk; the shared helper closes it. This file no longer builds its
// own synthetic table.
func TestLexiconMetaSelfTestTable(t *testing.T) {
terms := lexicon.BannedTerms()
// The spec lists 10 banned terms (plan docs say "9", counting dollar/euro
@@ -88,22 +90,10 @@ func TestLexiconMetaSelfTestTable(t *testing.T) {
if len(terms) != 10 {
t.Fatalf("BannedTerms() len = %d, want 10", len(terms))
}
// Each synthetic string embeds exactly one banned term in a plausible
// sentence context. Each must be detected.
synthetic := []string{
"open a " + terms[0] + " here", // bank
"make a " + terms[1] + " now", // deposit
"compounding " + terms[2] + " rate", // interest
"the " + terms[3] + " is 5pct", // yield
"foreign " + terms[4] + " pair", // currency
"price in " + terms[5], // dollar
"price in " + terms[6], // euro
"freeze the " + terms[7], // account
"move to " + terms[8] + " now", // savings
"the " + terms[9] + " lost money", // depositor
}
// REQ-029: consume the shared synthetic-string helper (G-014 single source).
synthetic := lexicon.SyntheticBannedStrings()
if len(synthetic) != len(terms) {
t.Fatalf("synthetic table len = %d, want %d", len(synthetic), len(terms))
t.Fatalf("SyntheticBannedStrings() len = %d, want %d (must match BannedTerms())", len(synthetic), len(terms))
}
for i, s := range synthetic {
found, ok := lexicon.FindBannedTerm(s)
+52
View File
@@ -109,6 +109,58 @@ func TestSignalKindCountLockedConst(t *testing.T) {
}
}
// TestSignalKindShapeIntentional (REQ-031, AUDIT §193 P1-2) is a regression
// GUARD that documents and locks the 4-source SignalKind shape. It is NOT a
// shape change — the existing TestSignalKindCountLockedConst already locks
// the count. This test adds the INTENT documentation so a future agent who
// changes SignalKindCount from 4 to 5 (e.g., to "restore" the spec's 5-source
// VoiceSource list) must also update this test, surfacing the AUDIT rationale
// for review.
//
// AUDIT §193 P1-2 rationale (why SignalKind is 4 sources, NOT the spec's 5):
//
// The v0.2 P3-01-01 deliverable specified VoiceSource with 5 sources
// (Stash/Standing/Vouch/Freeholder/Guild). The implementation uses
// SignalKind with 4 sources (Stash/Standing/Vouch/Capital). The 4-source
// shape is a defensible design refinement:
// - Freeholder is an ELIGIBILITY property (upstream in x/standing), not
// a voice signal. A Freeholder-eligible Reach is a precondition for
// voting, not a signal that feeds a vote's weight.
// - Guild is a COUNCIL TIER (one of the three councils is the Guild
// Council), not a voice signal. Including Guild as a signal kind
// would conflate the council tier with the signal source.
// - Capital is committed-capital (vision §9.1, one of the four
// Freeholder signals per REQ-005), which the spec's VoiceSource list
// omitted. Adding Capital corrects the spec to match vision §9.1's
// four-signal definition (REQ-005: "Four Freeholder signals locked").
//
// The 4-source shape matches REQ-005 exactly. The spec deliverable text
// was wrong, not the implementation. v0.4 (D-050) DOCUMENTS this and
// locks the 4-source shape; changing it to 5 is a locked-const change
// rejected by the D-001 refinement-only filter and deferred to a future
// milestone that re-litigates REQ-005's signal definition.
//
// See .ciagent/oy/ARCHITECTURE.md §"Council Voice/Council Interface —
// Lifecycle Type Divergence Decisions (v0.4, REQ-031)" for the full rationale.
func TestSignalKindShapeIntentional(t *testing.T) {
// LOCKED: 4 sources. Changing this to 5 requires updating this test's
// intent block AND re-litigating REQ-005's four-signal definition.
const expectedSignalCount = 4
if types.SignalKindCount != expectedSignalCount {
t.Fatalf("SignalKindCount = %d, want %d (REQ-031 intent guard: the 4-source shape is intentional per AUDIT §193 P1-2; see ARCHITECTURE.md v0.4 divergence section before changing this)", types.SignalKindCount, expectedSignalCount)
}
want := []types.SignalKind{types.SignalStash, types.SignalStanding, types.SignalVouch, types.SignalCapital}
all := types.AllSignalKinds()
if len(all) != len(want) {
t.Fatalf("AllSignalKinds() len = %d, want %d", len(all), len(want))
}
for i, s := range all {
if s != want[i] {
t.Errorf("AllSignalKinds()[%d] = %q, want %q (REQ-031 intent guard: the 4-source shape {Stash, Standing, Vouch, Capital} is intentional per AUDIT §193 P1-2; Freeholder and Guild are NOT signal kinds)", i, s, want[i])
}
}
}
// TestAllSignalKindsNames asserts the 4 signal names (Stash, Standing,
// Vouch, Capital) cross-ref v0.1 x/standing FreeholderSignals (StashMaturity,
// MultiDomainStanding, CommunityEndorsement, CommittedCapital).
+70
View File
@@ -0,0 +1,70 @@
package types
// cross_const_test.go (REQ-030, REVIEW.md P2 / A-304, GRILL G-015) is a
// cross-package const-equality test that catches silent drift between the
// x/hub LOCAL consts (LendingCouponCapBps / LendingCouponFloorBps) and the
// x/bond mission-locked consts (CouponCapBps / CouponFloorBps, D-028).
//
// Before REQ-030, the two const pairs were cross-documented only by a comment
// (x/hub/types/types.go:46-55) — no automated check existed. A future
// mission-locked change to x/bond.CouponCapBps without a matching x/hub change
// would silently drift. This test fails closed on either kind of drift:
//
// - single-sided drift: hub stays 800, bond changes to 900 → the equality
// test fails.
// - paired drift: BOTH change to the same wrong value (e.g., both 900) → the
// equality test passes BUT the absolute-value test (G-015) fails, because
// the mission-locked value is 800, not 900.
//
// G-003 (no production cross-module struct imports): this is a TEST-ONLY
// import of x/bond/types in a _test.go file. G-003's test-import exemption
// (documented in v0.2 GRILL G-003 and already exercised by
// x/bearers/types/types_test.go:7 importing x/processing/types) permits
// cross-package test imports. NO production .go file in x/hub/types/ imports
// x/bond/types (the P1-99-01 verification greps non-test .go files to confirm).
import (
"testing"
bondtypes "github.com/oy/openyield/x/bond/types"
)
// TestLendingCouponCapMatchesBondCap asserts the x/hub LOCAL
// LendingCouponCapBps equals the x/bond mission-locked CouponCapBps (A-304).
// Fails on single-sided drift (one changes, the other does not).
func TestLendingCouponCapMatchesBondCap(t *testing.T) {
if LendingCouponCapBps != bondtypes.CouponCapBps {
t.Errorf("A-304 drift: x/hub LendingCouponCapBps = %d, x/bond CouponCapBps = %d (must match)", LendingCouponCapBps, bondtypes.CouponCapBps)
}
}
// TestLendingCouponFloorMatchesBondFloor asserts the x/hub LOCAL
// LendingCouponFloorBps equals the x/bond mission-locked CouponFloorBps
// (A-304). Fails on single-sided drift.
func TestLendingCouponFloorMatchesBondFloor(t *testing.T) {
if LendingCouponFloorBps != bondtypes.CouponFloorBps {
t.Errorf("A-304 drift: x/hub LendingCouponFloorBps = %d, x/bond CouponFloorBps = %d (must match)", LendingCouponFloorBps, bondtypes.CouponFloorBps)
}
}
// TestConstsAreMissionLocked800And0 (GRILL G-015) asserts the ABSOLUTE
// mission-locked values: both caps are 800 (8pct, D-028) and both floors are 0
// (0pct, D-028). This catches PAIRED drift — if both consts change to the same
// wrong value (e.g., both 900), the equality tests above pass but this test
// fails, because the mission-locked value is 800, not 900. The 8pct cap /
// 0pct floor is the anti-greed covenant (vision §17, §18); defending the
// absolute value is the highest-priority regression guard in v0.4.
func TestConstsAreMissionLocked800And0(t *testing.T) {
if LendingCouponCapBps != 800 {
t.Errorf("G-015: x/hub LendingCouponCapBps = %d, want 800 (mission-locked 8pct, D-028)", LendingCouponCapBps)
}
if bondtypes.CouponCapBps != 800 {
t.Errorf("G-015: x/bond CouponCapBps = %d, want 800 (mission-locked 8pct, D-028)", bondtypes.CouponCapBps)
}
if LendingCouponFloorBps != 0 {
t.Errorf("G-015: x/hub LendingCouponFloorBps = %d, want 0 (mission-locked 0pct, D-028)", LendingCouponFloorBps)
}
if bondtypes.CouponFloorBps != 0 {
t.Errorf("G-015: x/bond CouponFloorBps = %d, want 0 (mission-locked 0pct, D-028)", bondtypes.CouponFloorBps)
}
}