Skip to content

Commit e5e7c5c

Browse files
committed
feat: add TestCoreStructure and TestCrossPackageTypes, migrate 30+ core/ files
TestCoreStructure: core/ directories may only contain doc.go and test files. All domain logic must live in subpackages to prevent god packages. Migrated 30 files across 14 CLI modules to new subpackages (dep/core/golang, dep/core/node, doctor/core/check, drift/core/fix, why/core/strip, etc.). TestCrossPackageTypes: flags exported types used cross-module. Module boundaries recognized: cli/<X> consuming <X> domain, mcp/* internal, write/<X> mapping to <X>. Moved 4 genuinely cross-cutting types to entity/ (NotifyPayload, TemplateRef, EntryParams, IndexEntry). Deleted dead entity types (ClaudeSettings, IndexEntryBlock). Also: TestNoMixedVisibility ensures files with exported functions don't contain unexported functions. Renamed 3 stuttery functions created by the migration (strip.StripMkDocs, show.ShowDoc, menu.ShowMenu). Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent 5623689 commit e5e7c5c

130 files changed

Lines changed: 2401 additions & 1348 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package audit
8+
9+
import (
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
"testing"
14+
)
15+
16+
// allowedCoreFiles lists the files permitted directly
17+
// in a core/ directory (not in subdirectories).
18+
// builder.go: dep/core houses the interface + registry
19+
//
20+
// that imports language subpackages.
21+
//
22+
// types.go: shared types used by multiple subpackages
23+
//
24+
// where moving would create circular imports.
25+
var allowedCoreFiles = map[string]bool{
26+
"doc.go": true,
27+
"builder.go": true,
28+
"types.go": true,
29+
}
30+
31+
// TestCoreStructure ensures core/ directories contain
32+
// only doc.go and test files at the top level. All
33+
// domain logic must live in subpackages (e.g.
34+
// core/budget/, core/score/). This prevents core/
35+
// from becoming a god package.
36+
//
37+
// Test files are exempt.
38+
func TestCoreStructure(t *testing.T) {
39+
cliRoot := filepath.Join("..", "cli")
40+
var violations []string
41+
42+
walkErr := filepath.WalkDir(
43+
cliRoot,
44+
func(
45+
path string, d os.DirEntry, err error,
46+
) error {
47+
if err != nil {
48+
return err
49+
}
50+
if d.IsDir() {
51+
return nil
52+
}
53+
if !strings.HasSuffix(d.Name(), ".go") {
54+
return nil
55+
}
56+
if isTestFile(d.Name()) {
57+
return nil
58+
}
59+
60+
rel, relErr := filepath.Rel(cliRoot, path)
61+
if relErr != nil {
62+
return relErr
63+
}
64+
65+
// Only check files directly in core/
66+
// directories, not in core/subpkg/.
67+
dir := filepath.Dir(rel)
68+
if !strings.HasSuffix(dir, "/core") &&
69+
dir != "core" {
70+
return nil
71+
}
72+
73+
if allowedCoreFiles[d.Name()] {
74+
return nil
75+
}
76+
77+
abs, absErr := filepath.Abs(path)
78+
if absErr != nil {
79+
return absErr
80+
}
81+
82+
violations = append(violations,
83+
abs+": "+d.Name()+
84+
" in core/ (move to subpackage)",
85+
)
86+
87+
return nil
88+
},
89+
)
90+
if walkErr != nil {
91+
t.Fatalf("walk: %v", walkErr)
92+
}
93+
94+
if len(violations) > 0 {
95+
t.Errorf(
96+
"%d core/ structure violations:",
97+
len(violations),
98+
)
99+
}
100+
limit := 30
101+
if len(violations) < limit {
102+
limit = len(violations)
103+
}
104+
for _, v := range violations[:limit] {
105+
t.Error(v)
106+
}
107+
if len(violations) > 30 {
108+
t.Errorf(
109+
"... and %d more",
110+
len(violations)-30,
111+
)
112+
}
113+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package audit
8+
9+
import (
10+
"go/types"
11+
"strings"
12+
"testing"
13+
14+
"golang.org/x/tools/go/packages"
15+
)
16+
17+
// typeExemptPackages lists packages where exported
18+
// types are expected to be used cross-package by
19+
// design (entity, config, proto, etc.).
20+
var typeExemptPackages = map[string]bool{
21+
"entity": true,
22+
"proto": true,
23+
"core": true,
24+
}
25+
26+
// TestCrossPackageTypes flags exported type
27+
// definitions that are used from other packages but
28+
// are not in internal/entity/ or other exempt
29+
// packages. Cross-cutting types should live in
30+
// internal/entity/ for discoverability.
31+
//
32+
// Test files are exempt.
33+
func TestCrossPackageTypes(t *testing.T) {
34+
pkgs := loadPackages(t)
35+
cmdPkgs := loadCmdPackages(t)
36+
allPkgs := make(
37+
[]*packages.Package,
38+
0, len(pkgs)+len(cmdPkgs),
39+
)
40+
allPkgs = append(allPkgs, pkgs...)
41+
allPkgs = append(allPkgs, cmdPkgs...)
42+
43+
// Phase 1: collect all exported type definitions
44+
// outside exempt packages.
45+
type typeDef struct {
46+
pkg string
47+
name string
48+
pos string
49+
}
50+
defs := make(map[string]typeDef) // key: pkgPath.Name
51+
52+
for _, pkg := range pkgs {
53+
// Skip exempt packages.
54+
parts := strings.Split(pkg.PkgPath, "/")
55+
lastPart := parts[len(parts)-1]
56+
if typeExemptPackages[lastPart] {
57+
continue
58+
}
59+
// Skip core/ subpackages (e.g. core/check,
60+
// core/python) — types there serve their
61+
// parent CLI module by design.
62+
if isCoreSubpackage(pkg.PkgPath) {
63+
continue
64+
}
65+
// Skip config/ (types there are config
66+
// structs, not domain types).
67+
if strings.Contains(
68+
pkg.PkgPath, "internal/config/",
69+
) {
70+
continue
71+
}
72+
73+
for ident, obj := range pkg.TypesInfo.Defs {
74+
if obj == nil {
75+
continue
76+
}
77+
_, isTypeName := obj.(*types.TypeName)
78+
if !isTypeName {
79+
continue
80+
}
81+
if !isExported(ident.Name) {
82+
continue
83+
}
84+
pos := pkg.Fset.Position(ident.Pos())
85+
if isTestFile(pos.Filename) {
86+
continue
87+
}
88+
89+
key := obj.Pkg().Path() + "." + obj.Name()
90+
defs[key] = typeDef{
91+
pkg: shortPkg(pkg.PkgPath),
92+
name: obj.Name(),
93+
pos: pos.String(),
94+
}
95+
}
96+
}
97+
98+
// Phase 2: find types used cross-package.
99+
crossPkgUse := make(map[string]string) // key → using pkg
100+
101+
for _, pkg := range allPkgs {
102+
for ident, obj := range pkg.TypesInfo.Uses {
103+
if obj == nil || obj.Pkg() == nil {
104+
continue
105+
}
106+
pos := pkg.Fset.Position(ident.Pos())
107+
if isTestFile(pos.Filename) {
108+
continue
109+
}
110+
111+
key := obj.Pkg().Path() + "." + obj.Name()
112+
if _, defined := defs[key]; !defined {
113+
continue
114+
}
115+
116+
// Cross-package if user's package differs
117+
// from definition's package.
118+
if pkg.PkgPath == obj.Pkg().Path() {
119+
continue
120+
}
121+
// Skip when the consumer is a core
122+
// subpackage — these are internal to
123+
// their CLI module by design.
124+
if isCoreSubpackage(pkg.PkgPath) {
125+
continue
126+
}
127+
// Skip same-module usage. Types shared
128+
// within a module (e.g. mcp/handler →
129+
// mcp/server) are module-internal.
130+
if sameModule(
131+
pkg.PkgPath, obj.Pkg().Path(),
132+
) {
133+
continue
134+
}
135+
crossPkgUse[key] = shortPkg(
136+
pkg.PkgPath,
137+
)
138+
}
139+
}
140+
141+
// Phase 3: report types used cross-package that
142+
// are not in entity/.
143+
var violations []string
144+
for key, usingPkg := range crossPkgUse {
145+
def := defs[key]
146+
violations = append(violations,
147+
def.pos+": type "+def.pkg+"."+
148+
def.name+" used from "+usingPkg+
149+
" (consider entity/)",
150+
)
151+
}
152+
153+
if len(violations) == 0 {
154+
return
155+
}
156+
157+
t.Errorf(
158+
"%d cross-package types outside entity/:",
159+
len(violations),
160+
)
161+
limit := 30
162+
if len(violations) < limit {
163+
limit = len(violations)
164+
}
165+
for _, v := range violations[:limit] {
166+
t.Error(v)
167+
}
168+
if len(violations) > 30 {
169+
t.Errorf(
170+
"... and %d more",
171+
len(violations)-30,
172+
)
173+
}
174+
}
175+
176+
// isCoreSubpackage returns true if pkgPath is a
177+
// subpackage of a core/ directory (e.g.
178+
// ".../cli/doctor/core/check").
179+
func isCoreSubpackage(pkgPath string) bool {
180+
return strings.Contains(pkgPath, "/core/")
181+
}
182+
183+
// domainAliases maps write/ package names to their
184+
// corresponding domain module when the names differ.
185+
var domainAliases = map[string]string{
186+
"resource": "sysinfo",
187+
}
188+
189+
// sameModule returns true if two package paths share
190+
// the same domain. Handles: same module root,
191+
// cli/<X> consuming internal/<X>, err/<X> consumed
192+
// from cli/<X>, and domain aliases.
193+
func sameModule(a, b string) bool {
194+
ma := canonicalModule(moduleRoot(a))
195+
mb := canonicalModule(moduleRoot(b))
196+
if ma == "" || mb == "" {
197+
return false
198+
}
199+
if ma == mb {
200+
return true
201+
}
202+
// cli/* consuming any domain module is the
203+
// standard consumer layer pattern.
204+
if strings.HasPrefix(ma, "cli/") &&
205+
!strings.HasPrefix(mb, "cli/") {
206+
return true
207+
}
208+
if strings.HasPrefix(mb, "cli/") &&
209+
!strings.HasPrefix(ma, "cli/") {
210+
return true
211+
}
212+
// err/<X> consumed from cli/<X> or <X>.
213+
if strings.HasPrefix(ma, "err/") {
214+
base := ma[len("err/"):]
215+
if mb == base ||
216+
mb == "cli/"+base {
217+
return true
218+
}
219+
}
220+
if strings.HasPrefix(mb, "err/") {
221+
base := mb[len("err/"):]
222+
if ma == base ||
223+
ma == "cli/"+base {
224+
return true
225+
}
226+
}
227+
return false
228+
}
229+
230+
// canonicalModule resolves domain aliases.
231+
func canonicalModule(mod string) string {
232+
if alias, ok := domainAliases[mod]; ok {
233+
return alias
234+
}
235+
return mod
236+
}
237+
238+
// moduleRoot extracts the first path segment after
239+
// "internal/" as the module root. For cli/ packages,
240+
// uses the CLI subcommand (e.g. "cli/doctor").
241+
// For write/<X>, uses X to match with internal/<X>.
242+
func moduleRoot(pkgPath string) string {
243+
const prefix = "ctx/internal/"
244+
idx := strings.Index(pkgPath, prefix)
245+
if idx < 0 {
246+
return ""
247+
}
248+
rest := pkgPath[idx+len(prefix):]
249+
250+
// write/<X> → X
251+
if strings.HasPrefix(rest, "write/") {
252+
parts := strings.SplitN(
253+
rest[len("write/"):], "/", 2,
254+
)
255+
return parts[0]
256+
}
257+
258+
// cli/<X> → cli/<X>
259+
if strings.HasPrefix(rest, "cli/") {
260+
parts := strings.SplitN(
261+
rest[len("cli/"):], "/", 2,
262+
)
263+
return "cli/" + parts[0]
264+
}
265+
266+
// err/<X> → err/<X>
267+
if strings.HasPrefix(rest, "err/") {
268+
parts := strings.SplitN(
269+
rest[len("err/"):], "/", 2,
270+
)
271+
return "err/" + parts[0]
272+
}
273+
274+
// Top-level: mcp, trace, notify, sysinfo, etc.
275+
parts := strings.SplitN(rest, "/", 2)
276+
return parts[0]
277+
}

0 commit comments

Comments
 (0)