a780884379
P1 complete. Docs lexicon firewall (lexicon_meta_docs_test.go, 5 tests incl. G-013 walk-coverage + G-014 shared self-test). MkDocs Material scaffold with 26-page nav (G-011). README.md + docs/index.md + 7 docs/shared/ pages. Both firewalls green, go test ./... 22 packages green, no regression. ---ci--- project: oy phase: 1 milestone: v0.3 status: complete tag_base: v0.2.x phase_role: execution requirements: covered: [REQ-028] partial: [REQ-027] ---/ci---
317 lines
12 KiB
Go
317 lines
12 KiB
Go
// 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 <repoRoot>/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)
|
|
}
|
|
}
|