oficina / sheets Public

Fix guard-blind-spot review findings: tree matching, not exact-dir

Review found the guards checking file location or import identity by
exact string/directory equality rather than tree membership, so a
future subpackage would go unchecked instead of exempted or caught —
the exact failure mode these guards exist to prevent, happening inside
the guard itself.

- calc_isolation_test.go (Important): calcHome was matched against
  filepath.Dir(rel) directly, so only files whose *immediate* parent is
  internal/sheets/calc were scanned. A calc/functions subpackage would
  be invisible. Now uses inTree() (prefix match) so the whole calc tree
  is covered.
- calc_isolation_test.go (Minor): the store forbidden-import comparison
  was exact equality, so a hypothetical store/schema subpackage could
  be imported undetected. Now uses importUnder() (prefix match) for the
  store target. The internal/sheets-itself target stays exact-match on
  purpose — treating it as a tree would also silently forbid calc from
  importing sibling packages (format, xlsx, seal, sealtest, guards)
  that the rule never named; that's a real rule change, not a
  blind-spot fix, and not this review's ask.
- excelize_confinement_test.go (Minor): same exact-dir pattern, but in
  the safe direction (a future xlsx subpackage would be wrongly
  blocked rather than wrongly allowed). Fixed for consistency, using
  the same inTree() helper.
- linecap_test.go (Minor): countLines' comment claimed wc -l semantics
  it doesn't have (bufio.Scanner counts a final unterminated line;
  wc -l counts newline characters and would not). Comment now
  describes the actual, stricter behavior instead.

Added inTree() and importUnder() to repo_test.go as the shared
tree/prefix-matching helpers both fixes use.

Verified: gofmt clean, go vet clean, all five guards still pass on the
real tree, and the calc-isolation fix was proven live — created
internal/sheets/calc/scratch/scratch.go importing store, confirmed
TestCalcIsolatedFromStoreAndSheets now catches it (it would have been
invisible under the old exact-dir-match code), then deleted the
scratch package. Full `make test` green end to end (1m44.9s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
Paul Campbell pushed by paul@keymail.dev cf7f93e6218dfd9b0c9ce19a40db2fb2cb8b4262 parent f4ba063
4 files changed, +62 −27
  • internal/guards/calc_isolation_test.go +26 −19
  • internal/guards/excelize_confinement_test.go +9 −6
  • internal/guards/linecap_test.go +8 −2
  • internal/guards/repo_test.go +19 −0
diff --git a/internal/guards/calc_isolation_test.go b/internal/guards/calc_isolation_test.go
index 1e4ea45..9c8a12e 100644
--- a/internal/guards/calc_isolation_test.go
+++ b/internal/guards/calc_isolation_test.go
@@ -1,38 +1,45 @@
package guards
import (
- "path/filepath"
"testing"
)
-// calcHome is the formula engine's package.
+// calcHome is the formula engine's package tree — calc itself and any
+// subpackage of it (e.g. a future calc/functions).
const calcHome = "internal/sheets/calc"
-// calcForbiddenImports are import paths calc must never reach for: the
-// storage layer, and the app package (internal/sheets) that wires it to
-// storage and HTTP. calc is the pure formula engine — parsing, the
-// dependency graph, evaluation — and importing either would mean it is
-// no longer pure.
-var calcForbiddenImports = []string{
- "amadan.net/oficina/sheets/internal/sheets/store",
- "amadan.net/oficina/sheets/internal/sheets",
-}
+// storeImport is the storage layer's own import path. Matched as a tree
+// (storeImport itself, or anything nested under it, e.g. a hypothetical
+// future store/schema) so a subpackage of store can't quietly slip past
+// an exact-equality check the way store's root package can't today.
+const storeImport = "amadan.net/oficina/sheets/internal/sheets/store"
+
+// sheetsImport is internal/sheets itself — the app/wiring package, not
+// its namespace. Deliberately matched by exact equality, not as a tree:
+// internal/sheets/format, /xlsx, /seal, /sealtest and /guards all live
+// under that same import prefix, and the rule as written ("internal/sheets
+// itself") names the one package, not everything nested under it. Widening
+// this to a tree match would silently forbid calc from importing any of
+// those siblings too — a real rule change, not a blind-spot fix, and one
+// nobody has decided to make.
+const sheetsImport = "amadan.net/oficina/sheets/internal/sheets"
-// TestCalcIsolatedFromStoreAndSheets fails on any file in
-// internal/sheets/calc that imports internal/sheets/store or
-// internal/sheets itself.
+// TestCalcIsolatedFromStoreAndSheets fails on any file in the
+// internal/sheets/calc tree that imports the store package (or any
+// future subpackage of it) or internal/sheets itself.
func TestCalcIsolatedFromStoreAndSheets(t *testing.T) {
root := repoRoot(t)
for _, path := range goFiles(t, root) {
rel := relPath(t, root, path)
- if filepath.ToSlash(filepath.Dir(rel)) != calcHome {
+ if !inTree(rel, calcHome) {
continue
}
for _, imp := range fileImports(t, path) {
- for _, forbidden := range calcForbiddenImports {
- if imp == forbidden {
- t.Errorf("%s: imports %q, forbidden in %s (calc isolation)", rel, imp, calcHome)
- }
+ switch {
+ case importUnder(imp, storeImport):
+ t.Errorf("%s: imports %q, forbidden in %s (calc isolation)", rel, imp, calcHome)
+ case imp == sheetsImport:
+ t.Errorf("%s: imports %q, forbidden in %s (calc isolation)", rel, imp, calcHome)
}
}
}
diff --git a/internal/guards/excelize_confinement_test.go b/internal/guards/excelize_confinement_test.go
index 427b4ec..c74a8de 100644
--- a/internal/guards/excelize_confinement_test.go
+++ b/internal/guards/excelize_confinement_test.go
@@ -1,25 +1,28 @@
package guards
import (
- "path/filepath"
"testing"
)
// excelizeImport is the XLSX library's import path.
const excelizeImport = "github.com/xuri/excelize/v2"
-// excelizeHome is the only package allowed to import it. Confining
-// excelize's quirks to one package keeps the XLSX format's weirdness
-// from leaking into the rest of Sheets.
+// excelizeHome is the package tree allowed to import it — xlsx itself
+// and any subpackage of it. Matched as a tree, not by exact directory
+// equality: equality would make a future xlsx subpackage look like it
+// was outside home and wrongly forbid it from importing excelize (the
+// over-strict direction — a false positive, not a leak — but still
+// wrong). Confining excelize's quirks to this one tree keeps the XLSX
+// format's weirdness from leaking into the rest of Sheets.
const excelizeHome = "internal/sheets/xlsx"
// TestExcelizeConfinedToXLSXPackage fails on any excelize import outside
-// internal/sheets/xlsx.
+// the internal/sheets/xlsx tree.
func TestExcelizeConfinedToXLSXPackage(t *testing.T) {
root := repoRoot(t)
for _, path := range goFiles(t, root) {
rel := relPath(t, root, path)
- if filepath.ToSlash(filepath.Dir(rel)) == excelizeHome {
+ if inTree(rel, excelizeHome) {
continue
}
for _, imp := range fileImports(t, path) {
diff --git a/internal/guards/linecap_test.go b/internal/guards/linecap_test.go
index ec3cc51..e475cb6 100644
--- a/internal/guards/linecap_test.go
+++ b/internal/guards/linecap_test.go
@@ -38,8 +38,14 @@ func TestFileLineCap(t *testing.T) {
}
}
-// countLines counts newline-terminated lines the way `wc -l` does,
-// which is what the 300-line figure in AGENTS.md was measured against.
+// countLines counts lines the way bufio.Scanner's ScanLines does: each
+// newline-terminated line, plus a final line even if it has no trailing
+// newline. That is NOT what `wc -l` reports — wc -l counts newline
+// characters, so a file whose last line lacks a trailing newline gets
+// one fewer from wc than from this. This function's count is the
+// stricter of the two (it can only report equal or one higher), and
+// gofmt already guarantees a trailing newline on every .go file this
+// test walks, so the difference never shows up here in practice.
func countLines(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
diff --git a/internal/guards/repo_test.go b/internal/guards/repo_test.go
index 6627e19..1d9fb71 100644
--- a/internal/guards/repo_test.go
+++ b/internal/guards/repo_test.go
@@ -125,3 +125,22 @@ func fileImports(t *testing.T, path string) []string {
}
return imports
}
+
+// inTree reports whether rel — a repo-relative file path — lives inside
+// the directory tree rooted at dir (dir itself, or any subdirectory of
+// it, at any depth). Used instead of comparing filepath.Dir(rel) == dir
+// so a rule means "this whole package tree" and not just "files whose
+// immediate parent happens to match" — a future subpackage (e.g.
+// calc/functions) must inherit the rule, not silently fall outside it.
+func inTree(rel, dir string) bool {
+ return strings.HasPrefix(rel, dir+"/")
+}
+
+// importUnder reports whether imp is base itself, or an import path
+// nested under base (base plus "/something"). Used instead of plain
+// equality so a forbidden import stays forbidden if it grows
+// subpackages later (e.g. a future .../store/schema), rather than the
+// check silently narrowing to just the package's root import path.
+func importUnder(imp, base string) bool {
+ return imp == base || strings.HasPrefix(imp, base+"/")
+}