Skip to content

the SORT axis reads the blanket SYSTEM_FIELDS union but never asks the #8116 provenance question, while the SEARCH axis on the identical index does #10474

Description

@os-zhuang

Found while building the #8999 consumer census (PR on claude/issue-8999-system-fields-spread-sweep). ⛔ Filed, not fixed — out of that card's scope, which is the census instrument.

What the census turned up

The census enumerates every consumer of the blanket SYSTEM_FIELDS union by module graph and records, per consumer, whether it asks the #8116 provenance question (unprovisionedInjectedColumnsFor / indexUnprovisionedAnchors / unprovisionedAnchorCause). Two of the ten rule-file consumers reference none of them:

consumerreaches the union byasks provenance
validate-sortable-fields.tsdirectno
validate-translation-references.tsderived (spread) + directno

Both are recorded in the census ledger with a reason rather than silently exempted, so this issue is about confirming or overturning those two rulings — not about a test that is failing.

1. validate-sortable-fields.ts — the one that looks real

It landed 2026-08-17 (#9314), after the #8996 sweep that wired the other write/read sites, so it was never in any sweep's population.

packages/lint/src/validate-sortable-fields.ts:310

if(SYSTEM_FIELDS.has(head))continue;

The rule's header records skip ② as "an object that declares no field map at all — external objects and datasource-introspected schemas whose columns resolve at runtime", which reads as though an ADR-0015 external object never reaches the union branch. It is not what the code keys on. The index it consults is the shared indexObjectSearchTargets (packages/lint/src/validate-searchable-fields.ts:312), whose declaredFieldTarget returns null on exactly one condition:

constfields=obj.fields;if(!fields||typeoffields!=='object')returnnull;if(names.size===0)returnnull;

Nothing there tests external. So an external object that does declare a mapped field map (the shape #7859 is about — the author vouches for remote columns through the binding's columnMap) is indexed normally, reaches line 310, and a sort: [{ field: 'created_at' }] over it is skipped in silence.

That is the asymmetry: on that same object, the SEARCH axis warns — validate-searchable-fields was wired for exactly this by #8404, and it passes indexUnprovisionedAnchors into the shared checkSearchableFieldList core. The FILTER and page-binding axes warn too (#8340). SORT is the one axis that consults the union and says nothing, and the runtime consequence is the one #8116 describes: an ORDER BY over an anchor with no storage, HTTP 200 and a silently wrong ordering rather than an error.

⚠️ Not verified end-to-end: I did not construct a running external-object stack and observe the missing finding. What is verified is the code path — declaredFieldTarget does not exclude external objects, and the rule references no provenance helper. Worth a triage read before anyone writes the fix.

2. validate-translation-references.ts — recorded as deliberately not asking

packages/lint/src/validate-translation-references.ts:177 spreads the union into its own rule-local IMPLICIT_FIELDS, and has done since before #8340. It is a spread consumer that no sweep ever listed, which is its own small data point for #8999.

My ruling in the ledger is that it correctly asks nothing: a translation bundle supplies a label for a column and never reads the value, so "this anchor has no storage behind it" says nothing about whether the label resolves. The #8116 warning is about predicates and pointers over the value. Recorded here so a second pair of eyes can overturn it rather than inherit it.

Why this is filed rather than fixed

The #8999 card's deliverable is the census instrument; wiring a provenance warning into the SORT axis is a rule-shape change with its own message wording, its own tests, and a judgement about the dotted-path posture the rule deliberately shares with the FILTER and PROJECTION axes (#4256 / #7532 / #7589). That is not a rider on a test-only PR.

⚠️ One consequence for triage: #8999 was held on the argument that after #8996 the family had "no known remaining hole". That premise no longer holds literally — though neither row above is a proven defect yet, which is what this issue is for.

Related: #8999 (the census) · #8996 / #8663 (the sweep that wired the others) · #8404 (the SEARCH axis wiring this mirrors) · #8340 · #8116 (the provenance question) · #9314 (where the SORT axis landed) · #7859

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions