Observation filed out of #15325 (the dispatch asked whether anything could have caught either sealing event; ⛔ proposed here as a card, not landed there). Unassigned; for triage.
The same event, twice
Both consumers were predicted by #13123's own body ("ratify the subpath rather than read dist/ paths"), and both were discovered after publish, by the consumer, during an upgrade. Nothing in this repo could have gone red: the deep imports live in other repositories, and every in-repo consumer reaches any file through a relative import, a vitest alias or a paths entry — inside the monorepo nothing is sealed.
What #15325 leaves behind, and what it cannot do
packages/cli/test/published-subpath-hook-body.pin.test.ts packs the package with pnpm pack, unpacks it into a throwaway node_modules outside the workspace, and resolves the ratified specifiers from there. It pins the current exports map exactly (., ./console, ./hook-body, ./package.json), so a subpath removed from @objectstack/cli now goes red in-repo. It cannot see the next package that gains an exports map, and it knows nothing about which specifiers out-of-repo consumers actually use.
Proposal (options, not a decision)
A. A consumer-specifier ledger, held against the exports maps. packages/qa/downstream-contract already exists as "a frozen, representative third-party consumer of @objectstack/spec" whose fixtures a spec change must not break. Extend the idea one level down: a small, hand-curated JSON ledger of the bare specifiers known out-of-repo consumers import (@objectstack/cli/console — cloud objectos-runtime; @objectstack/cli/hook-body — hotcrm action-sandbox; @objectstack/cli/package.json; …), each with the consumer and a date, checked by a gate (or a downstream-contract test) that resolves every entry through the workspace package's exports map with Node's real resolver. A seal that drops a ledgered specifier is red on the PR that seals it, naming the consumer it would break. Shrink-only in the sense that an entry leaves only with a recorded consumer-side migration.
B. check:published-files GATED, one step further. That gate already asserts every publishable package declares an exports map. It could additionally refuse a PR that introduces a map (or removes a subpath from one) unless the changeset for that package is at least minor and carries a BREAKING-style consumer note naming the deep paths that stop resolving — the packaging analogue of the ADR-0087 disposition marker. Mechanical, no ledger; it makes the seal loud rather than predicting who it hits.
C. Both — B makes any seal a declared act; A makes the known consumers a test.
A is the one that would have caught #13662 and #15325 specifically; B is the one that would have made #13123 itself say which doors it closed.
Boundary
⛔ Not a request to widen any exports map or to weaken check:published-files's GATED invariant — sealing was right in both cases (#15325 triage, and the reporter, both say so). The gap is visibility, not policy.
Observation filed out of #15325 (the dispatch asked whether anything could have caught either sealing event; ⛔ proposed here as a card, not landed there). Unassigned; for triage.
The same event, twice
@objectstack/clibehind anexportsmap (feat(cli,plugin-hono-server): declareexportsmaps, and make "a published package declares one" a gate #13123). cloud'sobjectos-runtimedeep-importeddist/utils/console.js⇒ broken ⇒./consoleratified (feat(cli): ratify./consoleas a public subpath export — the out-of-repo consumer #13123 predicted #13662).dist/utils/extract-hook-body.js) ⇒ broken at type level and run time ⇒./hook-bodyratified (17.3.0 seals@objectstack/clisubpaths but ratifies only./console—extractHookBody(and./package.json) have no public entry, and an app's hook-body fidelity harness breaks with no replacement #15325).Both consumers were predicted by #13123's own body ("ratify the subpath rather than read
dist/paths"), and both were discovered after publish, by the consumer, during an upgrade. Nothing in this repo could have gone red: the deep imports live in other repositories, and every in-repo consumer reaches any file through a relative import, a vitest alias or apathsentry — inside the monorepo nothing is sealed.What #15325 leaves behind, and what it cannot do
packages/cli/test/published-subpath-hook-body.pin.test.tspacks the package withpnpm pack, unpacks it into a throwawaynode_modulesoutside the workspace, and resolves the ratified specifiers from there. It pins the current exports map exactly (.,./console,./hook-body,./package.json), so a subpath removed from@objectstack/clinow goes red in-repo. It cannot see the next package that gains anexportsmap, and it knows nothing about which specifiers out-of-repo consumers actually use.Proposal (options, not a decision)
A. A consumer-specifier ledger, held against the exports maps.
packages/qa/downstream-contractalready exists as "a frozen, representative third-party consumer of@objectstack/spec" whose fixtures a spec change must not break. Extend the idea one level down: a small, hand-curated JSON ledger of the bare specifiers known out-of-repo consumers import (@objectstack/cli/console— cloudobjectos-runtime;@objectstack/cli/hook-body— hotcrmaction-sandbox;@objectstack/cli/package.json; …), each with the consumer and a date, checked by a gate (or a downstream-contract test) that resolves every entry through the workspace package'sexportsmap with Node's real resolver. A seal that drops a ledgered specifier is red on the PR that seals it, naming the consumer it would break. Shrink-only in the sense that an entry leaves only with a recorded consumer-side migration.B.
check:published-filesGATED, one step further. That gate already asserts every publishable package declares anexportsmap. It could additionally refuse a PR that introduces a map (or removes a subpath from one) unless the changeset for that package is at leastminorand carries aBREAKING-style consumer note naming the deep paths that stop resolving — the packaging analogue of the ADR-0087 disposition marker. Mechanical, no ledger; it makes the seal loud rather than predicting who it hits.C. Both — B makes any seal a declared act; A makes the known consumers a test.
A is the one that would have caught #13662 and #15325 specifically; B is the one that would have made #13123 itself say which doors it closed.
Boundary
⛔ Not a request to widen any exports map or to weaken
check:published-files's GATED invariant — sealing was right in both cases (#15325 triage, and the reporter, both say so). The gap is visibility, not policy.