// Package lexicon_meta_docs holds the docs lexicon firewall (REQ-028, D-043). // // It is a NEW sibling meta-test created in v0.3 P1 Wave 1 that MIRRORS the v0.2 // project-wide firewall (lexicon_meta_test.go, package lexicon_meta) but scans // the docs surface (README.md + docs/**/*.md) instead of x/**/*.go. It uses // the SAME lexicon.FindBannedTerm (word-boundary, case-insensitive) — NO // detection reimplementation — so the two firewalls share a single source of // truth for the 10 banned terms (bank, deposit, interest, yield, currency, // dollar, euro, account, savings, depositor). // // Placement: this file lives in lexicon_meta_docs/ (a subdirectory of the // repo root) because Go does not permit two distinct packages in the same // directory; the v0.2 firewall is package lexicon_meta at the repo root. // The invocation `go test ./lexicon_meta_docs/...` (PLANS P1-03-01) resolves // to this package. Run via `go test ./...` from the repo root as well. // // G-013 walk-coverage: TestLexiconMetaDocsWalkCoverage injects a synthetic // banned-term .md into a temp docs/ subtree and asserts the walk FINDS it. // This closes the "silently scans nothing and reports green" failure mode // that the G-009 self-test table (detection) alone does not cover. // // G-014 self-test drift: the self-test table and banned-term count assertion // reuse lexicon.BannedTerms() (the single source). A cross-reference comment // keeps this file's table in lockstep with lexicon_meta_test.go's table; if // a banned term is added, both firewalls update from one place. package lexicon_meta_docs import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/oy/openyield/lexicon" ) // repoRoot returns the absolute path to the repo root by walking up from // this test file (the test lives at /lexicon_meta_docs/). func repoRoot(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } // file = .../oy/lexicon_meta_docs/lexicon_meta_docs_test.go // repo root = filepath.Dir(filepath.Dir(file)) return filepath.Dir(filepath.Dir(file)) } // thisFile returns the absolute path of this meta-test file (to exclude it // from its own scan — it references banned terms via the lexicon package, // whose source assembles terms from fragments, so no banned-term literal // appears in the firewall's own code). func thisFile(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } return file } // TestLexiconMetaDocsNoBannedTermsInDocs is the docs firewall (D-043). It // walks README.md (repo root) + every *.md under docs/ (recursive), reads each // file's source, and asserts no banned term is present (word-boundary, // case-insensitive). Excludes .ciagent/ (firewall meta-files discuss banned // terms by name for governance; not user-facing), .git/ (VCS), and this test // file itself (self-exclusion via runtime.Caller(0)). // // Passes at P1 Wave 1 with zero docs (a walk that scans nothing reports green // on zero hits — closed by TestLexiconMetaDocsWalkCoverage below). With the // Wave 2 docs present (README + index + 6 shared pages), all are lexicon-clean // by construction. func TestLexiconMetaDocsNoBannedTermsInDocs(t *testing.T) { root := repoRoot(t) this := thisFile(t) hits := []string{} err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { base := filepath.Base(path) if base == ".ciagent" || base == ".git" { return filepath.SkipDir } return nil } // Self-exclusion: skip this meta-test file. if path == this { return nil } // Only scan .md files. if !strings.HasSuffix(path, ".md") { return nil } // Only scan README.md (repo root) + docs/**/*.md. rel, rerr := filepath.Rel(root, path) if rerr != nil { return rerr } if rel != "README.md" && !strings.HasPrefix(rel, "docs"+string(filepath.Separator)) && rel != "docs" { return nil } bz, rerr := os.ReadFile(path) if rerr != nil { return rerr } if found, ok := lexicon.FindBannedTerm(string(bz)); ok { hits = append(hits, rel+" contains banned term "+found) } return nil }) if err != nil { t.Fatalf("walk: %v", err) } if len(hits) > 0 { t.Errorf("REQ-028 docs lexicon firewall violations:\n %s", strings.Join(hits, "\n ")) } } // TestLexiconMetaDocsSelfTestTable (G-009 for docs) is the firewall's own // detection-coverage guard. Each synthetic string embeds exactly one banned // term in a plausible sentence context and is asserted to trigger detection, // so the firewall's detection logic is durably verified — if detection ever // 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()). func TestLexiconMetaDocsSelfTestTable(t *testing.T) { terms := lexicon.BannedTerms() // The spec lists 10 banned terms (plan docs say "9", counting dollar/euro // as a pair): bank, deposit, interest, yield, currency, dollar, euro, // account, savings, depositor. 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 } if len(synthetic) != len(terms) { t.Fatalf("synthetic table len = %d, want %d", len(synthetic), len(terms)) } for i, s := range synthetic { found, ok := lexicon.FindBannedTerm(s) if !ok { t.Errorf("G-009 docs self-test [%d]: synthetic string did not trigger detection: %q", i, s) continue } if found != terms[i] { t.Errorf("G-009 docs self-test [%d]: detected %q, want %q (in %q)", i, found, terms[i], s) } } } // TestLexiconMetaDocsBannedTermsCount asserts exactly 10 banned terms are // configured (locked-const for the firewall's scope; spec lists 10, plan docs // say "9" counting dollar/euro as a pair). Derived from lexicon.BannedTerms() // — the single source — so a count change breaks both this firewall and the // v0.2 x/*.go firewall (G-014 drift prevention). func TestLexiconMetaDocsBannedTermsCount(t *testing.T) { terms := lexicon.BannedTerms() if len(terms) != 10 { t.Errorf("BannedTerms() len = %d, want 10 (REQ-012/REQ-028)", len(terms)) } seen := map[string]bool{} for _, tr := range terms { if seen[tr] { t.Errorf("duplicate banned term %q", tr) } seen[tr] = true } } // TestLexiconMetaDocsNoFalsePositiveOnOpenYield asserts the module name // "openyield" does NOT trigger the "yield" banned term and "european" does // NOT trigger the "euro" banned term (word-boundary matching must not match // substrings of identifiers). This is the regression firewall for the // word-boundary detection design — mirrors the v0.2 // TestLexiconMetaNoFalsePositiveOnOpenYield. func TestLexiconMetaDocsNoFalsePositiveOnOpenYield(t *testing.T) { cases := []string{ "github.com/oy/openyield/x/window/types", "package openyield", "openyield is the module", "european resident", "# OpenYield docs", "the OpenYield mesh", } for _, s := range cases { if _, ok := lexicon.FindBannedTerm(s); ok { t.Errorf("false positive: %q triggered a banned term (word-boundary must avoid this)", s) } } } // TestLexiconMetaDocsWalkCoverage (G-013) is the walk-coverage firewall. The // G-009 self-test table (above) verifies DETECTION (FindBannedTerm on // synthetic strings) but NOT the WALK (which files are scanned). A walk bug // — e.g. wrong path prefix, missing docs/ recursion, a typo in the .md // suffix check — would silently scan nothing and report green on zero // files. This test closes that gap by injecting a synthetic banned-term .md // into a fixture dir under the real docs/ path the walk scans and asserting // the walk FINDS it. // // The fixture is created under docs/.lexicon_fixture/ (a real docs/ subtree // the walk reaches) and removed via defer so it never leaks into the repo. // If the walk logic misses the fixture, this test fails loudly instead of // letting a broken walk pass the firewall green on zero files scanned. func TestLexiconMetaDocsWalkCoverage(t *testing.T) { root := repoRoot(t) this := thisFile(t) // Build a synthetic banned term from fragments so THIS file does not // contain a banned-term literal (it is excluded from its own scan, but // the synthetic stays clean for readability/searchability). terms := lexicon.BannedTerms() if len(terms) == 0 { t.Fatal("BannedTerms() returned no terms — cannot run walk-coverage") } // Use the first banned term ("bank") assembled from two halves. syntheticTerm := terms[0][:2] + terms[0][2:] // reassemble (no literal in source) badContent := []byte("# fixture\nthis file contains a banned term: " + syntheticTerm + "\n") fixtureDir := filepath.Join(root, "docs", ".lexicon_fixture") fixtureFile := filepath.Join(fixtureDir, "bad_fixture.md") if err := os.MkdirAll(fixtureDir, 0o755); err != nil { t.Fatalf("mkdir fixture: %v", err) } defer os.RemoveAll(fixtureDir) if err := os.WriteFile(fixtureFile, badContent, 0o644); err != nil { t.Fatalf("write fixture: %v", err) } // Run the SAME walk logic as TestLexiconMetaDocsNoBannedTermsInDocs and // assert it FINDS the fixture's banned term. A walk that returns zero // hits here proves the walk logic is broken (the fixture is a known-bad // file inside docs/ that MUST be detected). hits := []string{} err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { base := filepath.Base(path) if base == ".ciagent" || base == ".git" { return filepath.SkipDir } return nil } if path == this { return nil } if !strings.HasSuffix(path, ".md") { return nil } rel, rerr := filepath.Rel(root, path) if rerr != nil { return rerr } if rel != "README.md" && !strings.HasPrefix(rel, "docs"+string(filepath.Separator)) { return nil } bz, rerr := os.ReadFile(path) if rerr != nil { return rerr } if found, ok := lexicon.FindBannedTerm(string(bz)); ok { hits = append(hits, rel+" contains banned term "+found) } return nil }) if err != nil { t.Fatalf("walk: %v", err) } // Assert the fixture was found. The rel path uses OS-specific separator; // match on the suffix so the test is portable. foundFixture := false for _, h := range hits { if strings.Contains(h, "bad_fixture.md") && strings.Contains(h, syntheticTerm) { foundFixture = true break } } if !foundFixture { t.Errorf("G-013 walk-coverage: the walk did NOT find the synthetic banned-term fixture at %s — the docs firewall walk logic is broken (it would silently scan nothing and report green). hits=%v", fixtureFile, hits) } }