4.2 KiB
OpenSpec Change: barrel-explicit-exports
- Change ID: barrel-explicit-exports
- Scope: cross-cutting — every package under
packages/*/ - Status: PROPOSED
- Date: 2026-05-03
- Reference:
openspec/AGENTS.md§6a (Barrel exports — explicit named, notexport *)
Summary
Replace export * from "./<feature>.js" with explicit named re-exports in every per-feature <feature>/index.ts barrel across the monorepo. The package-root src/index.ts files are out of scope for this change — those keep export * for now and may be migrated to export * as <name> from namespace exports in a future change (also permitted under §6a). Generated selector indexes (*.select/index.ts) are also out of scope, since they are the one place §6a explicitly allows transparent forwarding.
The motivation is direct from §6a: the public API of a feature module must be the contract the barrel declares, not whatever the source file happens to export today. export * silently leaks every new symbol added to the source, which (a) defeats reviewability, (b) makes accidental private-helper exposure trivial, and (c) blurs the documented surface area against the actual one. Tree-shaking is unaffected — "sideEffects": false rollup/esbuild treat both forms identically.
Scope
In: 536 distinct <feature>/index.ts files across 70 packages, totalling ~875 export * lines (the remaining ~37 of the 912 total live in package roots / generated selectors / impl files).
Out:
- 21 package-root
src/index.tsfiles (deferred to a follow-up change). - 14
*.select/index.tsgenerated selector forwarders (allowed by §6a). - 11 non-index impl files using
export *(e.g.aes-256-gcm.tsforwarding to its.select/directory) — these are forwarder shims for the selector mechanism, not feature barrels. - 1
.d.tsthird-party type stub.
Why one cross-cutting change instead of one per package
This is a single mechanical rule application, not a design decision per package. The rule is already canonicalised in openspec/AGENTS.md §6a; there is nothing to negotiate per package. Splitting into 70 micro-proposals would inflate process overhead without changing what gets shipped or how it is reviewed. The per-package verification gate (§13: lint typecheck test + 100% coverage, plus dependent sweep on foundation packages) is preserved — the change is implemented one package at a time, gates run per package, and only after all 70 packages are green is the change archived.
Behavior change
None. Every symbol that was reachable through export * remains reachable, with the same name and same type. Consumers continue to import via import { X } from "@suaveplan/<pkg>/<feature>/<feature>" exactly as before. The diff is barrel-internal: the barrel now lists the symbols by name instead of forwarding the whole module.
The one observable difference is what does NOT happen any more — adding a new export to <feature>.ts will not auto-leak through the barrel. This is the intended invariant.
Verification per package
For every package touched:
bunx turbo lint typecheck test --filter=@suaveplan/<pkg>— green.bunx turbo test:coverage --filter=@suaveplan/<pkg>— 100/100/100/100.- Foundation packages (
error,events,logger,validation, pluscrypto-utilssince several primitive packages depend on it): alsobunx turbo test --filter=...@suaveplan/<pkg>to catch dependent regressions. - Banned-pattern scan and missing-
.mdscan re-run to confirm no incidental regressions.
Impact
Affected dependents
Cross-cutting; every package in packages/*/ is affected. Foundation packages enumerate their dependent sets via bunx turbo ls --filter=...@suaveplan/<pkg> per the §13 step 10 / §14a contract — those sweeps run per foundation package as it ships. For non-foundation packages, the per-package gate in step 1 above is sufficient (no dependents on a leaf package exist by definition).
Migration
None for consumers. Internal-only refactor.
Spec delta
openspec/specs/monorepo-structure/spec.md gains a Barrel export form requirement that codifies §6a as an enforceable spec scenario (so future packages cannot regress).