From 2ca0e1aa4ba341142e6e2cb57fdf85b9575186a1 Mon Sep 17 00:00:00 2001 From: cloudinit-bot Date: Mon, 17 Aug 2026 23:30:30 +0000 Subject: [PATCH] refactor(lexicon): shared SyntheticBannedStrings() helper (REQ-029, G-014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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--- --- lexicon/lexicon.go | 38 +++++++++++ lexicon_meta_docs/lexicon_meta_docs_test.go | 42 ++++--------- lexicon_meta_test.go | 28 +++------ x/hub/types/cross_const_test.go | 70 +++++++++++++++++++++ 4 files changed, 128 insertions(+), 50 deletions(-) create mode 100644 x/hub/types/cross_const_test.go diff --git a/lexicon/lexicon.go b/lexicon/lexicon.go index db127a3..0696b55 100644 --- a/lexicon/lexicon.go +++ b/lexicon/lexicon.go @@ -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 + } +} diff --git a/lexicon_meta_docs/lexicon_meta_docs_test.go b/lexicon_meta_docs/lexicon_meta_docs_test.go index f69e474..ed3a124 100644 --- a/lexicon_meta_docs/lexicon_meta_docs_test.go +++ b/lexicon_meta_docs/lexicon_meta_docs_test.go @@ -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) diff --git a/lexicon_meta_test.go b/lexicon_meta_test.go index b6a7488..0280f3b 100644 --- a/lexicon_meta_test.go +++ b/lexicon_meta_test.go @@ -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) diff --git a/x/hub/types/cross_const_test.go b/x/hub/types/cross_const_test.go new file mode 100644 index 0000000..427fa80 --- /dev/null +++ b/x/hub/types/cross_const_test.go @@ -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) + } +}