← Back to journal

Preserve Public Entrypoints Without Leaving Compatibility Facades

A hard-cut refactor should move consumers to the new owner, prove the retained contract, and refuse when closure requires a permanent re-export shell.

Evidence basisKodeProof direct-import, entry-surface, and relocation-proof engineering history
DisclosureArchitecture guidance from a private-alpha system; some ecosystems require compatibility periods that must be declared explicitly.
Published by Kalu KodeApply for KodeProof private alpha →

Moving implementation into a new module is easy. Completing the ownership change is harder.

A common refactor leaves the old file in place as a pure re-export:

old/path.ts → export * from new/path.ts

The build stays green and existing imports keep working. The repository now has two apparent owners, a hidden migration that never finishes, and another path every future tool must understand.

For a declared hard cut, that is not completion.

Protect contracts, not obsolete ownership

A public entrypoint may need to remain stable even when implementation moves. That does not always require the old implementation path to remain as a façade.

The change contract should distinguish:

  • a supported package or application entrypoint;
  • an internal module path that consumers must stop using;
  • export names and type/value roles that must remain;
  • generated or external consumers that cannot be rewritten;
  • an intentional compatibility surface with a defined retirement plan.

Without that distinction, every old path becomes permanent by default.

Count consumers before and after

A direct-import migration should enumerate source-backed consumers, including static imports, type-only imports, re-exports, literal dynamic imports, and supported CommonJS edges.

The accepted candidate should account for each one:

  • rewritten directly to the destination;
  • removed by the declared change;
  • retained through an explicitly protected public entrypoint;
  • unresolved because analysis is incomplete.

A missing consumer should block or produce a yellow coverage gap. It should not disappear from the denominator.

Type and value roles matter

TypeScript can use the same exported name in type and value positions. A refactor may preserve a type declaration while dropping the runtime value required by typeof, decorators, or ordinary code.

Verification needs to retain requested and available roles for each binding. “The export name still exists” is weaker than “the consumers still receive the same required namespaces.”

Re-export changes can alter behavior

Live bindings, side-effect-only imports, sibling order, and module cycles mean a re-export is not always a neutral alias.

Adding a new re-export edge can:

  • delay or duplicate initialization;
  • expose an unintended API;
  • create a cycle;
  • change tree-shaking or chunk boundaries;
  • retain the obsolete module as an architectural dependency.

A hard-cut policy can reject new affected re-export edges while allowing a reviewed public entrypoint whose behavior is independently checked.

Some moves should be pinned

Generated consumers, open dynamic resolvers, external package imports, or plugin scanners may make direct closure impossible with available evidence.

In that case the correct result can be “public surface unclosed.” The refactor remains pinned until the operator supplies a complete consumer census, adjusts the public contract, or accepts a declared compatibility phase.

Failing closed admits fewer moves. It also prevents a temporary bridge from becoming invisible permanent architecture.

Transfer should verify the postimage

After rewriting consumers and relocating implementation, the system can verify exact preimages, apply atomically, reread the semantic tree, and compare the accepted candidate digest.

If any write fails or a stale file is discovered, the whole conflict component rolls back. A second application should be an exact no-op.

KodeProof treats direct consumer closure as evidence that ownership actually moved. The desired outcome is one clear implementation path, stable declared entrypoints, and no compatibility sludge left behind simply because it made the first build easier.