Repository navigation
Description
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.
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.
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
os-zhuang commentedon Aug 21, 2026
Triage:
pm:queue,Task,domain:devx(packages/lintrule-shape work). Scope = the card's two ledger rulings: ① verify the external-object SORT path end-to-end first (premise-first — the card is explicit that the code path is verified but the missing finding was never observed on a running stack), then wire provenance into the SORT axis mirroring #8404's SEARCH wiring, sharing the dotted-path posture with FILTER/PROJECTION; ② translation-references: confirm-or-overturn with fresh eyes, default is the ledger's "labels never read values" ruling.Flow-on for the blocked/hold machinery: this card falsifies the "no known remaining hole after #8996" premise that #8999's hold rested on — noted in this round's triage brief so the hold's re-verification picks it up. Size/model: M, default judgment tier.
Generated by Claude Code
claude commentedon Aug 21, 2026
Claim — PM dispatch (devx seat)
c970724d-303c-5614-9d20-a3f92205cfadclaude/issue-10474-sort-axis-provenanceTaking triage's scoping: ① verify the external-object SORT path end-to-end first — the card
is explicit that the code path is verified but the missing finding was never observed on a
running stack — then ② wire provenance into the SORT axis mirroring #8404's SEARCH wiring,
sharing the dotted-path posture with FILTER/PROJECTION, and rule on the
validate-translation-references.tsledger entry.census rulings, not about a failing test. If ① shows the external-object SORT path does not in
fact reach the union branch the way the census assumes, that overturns the ruling and the right
outcome is a report saying so — not a wiring change built on an unverified premise.
packages/lint, #10653) is enqueued and touchespackages/lint/src/index.tsamong others. This card's files —validate-sortable-fields.tsandvalidate-translation-references.ts— are disjoint from it,but if the dev needs to add an export,
index.tsis the one shared file and #10697 should beallowed to land first.
Generated by Claude Code
feat(lint): ask the #8116 provenance question on the SORT axis (#10474)
claude commentedon Aug 21, 2026
{ "issue": 10474, "status": "done", "branch": "claude/issue-10474-sort-axis-provenance", "pr": "https://github.com/objectstack-ai/objectstack/pull/10745", "premise_still_valid": true, "summary": "Step 1 CONFIRMED the SORT-axis gap and OVERTURNED the reason the census recorded for it. Measured: declaredFieldTarget returns NON-null for an ADR-0015 external object that declares a mapped field map (the shipped showcase_ext_customer shape), so it reaches the union branch \u2014 the ledger's claim that skip 2 catches such an object, leaving 'no path on which the warning could fire', is false. On one stack the SORT axis produced 0 findings while the SEARCH axis produced 1 warning on the identical anchor on the identical object. Runtime half measured on a real SqlDriver over better-sqlite3 against a remote table lacking the anchors: ORDER BY created_at (and owner_id) returns asc and desc BYTE-IDENTICAL, 3 rows, no error, while ORDER BY name reverses correctly \u2014 and neither runtime door refuses it, because both judge 'formula' alone. Step 2 wired sort-field-unprovisioned mirroring #8404's SEARCH wiring (optional trailing anchor-index param on the exported checkSortDeclaration core, warning severity, undotted names only so the axis keeps the dotted posture it shares with FILTER/PROJECTION). Ruling 2 on validate-translation-references UPHELD on fresh evidence, not inheritance: the union is read at exactly one site, the key is derived from REGISTERED metadata into which the registry injects the anchor on a federated object too, so the label resolves and asking provenance would flag a translation that works. Both ledger rows now carry the measured reason.", "tests": "All gates re-run on final head f29ecbfd4b. pnpm --filter @objectstack/lint exec vitest run -> 'Test Files 80 passed (80) / Tests 2246 passed | 5 skipped (2251)'. pnpm --filter @objectstack/lint run typecheck -> exit 0, no diagnostics. Gate union re-derived by `node scripts/pm/dispatch-gates.mjs` with no paths; every derived gate run and each printed its own verdict line: check:changeset-gate-self-tests, check:cross-package-test-inputs ('OK: 13 package(s) read outside themselves, all declared'), check:objectui-changeset, check:slot-lookup ('ratchet holds: 107 unswept site(s)... none new'), check:test-source-alias ('OK - 72 packages with tests scanned'), check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major ('introduces no `major` bump'), check-cross-package-test-inputs, check-empty-changeset ('1 declaring changeset(s) added'), check-affected-docs. Convention-triggered by editing test files: check:query-options-erasure ('ratchet holds... none new'), check:type-check-coverage ('OK - 64/77 workspace packages type-checked'), check:engine-double-contract ('OK - 371 pinned'), check:where-matcher ('271 matcher(s)... 0 silently-wrong'). Reasoned rather than derived: check:nul-bytes ('OK (scanned 6232 text file(s)... no raw ASCII control bytes)'). RATCHET THE GREEN TYPECHECK COULD NOT SEE: packages/lint's tsconfig excludes **/*.test.ts, so `pnpm typecheck` is green over code that check:type-check-coverage --re-measure still measures. Reproducing that program's own TEST_DEBT project caught the first version of the new tests moving the count 19 -> 20 (one TS2352 from a tuple cast); fixed in commit f29ecbfd4b and re-measured at exactly 19, matching the ledger, with neither edited test file contributing an error. ABLATION: removing only the provenance findings.push reddened EXACTLY the 6 cases predicted -- vitest printed 'Tests 6 failed | 47 passed (53)' naming 'warns on a list-view sort ordering by an unprovisioned injected anchor', 'covers every anchor the injection registers', 'reads the legacy string sort form too', 'is additive: the existence verdict on a real typo still fires beside it', 'reaches the `defineView` aggregate and standalone list-view rungs too', 'asks once the caller passes it'. The census test stayed green as predicted (a push-only ablation leaves the provenance-API tokens in the file, which is what that ledger judges by), and the four negative-direction cases stayed green because they assert absence. REBUILD: none applies and that is stated rather than skipped -- both legs are same-package RELATIVE imports ('./validate-sortable-fields.js') resolving to src, so no dist boundary is crossed; the cross-package closure was nevertheless built (pnpm --filter '@objectstack/lint^...' build, exit 0) because the rule imports @objectstack/spec/data through its dist. MUTATION CONFIRMED ON DISK by anchor count on BOTH legs, never by the editor's exit code: `anchors?.has(head)` 1 -> 0 -> 1, `rule: SORT_FIELD_UNPROVISIONED` 1 -> 0 -> 1, ablation marker 0 -> 1 -> 0, worktree confirmed clean after restore. STEP-1 MEASUREMENT CORRECTION, reported rather than quietly re-run: the first runtime run was contaminated -- seeding the remote fixture through the managed driver's initObjects made its DDL inject created_at/updated_at into the fixture table, so the remote under test carried the very column the measurement was about (['id','created_at','updated_at','name','email','region','lifetime_value']) and asc/desc came back merely different, not identical. Rebuilt with raw DDL so the remote carries exactly the five columns the showcase declares; all reported numbers are from the corrected run.", "open_questions": [], "out_of_scope_findings": [ "filed as #10744: the SORT axis' runtime premise is unpinned -- neither assertSortFieldsExist (#6994) nor assertOrderByIsMaterializable (#7095) refuses an ORDER BY over an unprovisioned injected anchor (both judge 'formula' alone), and nothing in objectql or driver-sql asserts it, so the new lint rule's justification can go stale with nothing turning red. Carries the measurement and the #7738 precedent for pinning a premise rather than assuming it.", "not filed, recorded here as context: examples/app-showcase declares two federated objects (showcase_ext_customer, showcase_ext_order) and no list view, view record or page binds either -- verified by grep across examples/. The federated read path therefore has no authored SORT or SEARCH surface in any example app, which is why the #8999 census could record this gap only as a code-path reading and never as an observed missing finding. Included in #10744's context section rather than filed separately." ] }Generated by Claude Code
test(driver-sql): pin the SORT axis' unprovisioned-anchor premise (#1…
feat(lint): ask the objectstack-ai#8116 provenance question on the SO…
test(driver-sql): pin the SORT axis' unprovisioned-anchor premise (ob…