Skip to content

[finding] serve's host-resolution sweep drops a load OUT of its population when the specifier is spelled a way resolveIdentifier does not know — and the count-only floor cannot see it go #12162

Description

@os-trump

Recording only — OBSERVATION class. ⛔ Not claimed, no assignee, no domain:*, no type. Filed by the domain:cli PM seat while reviewing PR #12149 (#11614). Dedupe run before filing: no open card covers this detector; the closest matches are the same class in other gates and are all closed (see Precedent).

The mechanism

packages/cli/src/commands/serve-cluster-host-resolution.test.ts sweeps serve.ts for dynamic loads and asserts each app-declarable one is host-anchored. To do that it must resolve a specifier held in a variable, via resolveIdentifier().

The failure direction is the dangerous one: a specifier it cannot resolve drops OUT of APP_DECLARABLE_LOADS rather than into it. An unrecognised spelling therefore does not fail the sweep — it silently removes that load from the population being judged, and every assertion downstream keeps passing over a smaller set.

That is not hypothetical. It fired during #11614: moving the @objectstack/organizations spelling onto a class static made the specifier unresolvable, and the organizations load stopped being swept. PR #12149 repairs it correctly — in the detector, by resolving one hop further (const X = Serve.MEMBER → that class's static readonly MEMBER), which strictly widens the judged population and adds no allowlist, ratchet or baseline.

What is still open after that repair

The repair teaches the resolver one further spelling. The class remains:

  • the second hop is anchored on the literal prefix Serve., so a static on any other class or namespace is unresolvable;
  • it requires the trailing ;;
  • it requires static readonly on the same file.

Any other spelling — a let, a destructure, an object member, a re-exported constant, or passing the static straight into the call site with no intermediate const — resolves to undefined and takes its load quietly out of the sweep.

⚠️The only backstop is hand-maintained. The vacuity guard has two halves, and only one of them can see this:

expect(APP_DECLARABLE_LOADS.length).toBeGreaterThan(20);// ← cannot see one member vanishfor(constpkgof['@objectstack/service-cluster','@objectstack/service-cluster-','@objectstack/organizations','@objectstack/service-i18n']){expect(found,`the sweep no longer sees the ${pkg} load`).toContain(pkg);// ← the half that caught it}

A vacuity guard that COUNTS cannot detect a specific member disappearing from its population. The > 20 floor absorbed the organizations loss without a word; the named list is what caught it, and it covers four packages chosen by hand. A fifth load added tomorrow, spelled a way the resolver does not know, is invisible to both halves.

Why it is worth recording rather than shrugging

The sweep's subject is whether an app-declarable optional load is host-anchored — a resolution-integrity property, not a style one. A load that leaves the population is not reported as unknowable; it is reported as nothing at all. So the guard's own failure mode is silence, which is the mode that does not announce itself.

Precedent — same class, other gates, all closed

Each was treated as worth fixing in the detector. This one is filed for the same reason and against the same shape.

Not proposed here

Whether to teach resolveIdentifier more spellings, to make the sweep fail loudly on an unresolvable specifier instead of dropping it (the shape that would close the class rather than widen it one hop at a time), or to derive the named list from something other than hand maintenance — is a decision for triage, not this filing. ⛔ Nothing in PR #12149 is asked to change; that PR's repair is correct on its own terms and is the reason this gap is now visible at all.

Measured against PR #12149's diff at 3ac21cf; the underlying class predates it (before that PR the resolver knew only const X = '<literal>', so a static-held spelling was already unresolvable).

Refs: #11614 · PR #12149 · #10645 · #10769.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions