Epic 1 — Module convention & the public-API boundary check
Define what a module is — a folder under app/ with a public index.ts and a routes.ts — and build the static check that a module may only import another module's index.ts. This is the foundation everything else assumes. Specified in Modules & boundaries.
Goal
The convention is documented and the build fails with file:line when one module deep-imports another's internals — with no DI, no decorators, no runtime cost.
Definition of done
- A written spec for the module convention:
app/<name>/index.ts(public API),routes.ts(HTTP surface), everything else private. - A build-time check that classifies each import: core/npm/same-module (allow), another module's
index.ts(allow), any deeper path into another module (error). - Errors carry
file:lineand a fix hint ("reach it through../billingor export it from its index"). - Correct resolution of relative paths and configured path aliases; re-export barrels don't create false negatives.
- Zero runtime footprint — the check runs at build only.
The convention
Decision to settle here: the module root (app/* by default) and whether nested feature trees are allowed. Documented as a draft spec under framework/draft.
app/
billing/
index.ts # public API — the only legal cross-module import target
routes.ts # export const routes = { ... } (Bun.serve shape)
invoice.service.ts # private
_platform/
index.ts # shared infra (db, scheduler) via its own public API
The boundary check
New: a build pass (run inside, or alongside, the discovery macro) that reads import specifiers and applies one rule. No metadata, no annotations — derived from folder layout + the import graph.
// pseudo-rule
for (const imp of file.imports) {
const target = resolve(imp, file);
const owner = moduleOf(target); // which app/<name>/ owns it, if any
if (owner && owner !== moduleOf(file) && !isIndexOf(target, owner))
error(`${file}:${imp.line} — deep import into module '${owner}'.`);
}
Verify
bun --filter patties test boundary # allow public-index import; reject deep import; aliases resolve