Skip to content

test(cli): teach the host-anchoring sweep one more hop — an import alias to a literal in a sibling module - #12582

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-12533-resolver-import-alias-hop
Aug 27, 2026
Merged

test(cli): teach the host-anchoring sweep one more hop — an import alias to a literal in a sibling module#12582
os-litant merged 2 commits into
mainfrom
claude/issue-12533-resolver-import-alias-hop

Conversation

@os-litant

@os-litantos-litant commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12533

serve-cluster-host-resolution.test.ts's resolveIdentifier() 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.

⚠️Body placeholders are spelled out rather than written in angle brackets: GitHub's body sanitizer silently ate every angle-bracket placeholder here once, backticks included. AGENTS.md's "GitHub mutates body BYTES" clause, observed again.

Status — resumed after an unrelated termination

The first dev on this card was terminated mid-round by an account-level API limit, after pushing 2f17ecdc6 and opening this PR but before delivering a report. This round re-verified the inherited commit adversarially rather than adopting it, and origin/main was 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_PKG out of serve.ts, plus deleting serve-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.ts and site 8 are all untouched here.

Re-measured this round on origin/main7a25e7d60: serve.ts:548 is still static readonly ORGANIZATIONS_RUNTIME_PKG = '@objectstack/organizations';, a literal — so site 8's subject still exists and it is not a pin without a subject.

⚠️ NO LIVE SITE USES THIS HOP YET — do not read the branch as dead code

The hop is the capability. Its consumer is a follow-up.

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/organizations load 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:

  • it is consulted only where the resolver already returned undefined, at the three points that need a literal — the call-site identifier, const X = ALIAS, and static readonly MEMBER = ALIAS;
  • it returns a literal or undefined, never a different literal;
  • a const X = IDENTIFIER becomes a candidate only when IDENTIFIER is a name this module actually imports;
  • an alias it cannot follow is 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.

⚠️ One measured behaviour change, stated plainly

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:

import { SHARED_RUNTIME_PKG } from '@objectstack/types'; // bare ⇒ unfollowable
{ const pkg = '@objectstack/service-cluster'; } // farther, a literal
{ const pkg = SHARED_RUNTIME_PKG; importFromHost(pkg); } // nearer, an alias

measured on both sides this round:

resolveranswer
before this PR@objectstack/service-cluster — resolved, and judged
after this PRUNRESOLVED

So 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 pkg to a literal from a different block scope — the same phantom this file already carries a comment about, where a const pkg 1160 lines away kept @objectstack/organizations in 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.ts contains 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_PKG at :105, re-measured), read through serve.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 resolveImportedLiteral neutered, exactly 3 of 33 go red:

caseablation
alias → real sibling (tenancy-posture-hints.ts)⭐ RED
alias named directly at the call site⭐ RED
static readonly MEMBER = ALIAS — the rewrite that emptied the sweep⭐ RED
the seven REFUSES / unchanged casesgreen either way

The 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.

⚠️ The live sweep is unchanged in BOTH directions

Re-measured independently this round, not carried over: the swept population was dumped from the origin/main copy of this file and from this copy, both scanning the same serve.ts.

43 load sites · 4 app-declarable · 8 unresolved (identical, both copies)
sha256(population dump) 7e339a64adcf15a7cbb2fafbdf12f42b284207cfcaddf84f80def3b3643efd84
← byte-for-byte equal

APP_DECLARABLE_LOADS on 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_LOADS and UNRESOLVABLE_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's changeset-check has no path-based exemption, so the label is the only mechanism. This diff is one *.test.ts file in packages/cli; nothing is published and no package version moves.

Verification

Heavy work went through the shared verify lock (OS_VERIFY_LOCK_SLOT=issue-12533); the VERDICT line is the authority and every exit code was captured before any pipe.

Suite at ed7556190 (the merged head — dependency closure pnpm --filter '@objectstack/types...' build first):

pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 \
src/commands/serve-cluster-host-resolution.test.ts \
src/commands/zz-probe-base.test.ts src/commands/zz-probe-head.test.ts
Test Files 3 passed (3)
Tests 88 passed (88)
os-verify-lock: VERDICT command-exit 0

Ablation (resolveImportedLiteral forced to undefined, mutation confirmed on disk by a unique-anchor count and a byte delta before running):

 Tests 3 failed | 30 passed (33)
os-verify-lock: VERDICT command-exit 1

⚠️Declared narrowing. The full packages/cli vitest 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 status is 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 on ed7556190 where 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 9 packages/spec families under "THE LAYOUT MOVED" are #12514, already tracked, not refiled.

Gates run locally, exit code captured before any pipe, all green:

check:nul-bytes EXIT=0 "check-nul-bytes: OK (scanned 6977 text file(s))"
check:engine-double-contract EXIT=0
check:where-matcher EXIT=0
check:cross-package-test-inputs EXIT=0
check:test-source-alias EXIT=0
check:objectql-double-limit EXIT=0
node scripts/check-comment-mask-adoption.mjs EXIT=0
node scripts/check-ci-filter-parity.mjs EXIT=0
node scripts/check-cross-package-test-inputs.mjs EXIT=0
node scripts/check-plugin-teardown-shape.mjs EXIT=0

⚠️ One reading correction worth recording: check:comment-mask-adoption first came back exit 254, which is not a red gate — it is ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command not found. That family is spelled node 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) and check: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

…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
@github-actions

github-actionsBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • 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 — 0 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 527e0505d8729ae022121a992b62326035953a82packageMentionDocs.

@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed tests labels Aug 26, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review August 27, 2026 00:18
@os-litant
os-litant added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 1ca763bAug 27, 2026
37 checks passed
@os-litant
os-litant deleted the claude/issue-12533-resolver-import-alias-hop branch August 27, 2026 00:50
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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-litant@claude