Skip to content

service-package answers "no such package" / "no packages installed" over a driver it never queried — its own normalizeRows maps a non-answering seam onto zero rows #10965

Description

@os-elon

Found while fixing #10789 (PR #10964), by sweeping the repo for the same conflation class. Not fixed there — different package, different blast radius, and the consequence here is a PRODUCT answer rather than a reported status, so it wants its own triage.

The defect

packages/services/service-package/src/index.ts carries its own local
normalizeRows (:177-181), the same three-dialect flattener
metadata-protocol exports:

functionnormalizeRows(result: any): any[]{if(Array.isArray(result))returnresult;if(result&&Array.isArray(result.rows))returnresult.rows;return[];}

Its two consumers both run raw SQL through the engine seam and then read
emptiness as a fact about the data:

  • get(packageId, version) (:290) — normalizeRows(result), then
    if (rows.length === 0) return null;"this package is not installed".
  • list() (:324) — normalizeRows(result).map(...)"no packages are installed".

A seam that cannot answer returns null rather than throwing —
InMemoryDriver.execute() logs Raw execution not supported in InMemory driver
and returns null, which is the shape pinned on a real booted driver by
packages/cli/src/commands/migrate/duplicates.null-seam.test.ts (#10677). Both
call sites therefore flatten it to [] and report an absence they never
measured.

This is the third instance of one class: a seam that cannot answer is absent,
not empty.
#10677 / PR #10788 fixed it for os migrate duplicates, #10789 /
PR #10964 for backfillSeedTenancy. Both landed the same consumer-side shape —
judge the seam by whether it returns a RESULT SET, not by whether execute
exists.

Why this one may be worse than its two siblings

The other two report a status an operator reads. These return product
values
that callers act on: get() returning null is indistinguishable from
"not installed", and list() returning [] from "nothing installed". Whether
anything downstream then writes on that reading (a re-install, a hydration
skip) is exactly what triage should establish — I did not measure it, and it is
the difference between a cosmetic report and data loss.

Not measured here

  • Which hosts actually reach these two methods on a non-answering seam.
    service-package is a devDependency of packages/runtime and its
    sys_packages rehydration path is driven by app-plugin.disabled-seed.test.ts
    against a real driver. Whether the DevPlugin zero-install stack (a real
    InMemoryDriver, plugin-dev/src/dev-plugin.ts:473-482) reaches
    get()/list() at boot is unverified — I only established the conflation by
    reading, not by booting.
  • The mongodb driver, for the same reason it is out of scope on the two
    sibling cards: not loaded, not measured.
  • Whether the right fix is the shared predicate or a local one. Note that
    metadata-protocol deliberately does not publish its isResultSet from
    the package index (no consumer outside its own module, and its index comment
    names a test-only export as the shape to catch before it ships), and the CLI
    keeps its own copy — so a third consumer arriving is the first real evidence
    that unifying them is worth doing, which is itself a decision rather than a
    drive-by.

Refs

#10677 · PR #10788 · #10789 · PR #10964

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions