// 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 } }