Files
openyield/lexicon/lexicon.go
T
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

126 lines
5.1 KiB
Go

// Package lexicon holds the project-wide lexicon firewall (REQ-012).
//
// The 9 banned financial terms must never appear in any production or test
// .go file under x/. This package exposes the banned-terms list and detection
// helpers; the terms themselves are assembled at runtime from two-character
// fragments so that the SOURCE of this package does not contain any banned
// term as a literal substring. This is the standard lexicon-test bootstrapping
// pattern: the firewall's own code must not trip the firewall.
//
// The lexicon firewall is NEW in v0.2 (G-002): v0.1 is lexicon-clean in
// practice but has zero lexicon tests. The project-wide meta-test in
// P1-04-02 (lexicon_meta_test.go) is the durable firewall; per-package
// lexicon assertions in each new module's types_test.go scan the module's
// production files.
package lexicon
import (
"regexp"
"strings"
)
// term is a banned term assembled from two halves so the source file does
// not contain the literal banned word.
type term struct {
a, b string
}
// fragments holds the 9 banned terms as (a, b) halves. Neither half alone
// is a banned term, and concatenation produces the banned term at runtime.
var fragments = []term{
{"ba", "nk"}, // bank
{"depo", "sit"}, // deposit
{"intere", "st"}, // interest
{"yie", "ld"}, // yield
{"curre", "ncy"}, // currency
{"dol", "lar"}, // dollar
{"eu", "ro"}, // euro
{"acco", "unt"}, // account
{"savin", "gs"}, // savings
{"deposito", "r"}, // depositor
}
// BannedTerms returns the banned financial terms (REQ-012). The spec lists
// 10 terms (often described as "9" in plan docs, counting dollar/euro as a
// pair): bank, deposit, interest, yield, currency, dollar, euro, account,
// savings, depositor. The terms are assembled at runtime from fragments so
// this package's source does not contain any banned term as a literal
// substring.
func BannedTerms() []string {
out := make([]string, len(fragments))
for i, t := range fragments {
out[i] = t.a + t.b
}
return out
}
// bannedTermRegexes are the compiled word-boundary regexes for the 9 banned
// terms. Word boundaries prevent false positives like "openyield" matching
// "yield" or "european" matching "euro" — the firewall bans the words as
// concepts, not as arbitrary substrings. The regexes are case-insensitive.
var bannedTermRegexes = func() []*regexp.Regexp {
terms := BannedTerms()
out := make([]*regexp.Regexp, len(terms))
for i, t := range terms {
out[i] = regexp.MustCompile(`\b` + regexp.QuoteMeta(t) + `\b`)
}
return out
}()
// FindBannedTerm returns the first banned term found in s (case-insensitive,
// word-boundary match) and true, or "" and false if none. Used by the
// project-wide meta-test (P1-04-02) and the per-package lexicon assertions.
func FindBannedTerm(s string) (string, bool) {
lower := strings.ToLower(s)
terms := BannedTerms()
for i, re := range bannedTermRegexes {
if re.MatchString(lower) {
return terms[i], true
}
}
return "", false
}
// ContainsBannedTerm is an alias for FindBannedTerm kept for compatibility.
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
}
}