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:

consumer reaches the union by asks provenance
validate-sortable-fields.ts direct no
validate-translation-references.ts derived (spread) + direct no

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:

const fields = obj.fields;
if (!fields || typeof fields !== 'object') return null;

if (names.size === 0) return null;

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions