Uh oh!
There was an error while loading. Please reload this page.
test(cli): teach the host-anchoring sweep one more hop — an import alias to a literal in a sibling module - #12582
Merged
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): |
…solver-import-alias-hop
os-litant
marked this pull request as ready for review
August 27, 2026 00:18
This was referenced Aug 27, 2026
Uh oh!
There was an error while loading. Please reload this page.
os-litant added a commit
that referenced
this pull request
Aug 27, 2026
… ORGANIZATIONS_RUNTIME_PKG The organizations package spelling is declared twice inside `packages/cli`, and four docblocks explained why with the same reason: the host-anchoring sweep in `serve-cluster-host-resolution.test.ts` resolved the load through `Serve.ORGANIZATIONS_RUNTIME_PKG` and needed a LITERAL in `serve.ts`, or the load dropped out of the swept population silently. That reason died at `1ca763b60` (#12533, PR #12582), which taught `resolveIdentifier()` to follow an import alias into a sibling module of the same package. A true prohibition carrying a false explanation is worse than no note: the next author reads it as arbitrary and steps over it. Comment-only. Each note now records the constraint, the commit that retired it, and what actually survives — the duplication is CHECKED rather than required, and whether to end it is an open maintainer-facing decision (#12579). The ⛔ in `tenancy-posture-hints.ts` against importing `Serve.ORGANIZATIONS_RUNTIME_PKG` into the shared module is untouched: its reason is #12464's coupling ruling, not the sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
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 freeto 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.Status — resumed after an unrelated termination
The first dev on this card was terminated mid-round by an account-level API limit, after pushing
2f17ecdc6and opening this PR but before delivering a report. This round re-verified the inherited commit adversarially rather than adopting it, andorigin/mainwas merged in (ed7556190) — the branch had fallen 25 commits behind and the gate derivation was answering about a stale tree. The merge is clean and the branch's own diff is unchanged: one file, +441/−7.⛔ 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.Re-measured this round on
origin/main7a25e7d60:serve.ts:548is stillstatic readonly ORGANIZATIONS_RUNTIME_PKG = '@objectstack/organizations';, a literal — so site 8's subject still exists and it is not a pin without a subject.⛔ Do not delete the branch as unreachable. The follow-up is #12579, which remains open: the decision about whether to consume the hop, and the rewrite of the two ⛔ notes whose stated reason this hop changes. Deleting the branch before that is decided puts the tree 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;const X = IDENTIFIERbecomes a candidate only whenIDENTIFIERis a name this module actually imports;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.
The claim "strictly additive" is true of the live tree but not of every conceivable source, and the exception is worth naming rather than burying in a table row. For this shape:
measured on both sides this round:
@objectstack/service-cluster— resolved, and judgedUNRESOLVEDSo a load can leave the judged population on a source shape that does not exist today. That is deliberate, and it is the honest answer: the old answer resolves
pkgto a literal from a different block scope — the same phantom this file already carries a comment about, where aconst pkg1160 lines away kept@objectstack/organizationsin the named set on a tree where the real load had already dropped out. ⭐ And the new answer is loud, not silent: an unresolved, undeclared site fails the sweep by name, which is the failure mode the card was filed about.serve.tscontains zeroconst X = IDENTIFIER;and zerostatic readonly X = IDENTIFIER;forms today, so nothing in the tree reaches this. Pinned by the case "never lets an unresolvable alias inherit a farther binding of the same name".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(export const ORGANIZATIONS_RUNTIME_PKGat:105, re-measured), read throughserve.ts's own scan context. If that export is renamed or stops being a literal, it goes red here, naming the hop.Ten cases.⚠️ They are not equally load-bearing, and an ablation was run to say which — with
resolveImportedLiteralneutered, exactly 3 of 33 go red:tenancy-posture-hints.ts)static readonly MEMBER = ALIAS— the rewrite that emptied the sweepThe seven that survive ablation are fence cases: they do not prove the hop works, they constrain how far a future widening may go. That is a real job, but it is a different one, and calling all ten "proof of the hop" would have been the vacuity this card exists to prevent.
Re-measured independently this round, not carried over: the swept population was dumped from the
origin/maincopy of this file and from this copy, both scanning the sameserve.ts.APP_DECLARABLE_LOADSon both:@objectstack/organizations,@objectstack/service-cluster,@objectstack/service-cluster-,@objectstack/service-i18n. Nothing gained, nothing lost.⛔ Not touched
serve.ts·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. Verified mechanically: the diff's only 7 deleted lines are the resolver's own signature and the lines structurally adjacent to it — no named assertion is removed or edited.Changeset
skip-changeset, applied and read back. Measured against the repo's own rule:pr-automation.yml'schangeset-checkhas no path-based exemption, so the label is the only mechanism. This diff is one*.test.tsfile inpackages/cli; nothing is published and no package version moves.Verification
Heavy work went through the shared verify lock (
OS_VERIFY_LOCK_SLOT=issue-12533); theVERDICTline is the authority and every exit code was captured before any pipe.Suite at
ed7556190(the merged head — dependency closurepnpm --filter '@objectstack/types...' buildfirst):Ablation (
resolveImportedLiteralforced toundefined, mutation confirmed on disk by a unique-anchor count and a byte delta before running):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 probe copies. The probes are untracked copies, removed after the round;git statusis clean and the tracked file's blob hash equals its committed blob.Gate families re-derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(exit 0, no paths passed — the script takes its own change set from the merge base). ⭐ Derived twice: once on the pre-merge tree, where the script itself warned "STALE TREE … 25 commit(s) behind", and again oned7556190where that warning is gone. Both runs name the same 15 families — so the staleness the first derivation warned about did not change the answer. The 9packages/specfamilies under "THE LAYOUT MOVED" are #12514, already tracked, not refiled.Gates run locally, exit code captured before any pipe, all green:
check:comment-mask-adoptionfirst came back exit 254, which is not a red gate — it isERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command not found. That family is spellednode scripts/check-comment-mask-adoption.mjs, not a pnpm script; invoked correctly it is green. A not-found exit is NOT MEASURED, never a failure.⛔ 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