Target folder shape for every Go module and for web/src, and how far each one
is from it today. Spec 028 (specs/028-split-big-packages/) applies it with
behavior-preserving moves; until then, use it for new code and in review. Build
and test commands are in AGENTS.md and
contributing.md.
Root files allowed in every Go module: go.mod, go.sum, specs.md,
README.md, Dockerfile (and Dockerfile.*), .testcoverage.yml.
A long-running binary (a Deployment, DaemonSet, sidecar or Job image).
<module>/
├── cmd/main.go thin wiring: flags and env (svcutil), construct, run
├── internal/<concern>/ all behavior, one package per concern
├── specs.md
├── README.md
└── Dockerfile
cmd/: only wiring. A second binary getscmd/<binary>/main.go.internal/<concern>/: the code, grouped by what it does (auth,console,store, …).- Tests sit next to the file they test (
x.go→x_test.go). - No
.gofile at the module root.
A module other modules import, with no binary of its own.
<module>/
├── <name>.go … one flat root package named after the module
├── testdata/ fixtures, if any
└── specs.md
- One package at the root. A subpackage only for a separately importable concern.
- Tests next to the file they test.
A binary people run by hand.
<module>/
├── cmd/<binary>/main.go commands and flag parsing
├── internal/<concern>/ behavior
├── pkg/<name>/ only packages other modules import
└── specs.md
- Tests next to the file they test.
web/src/
├── pages/<Screen>.tsx one file per screen
├── pages/<Screen>/<Section>.tsx sections of a screen over 500 lines
├── routes/ route definitions only, no screen code
├── router/ assembles the route tree
├── components/ parts used by 2+ screens
├── components/ui/ primitives
├── lib/ API client, hooks, helpers, i18n
├── styles/
├── test/ shared test setup
├── api/types.ts generated, never edited by hand
└── types.ts hand-written shared types
pages/<Screen>.tsxholds a screen. When it passes 500 lines it is split into section files underpages/<Screen>/(e.g.pages/<Screen>/Footer.tsx,pages/<Screen>/UpperSection.tsx) andpages/<Screen>.tsxcomposes them.routes/only exposes pages: one route-definition file per page;router/only assembles them (spec 028 OD-7 fixes the file names).- A part moves to
components/once two or more screens use it; until then it stays with its screen. - Tests sit next to the file they test (
X.test.tsx,X_<topic>.test.tsx).
Measured on master at 54d5819 (2026-10-22). "Gap today" is matches or the
move spec 028 makes.
| Module | Kind | Target | Gap today |
|---|---|---|---|
agent |
Service | cmd/main.go + internal/<concern>/ |
matches |
api |
Service | cmd/main.go + internal/<concern>/ |
cmd/ holds 7 non-test files (bootstrap, gateway, management, memlimit, metrics, panel key) → keep wiring in cmd/, move behavior to internal/<concern>/; second binary cmd/messages-export/ matches; internal/handlers/ is one flat package of 146 files (spec 028 splits it) |
audit-syslog-bridge |
Service | cmd/main.go + internal/<concern>/ |
3 files in root package main (main.go 460 lines) → cmd/main.go + internal/<concern>/ |
capture-sidecar |
Service | cmd/main.go + internal/<concern>/ |
matches |
mcp-server |
Service | cmd/main.go + internal/<concern>/ |
root package main (main.go, tools.go, fixadvice.go) → cmd/main.go + internal/<concern>/; internal/kube/ matches |
operator |
Service | cmd/main.go + internal/<concern>/ |
matches; internal/controller/ is one flat package of 150 files (spec 028 splits it); api/v1alpha1/ see §3 |
sentinel |
Service | cmd/main.go + internal/<concern>/ |
main.go 1,017 lines at root → cmd/main.go + internal/<concern>/ |
telemetry-receiver |
Service | cmd/main.go + internal/<concern>/ |
12 source files in root package main → cmd/main.go + internal/<concern>/; web/ and design-export/ assets stay |
tunnel |
Service | cmd/main.go + internal/<concern>/ |
main.go 1,052 and playit_reporter.go 525 lines at root → cmd/main.go + internal/<concern>/; frpc/ (separate build module, not in go.work) stays |
gp-module |
CLI tool | cmd/gp-module/ + internal/<concern>/ |
matches; pkg/ holds the 5 packages api and test/e2e import |
i18n |
Library + CLI | flat root package + cmd/i18n-check/ |
matches (see §3) |
gameaction |
Library | flat root package | matches |
gameproto |
Library | flat root package | matches |
netguard |
Library | flat root package | matches |
svcutil |
Library | flat root package | matches |
telemetryschema |
Library | flat root package | matches |
test/e2e |
Test suite | root package + internal/<game>/ |
matches (see §3) |
web |
Web app | src/pages/ + route definitions in src/routes/ |
no src/pages/ yet: screens live in src/routes/ → move each to src/pages/<Screen>.tsx; screens over 500 lines to split: AdminSettings.tsx 2,478, CreateServer.tsx 1,352, tabs/Mods.tsx 1,291, tabs/settings/Networking.tsx 1,045, ThemeSettings.tsx 886, Users.tsx 825, Servers.tsx 680, tabs/Files.tsx 670, tabs/settings/ShareLinks.tsx 658, Backups.tsx 642, Dashboard.tsx 596 |
test/e2e: a test suite, not a service or library. Tests sit at the root (//go:build e2e) with per-game packages underinternal/<game>/and shared helpers ininternal/fakeoidc/,internal/probe/andinternal/protocol/.operator/api/v1alpha1: CRD types follow the kubebuilder layout thatmake generateandmake manifestsexpect.i18n: a library (root package pluscatalog/,icu/,modlocale/,pack/, each imported on its own) that also ships thei18n-checkCLI undercmd/i18n-check/; it matches both kinds.
Spec 028 adds a folder-shape check to make lint once every module matches this
doc. Until then, review enforces it: new code follows the target shape even in a
module that does not match yet.
Where each large package splits, and the line limit for files other than web
screens, belong to spec 028 (specs/028-split-big-packages/).