Skip to content

feat(cli): ratify ./console as a public subpath export — the out-of-repo consumer #13123 predicted - #13662

Merged
hotlong merged 1 commit into
mainfrom
feat/cli-console-subpath-export
Aug 31, 2026
Merged

hotlong merged 1 commit into
mainfrom
feat/cli-console-subpath-export

Conversation

@hotlong

@hotlong hotlong commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What

One deliberate door in the CLI's exports map: "./console" → dist/utils/console.{js,d.ts} — the self-contained static-serve util (resolveConsolePath / hasConsoleDist / createConsoleStaticPlugin + drift-guard helpers; fs/path only, no oclif).

Why

#13123 sealed the CLI behind an exports map and its own body names this exact remedy for an out-of-repo consumer: ratify the subpath as public surface rather than read dist/ paths. That consumer exists — cloud's objectos-runtime node server mounts the Console SPA through this util, formerly via @objectstack/cli/dist/utils/console.js. Under the sealed map that specifier now fails at both layers:

  • types: tsup DTS refuses with TS2307 (measured on cloud's pin-bump build);
  • runtime: ERR_PACKAGE_PATH_NOT_EXPORTED, which the consumer's defensive catch turns into a silent API-only boot — a working deployment loses its console with nothing but a warn line.

Duplicating the util in cloud was rejected: it is 654 lines carrying the objectui-pin drift guards (#7752) and the mount-decision policy — a fork would drift by construction.

Scope

The surface grows by ONE named door, not back to "everything under dist/". check:published-files' GATED invariant stays green (the map names real entries); census floor untouched. Changeset: @objectstack/cli minor.

Gates at this head

Full 71-package build, full-repo lint, check:published-files, check-empty-changeset — all exit 0. Cloud-side consumer verified against a locally patched checkout of this exact map: runtime resolution returns all three functions, objectos-runtime DTS builds green, and cloud's five harness suites pass.

🤖 Generated with Claude Code

…-repo consumer #13123 predicted

#13123 sealed the CLI behind an `exports` map, and its own body names the
remedy for a consumer that was reading `dist/` paths: ratify the subpath as
public surface rather than keep the deep read. That consumer exists — cloud's
`objectos-runtime` node server mounts the Console SPA through the CLI's
self-contained static-serve util (`resolveConsolePath` / `hasConsoleDist` /
`createConsoleStaticPlugin`, fs/path only, no oclif) via what was
`@objectstack/cli/dist/utils/console.js`. Under the sealed map that specifier
throws ERR_PACKAGE_PATH_NOT_EXPORTED at BOTH layers: tsup's DTS build refuses
(TS2307), and at runtime the consumer's defensive catch silently degrades
every boot to API-only — a working deployment loses its console with nothing
but a warn line.

`./console` maps to exactly the module that was being consumed
(`dist/utils/console.{js,d.ts}`), so the surface grows by one deliberate
door, not back to "everything under dist/". check:published-files' GATED
invariant stays green (the map still names real entries), and the census
floor is untouched.

Gates run at this head: full 71-package build, full-repo lint,
check:published-files, check-empty-changeset — all exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hotlong
hotlong enabled auto-merge August 31, 2026 05:38
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/cli/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/package.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c45d8e6b4ef915004f091e03134a9314587df13d → packageMentionDocs.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Aug 31, 2026
@hotlong
hotlong added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 8c82289 Aug 31, 2026
34 checks passed
@hotlong
hotlong deleted the feat/cli-console-subpath-export branch August 31, 2026 06:21
baozhoutao pushed a commit that referenced this pull request Sep 5, 2026
…e exports maps

An `exports` map is a PACKAGING contract, and inside this monorepo nothing is
sealed: every in-repo consumer reaches any file through a relative import, a
vitest alias or a `paths` entry. So the repo could not observe a sealing
regression at all, by construction — twice the seal 17.3.0 introduced (#13123)
was found after publish, by the consumer, during an upgrade (#13662, #15325).

`consumer-specifiers.ledger.json` imports that knowledge from outside: the bare
specifiers named out-of-repo consumers import, each with the consumer, a date
and the card that ratified it. The suite packs each ledgered package with
`pnpm pack`, unpacks it into a throwaway `node_modules` outside the workspace,
and resolves every specifier from there with Node's real resolver under both
the `require` and `import` conditions — failing with a message that names the
specifier AND the consumer it breaks.

Option A of #15589 only. Option B (a changeset-gated seal declaration in
`check:published-files`) is deliberately not here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
os-steve pushed a commit that referenced this pull request Sep 5, 2026
…to an already-published package

GATED asserts an `exports` map exists; it is silent on the TRANSITION, and the
transition is where the damage is. 17.3.0 added a map to `@objectstack/cli`,
which had published without one, and every deep path a consumer reached through
`dist/` stopped resolving at once -- found after publish, by the consumer,
twice (#13662, #15325).

ANNOUNCED is the second half of #15589, ruled B1 on #15715: a base-vs-HEAD
comparison, the same shape `check-adr-0087-registration --base` already uses,
and deliberately not `git log -S` archaeology. A `package.json` absent at the
merge base -- or present but private/unnamed there -- is born-sealed and passes,
because it has no published predecessor whose consumers could be deep-importing
it. Measured when #15715 was filed: 69 publishable packages declare a map from
51 introducing commits, but 56 were born with it and only 13 retrofitted, from
7 commits. Gating all 51 would demand a consumer note 44 times from packages
that had no consumers, every one of them on a new-package PR -- and a gate
answered by boilerplate 44 times out of 51 has stopped being read by the 7th
time it matters.

What the announcement must say is checked against the head map rather than for
a form of words: a `minor`-or-greater changeset on the package whose body names
a deep specifier the new map does NOT resolve. Naming a path that still
resolves tells a consumer nothing and does not satisfy it.

Absence is never a pass (#4690). Every way the base read can fail -- no
`origin/main`, no merge base, an unreadable `.changeset/` -- makes every package
look born-sealed and the clause vacuously green, so each is a refusal that names
itself, and the read carries its own census control (BASE_READ_FLOOR) exactly as
GATED carries EXPORTS_CENSUS_FLOOR. In CI the base comes from `Lint & Repo
Gates` checking out at `fetch-depth: 0`.

Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…hed exports maps (objectstack-ai#15717)

* wip(qa): inherited draft of the consumer-specifier ledger

Committed as-is from a dead session's worktree, before verification, so a
container restart cannot lose it. Verified and rewritten in the commits that
follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk

* test(qa): ledger the out-of-repo consumer specifiers, held against the exports maps

An `exports` map is a PACKAGING contract, and inside this monorepo nothing is
sealed: every in-repo consumer reaches any file through a relative import, a
vitest alias or a `paths` entry. So the repo could not observe a sealing
regression at all, by construction — twice the seal 17.3.0 introduced (objectstack-ai#13123)
was found after publish, by the consumer, during an upgrade (objectstack-ai#13662, objectstack-ai#15325).

`consumer-specifiers.ledger.json` imports that knowledge from outside: the bare
specifiers named out-of-repo consumers import, each with the consumer, a date
and the card that ratified it. The suite packs each ledgered package with
`pnpm pack`, unpacks it into a throwaway `node_modules` outside the workspace,
and resolves every specifier from there with Node's real resolver under both
the `require` and `import` conditions — failing with a message that names the
specifier AND the consumer it breaks.

Option A of objectstack-ai#15589 only. Option B (a changeset-gated seal declaration in
`check:published-files`) is deliberately not here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk

* test(qa): shrink SEALED_TODAY to empty — objectstack-ai#15611 landed and opened both doors

The set-equality assertion did exactly what its header said it would. objectstack-ai#15611
ratified `./hook-body` and `./package.json` and merged while this branch was in
flight; merging main turned the suite red naming the two lines to delete. They
are deleted, so the ledger of losses has reached its floor: every specifier a
named out-of-repo consumer imports now resolves from a packed tarball.

The empty table is KEPT rather than removed. "Every ledgered specifier resolves"
is the contract; "the sealed set is exactly empty" is the ratchet — the line an
author has to edit, past a ⛔, to make a red green by recording a break instead
of repairing it.

Also separates two diagnoses the exports map cannot tell apart on its own: a
specifier that fails with ERR_PACKAGE_PATH_NOT_EXPORTED is SEALED (the subject
of this file), while one failing MODULE_NOT_FOUND is a declared door leading
nowhere — the package is unbuilt, or `files` does not ship the path `exports`
names. Now that all three doors are open, an unbuilt tree would otherwise have
read as "hotcrm is broken".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…onto an already-published package (objectstack-ai#15715) (objectstack-ai#16082)

* feat(devx): check:published-files gates an exports map retrofitted onto an already-published package

GATED asserts an `exports` map exists; it is silent on the TRANSITION, and the
transition is where the damage is. 17.3.0 added a map to `@objectstack/cli`,
which had published without one, and every deep path a consumer reached through
`dist/` stopped resolving at once -- found after publish, by the consumer,
twice (objectstack-ai#13662, objectstack-ai#15325).

ANNOUNCED is the second half of objectstack-ai#15589, ruled B1 on objectstack-ai#15715: a base-vs-HEAD
comparison, the same shape `check-adr-0087-registration --base` already uses,
and deliberately not `git log -S` archaeology. A `package.json` absent at the
merge base -- or present but private/unnamed there -- is born-sealed and passes,
because it has no published predecessor whose consumers could be deep-importing
it. Measured when objectstack-ai#15715 was filed: 69 publishable packages declare a map from
51 introducing commits, but 56 were born with it and only 13 retrofitted, from
7 commits. Gating all 51 would demand a consumer note 44 times from packages
that had no consumers, every one of them on a new-package PR -- and a gate
answered by boilerplate 44 times out of 51 has stopped being read by the 7th
time it matters.

What the announcement must say is checked against the head map rather than for
a form of words: a `minor`-or-greater changeset on the package whose body names
a deep specifier the new map does NOT resolve. Naming a path that still
resolves tells a consumer nothing and does not satisfy it.

Absence is never a pass (objectstack-ai#4690). Every way the base read can fail -- no
`origin/main`, no merge base, an unreadable `.changeset/` -- makes every package
look born-sealed and the clause vacuously green, so each is a refusal that names
itself, and the read carries its own census control (BASE_READ_FLOOR) exactly as
GATED carries EXPORTS_CENSUS_FLOOR. In CI the base comes from `Lint & Repo
Gates` checking out at `fetch-depth: 0`.

Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
Co-authored-by: Claude <noreply@anthropic.com>

* fix(devx): only the changesets a change introduces can announce what it narrows

Measured while ablating the clause: a real `./console` subpath removal went
GREEN because an unrelated changeset already on main contained the string
`./console` in a sentence about a different release. The pending stock is ~1300
files of prose about these same packages, so "some changeset mentions this path"
is satisfied by accident constantly -- the boilerplate-answered gate this clause
exists to avoid, reached from the other direction.

The subject is now the diff: a changeset absent at the merge base, or one whose
text differs from its base copy. The removal half also reads the changeset BODY
rather than the whole file, since the frontmatter names the package on every
changeset and would otherwise let the bump line answer the note.

Nothing in the file is exported any more either. `check:entry-guard`'s second
rule is that a `scripts/**` file exporting a binding can be imported for it,
whereupon this gate's top-level dispatch -- and its `process.exit` -- runs inside
the importer; the self-test is in the same module and needs no exports.

Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xs tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant