test(cli): teach the host-anchoring sweep one more hop — an import alias to a literal in a sibling module - #12582
Draft
os-litant wants to merge 1 commit into
Draft
Conversation
…ias to a literal in a sibling module `serve-cluster-host-resolution.test.ts`'s `resolveIdentifier()` knew two spellings, both confined to `serve.ts` itself: `const X = '<literal>'` and `const X = Serve.MEMBER`. So single-sourcing a package spelling into a module `serve.ts` shares with another reader turned that load from "app-declarable, host-anchored, checked" into "unknowable" — and an unresolved specifier drops OUT of the judged population rather than into it, silently. Add a third hop: follow an import alias to a string literal in a sibling module of the SAME package, consulted at the three points the resolver would otherwise return `undefined` (the call-site identifier, `const X = <alias>`, and `static readonly MEMBER = <alias>`). It can only widen. It runs where the resolver already gave up and returns a literal or `undefined`, never a different literal; a `const X = <identifier>` becomes a candidate only when the name is one this module imports, so an unresolvable nearer binding can never shadow a farther literal and take a site out of the swept population. The fence is the package, not the filesystem: a bare specifier, a relative path that escapes the package, a re-export, a namespace import, a type import, a missing sibling and a non-literal export are each refused, with a case for each. No live site uses the hop yet — that is deliberate, and the consumer is a follow-up. Measured either side of this change, the swept population is byte-for-byte identical: 43 load sites, 4 app-declarable, 8 unresolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12533
serve-cluster-host-resolution.test.ts'sresolveIdentifier()learns one more hop: an import alias followed to a string literal in a sibling module of the same package. Nothing else in the file moves.What was measured first
On
origin/main@9bed0b0fe, every anchor the card and its claim comment handed down was re-derived rather than trusted:resolveIdentifierat:278UNRESOLVABLE_HOST_LOADSat:486serve.ts:548still a literalstatic readonly ORGANIZATIONS_RUNTIME_PKG = '@objectstack/organizations';tenancy-posture-hints.ts:89— "⛔ Do NOT close the gap…"serve-organizations-message-spelling.test.ts:52— "deliberately stays a string LITERAL"The card's premise holds: before this PR the resolver knew
const X = '@objectstack/service-i18n'andconst X = Serve.MEMBER, and knew no spelling that reaches outsideserve.ts.⛔ Scope: the hop only — NOT the refactor it enables
The card proposes the hop and the refactor that consumes it (single-sourcing
ORGANIZATIONS_RUNTIME_PKGout ofserve.ts, plus deletingserve-organizations-message-spelling.test.ts's site 8). Only the hop is here.PR #12532 put that duplication in place deliberately, with the reasoning written at both ends, and the card itself names the trap: #12532 shipped shape A "with the reasoning at both sites so the next author does not 'finish the job' and blind the sweep." Doing the refactor in this diff is that move with a safety net stapled to it — ⛔ a capability and the reversal it licenses do not land together. So
serve.ts,tenancy-posture-hints.tsand site 8 are all untouched here; site 8's subject still exists, so it is not a pin without a subject.This is the unusual part of the change and the reason it is stated in the body, in the resolver's docblock and again on the suite:
⛔ Do not delete the branch as unreachable. The follow-up is #12579 — the decision about whether to consume it, which also has to rewrite the two ⛔ notes whose stated reason this hop changes. Deleting the branch before that is decided puts the tree straight back in the state where single-sourcing a spelling silently empties the sweep, which is how this file lost the
@objectstack/organizationsload once already.The hop, and the property it had to keep
The resolver's docblock commits to one thing: resolving further "strictly WIDENS what the sweep judges; it can never excuse a load." The hop is built so that it cannot:
undefined, at the three points that need a literal — the call-site identifier,const X = ALIAS, andstatic readonly MEMBER = ALIAS;undefined, never a different literal, so nothing that resolves today resolves differently;const X = IDENTIFIERbecomes a candidate only whenIDENTIFIERis a name this module actually imports. Admitting every identifier would let an unresolvable nearer binding shadow a fartherconst X = '@objectstack/service-i18n'and take a site out of the judged population — the one direction this resolver is forbidden to move in;UNRESOLVED, and unresolved-and-undeclared is a loud failure naming file, line, callee and specifier.The fence — one hop, not a module resolver
⛔ Refused, each with a case: a bare (cross-package) specifier, a relative path that escapes the package even when the file is there and readable, a re-export, a namespace import, a type import, a missing sibling, and an export that is not a string literal. Comments in the sibling are blanked before it is read, so a commented-out declaration is prose rather than a spelling.
Anti-vacuity: how a hop with no live site was proven
The strongest anchor available without a consumer: the source is synthetic, but the sibling followed is the real
packages/cli/src/utils/tenancy-posture-hints.ts, read throughserve.ts's own scan context — the module a single-sourcing refactor would actually import from. If that export is renamed or stops being a literal, it goes red here, naming the hop.Ten cases, each of which fails if the hop is removed or loosened:
tenancy-posture-hints.ts)importFromHost(SHARED_PKG)with no local rebindingstatic readonly MEMBER = ALIAStypeimport / missing siblingconst X = '@objectstack/service-i18n',Serve.MEMBER, binding-below-callUNRESOLVED, never the farther literalThe card's own headline is that an unresolved specifier drops out rather than in, so "additive" had to be measured, not argued. The swept population was dumped from the base copy of this file and from this copy, both scanning the same
serve.ts:APP_DECLARABLE_LOADSon both:service-cluster,service-cluster-,service-i18n,organizations. Nothing gained, nothing lost. That is expected and it is the point —serve.tscontains zeroconst X = IDENTIFIER;and zerostatic readonly X = IDENTIFIER;forms today, so there is nothing for the hop to reach yet.⛔ Not touched
serve.ts(held by #12543 this round) ·serve-organizations-message-spelling.test.tssite 8 ·tenancy-posture-hints.ts·UNRESOLVABLE_HOST_LOADSandUNRESOLVABLE_BARE_IMPORTS(those declarations belong to #12540 / #12162) · every named assertion in the vacuity guard — that named half is what caught the loss this card is filed for, and it is extended by nothing and weakened by nothing here.Changeset
skip-changeset, measured against the repo's own rule rather than assumed:pr-automation.yml'schangeset-checkhas no path-based exemption — the label is the only mechanism, andlint.yml:2057states the prescription ("such a PR releases nothing, so by the workflow's own prescription it takes the label"). This diff is one*.test.tsfile inpackages/cli; nothing is published and no package version moves.Verification
All heavy work went through this container's shared verify lock (
OS_VERIFY_LOCK_SLOT=issue-12533); theVERDICTline is the authority, and every exit code below was captured before any pipe.The suite, plus a base-vs-head population probe, at
2f17ecdc6:packages/clivitest suite does not finish inside this container's ~10-minute foreground window, so the run is narrowed to the edited file and its two probe copies. The dependency closure was likewise narrowed topnpm --filter '@objectstack/types...' build—@objectstack/types/nodeis this file's only workspace import andpackages/cli's vitest config does not alias it, so it resolves throughexportstodist/. Everything else is CI's.The two probes are untracked copies generated inside the locked run and removed by its
trap … EXIT INT TERM;git statusafter the round showed only the intended modification.Gate families derived with⚠️ It reported the checkout 4 commits behind
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths passed — the script takes its own change set from the merge base).origin/mainwith 3 of the files it derives from changed in that range, so the family list is a well-formed answer about a slightly older tree; it is reported as measured rather than reconciled. The 9packages/specfamilies it printed under "THE LAYOUT MOVED" are #12514, already tracked — not refiled.turbo.json's@objectstack/cli#testalready declares$TURBO_DEFAULT$, and the one new read this change adds (packages/cli/src/utils/tenancy-posture-hints.ts) is insidepackages/cli— so noinputsentry is owed andcheck:cross-package-test-inputshas nothing new to see.packages/cli/tsconfig.jsonisinclude: ["src"]with noexclude, so*.test.tsreally is inside thetypecheckprogram rather than merely adjacent to it.⛔ Not measured locally, and named rather than skipped:
check:i18n/check:i18n-coverage(need the CLI built) andcheck:type-check-coverage/check:type-check-debt(need the whole workspace built — the ratchet refuses outright on an unbuilt worktree, and that refusal is NOT MEASURED, never a pass). CI owns those.Generated by Claude Code