Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6d33a58b3 | |||
| 7d1468b442 | |||
| 92966cb9c5 | |||
| 838bd06a9d | |||
| 57c7dc5ff5 | |||
| 2ca0e1aa4b | |||
| cc0940d9f8 | |||
| 0bb14bd1a1 |
@@ -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"]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user