Problem
opensaas generate seeds an Extension contract space under migrations/<spaceId>/ for every pack declared in db.extensions (#1135, ADR-0065). The seeding tracks the declaration in the grow direction only.
Dropping a pack from db.extensions rewrites the other three emissions correctly — prisma.config.ts goes to extensions: [], contract.ts to extensions: {} — but leaves migrations/<pack>/** on disk untouched. Because nothing was rewritten, CI's generate-determinism gate stays green, and generate exits 0. The failure surfaces later, out of the toolchain, on the next db init / db update:
Orphan contract-space directory migrations/<id>/ for an extension not in extensions; remove the directory or re-add the extension.
(kind: "orphanSpaceDir" — the mirror of the declaredButUnmigrated check ADR-0065 records.) The user's remedy today is to delete the directory by hand.
This was raised in review of #1188 and deliberately deferred: pruning is riskier than it looks, so it needed its own ticket rather than a rushed implementation.
Why this is not a one-liner
migrations/ is the project's own directory, not a framework-owned one. A directory under it may be app-authored — a hand-written migration space, or anything a user put there. There is no manifest of what the seeder created, so "not in db.extensions" is not sufficient grounds to delete a directory.
Safe pruning needs a positive identification that a given directory is a seeder-created pack space before anything is removed. The most likely marker is the space's own refs/head.json (written by runContractSpaceSeedPhase, and carrying the descriptor's hash + invariants), possibly combined with the snapshot store entry it points at. Any design has to answer:
- What exactly identifies a directory as seeder-created, and how does that survive a toolchain rc bump that changes the marker's shape?
- What happens to the content-addressed snapshots under
migrations/snapshots/<hash>/ once the last space referencing them is gone — pruned, or left (they are shared and cheap)?
- Does a prune ever run without an explicit signal, given
generate runs on every build? An opt-out, a prompt, or a report-only default are all plausible.
- Refuse-and-report is a legitimate outcome:
generate naming the orphan and telling the user to remove it is strictly better than today's silence, and may be the whole fix.
Acceptance
- Removing a pack from
db.extensions and re-running generate no longer leaves the project in a state where generate succeeds and db update refuses.
- Whatever the mechanism (prune or refuse-and-report), a directory under
migrations/ that the seeder did not create is never removed, and there is a test proving it.
- The
Known limits block on seedExtensionContractSpaces (packages/cli/src/generator/extension-spaces.ts) is updated when this lands.
References
Problem
opensaas generateseeds an Extension contract space undermigrations/<spaceId>/for every pack declared indb.extensions(#1135, ADR-0065). The seeding tracks the declaration in the grow direction only.Dropping a pack from
db.extensionsrewrites the other three emissions correctly —prisma.config.tsgoes toextensions: [],contract.tstoextensions: {}— but leavesmigrations/<pack>/**on disk untouched. Because nothing was rewritten, CI's generate-determinism gate stays green, andgenerateexits 0. The failure surfaces later, out of the toolchain, on the nextdb init/db update:(
kind: "orphanSpaceDir"— the mirror of thedeclaredButUnmigratedcheck ADR-0065 records.) The user's remedy today is to delete the directory by hand.This was raised in review of #1188 and deliberately deferred: pruning is riskier than it looks, so it needed its own ticket rather than a rushed implementation.
Why this is not a one-liner
migrations/is the project's own directory, not a framework-owned one. A directory under it may be app-authored — a hand-written migration space, or anything a user put there. There is no manifest of what the seeder created, so "not indb.extensions" is not sufficient grounds to delete a directory.Safe pruning needs a positive identification that a given directory is a seeder-created pack space before anything is removed. The most likely marker is the space's own
refs/head.json(written byrunContractSpaceSeedPhase, and carrying the descriptor'shash+invariants), possibly combined with the snapshot store entry it points at. Any design has to answer:migrations/snapshots/<hash>/once the last space referencing them is gone — pruned, or left (they are shared and cheap)?generateruns on every build? An opt-out, a prompt, or a report-only default are all plausible.generatenaming the orphan and telling the user to remove it is strictly better than today's silence, and may be the whole fix.Acceptance
db.extensionsand re-runninggenerateno longer leaves the project in a state wheregeneratesucceeds anddb updaterefuses.migrations/that the seeder did not create is never removed, and there is a test proving it.Known limitsblock onseedExtensionContractSpaces(packages/cli/src/generator/extension-spaces.ts) is updated when this lands.References