Uh oh!
There was an error while loading. Please reload this page.
feat(lint): ask the #8116 provenance question on the SORT axis (#10474) - #10745
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…atchet The tuple cast moved packages/lint's TEST_DEBT count 19 -> 20 (TS2352). The package tsconfig excludes *.test.ts, so `pnpm typecheck` could not see it — only the check:type-check-coverage --re-measure program can. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 98ac98ebdf6d510b07fc1e5aee44d8cacc14610c && git checkout 98ac98ebdf6d510b07fc1e5aee44d8cacc14610c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9185ff0213c1590bf3808d5955cddd2becf924ea f29ecbfd4b13be7f8c776355fc4dff3259974895 && git checkout -B drift-repro 9185ff0213c1590bf3808d5955cddd2becf924ea && git merge --no-ff f29ecbfd4b13be7f8c776355fc4dff3259974895
node scripts/docs-audit/affected-docs.mjs --json 9185ff0213c1590bf3808d5955cddd2becf924ea |
PM review — verified against the tree and the ledger, not the report. Approving. Step ① did what I asked and then went further: it overturned the census's reasonThe dispatch said confirming or overturning the ruling was a successful outcome. You did both halves at once, which is the useful case:
A card whose premise survives but whose stated reason does not is the case most likely to be waved through, because the headline still checks out. Both ledger rows now carry the measured reason instead. And the runtime half is measured, not inferred: on a real ⭐⭐ The contaminated measurement you caught on yourselfThis is the sharpest instrumentation catch I have seen tonight:
The fixture setup manufactured the condition whose absence was the subject of the measurement. And the symptom was not a failure — asc/desc came back different rather than identical, which is a perfectly plausible-looking result that would have quietly understated the defect. You rebuilt with raw DDL so the remote carries exactly the five columns the showcase declares, and every reported number is from the corrected run. Reporting that instead of silently re-running is what makes the rest of the numbers worth reading. ⭐ The ratchet a green typecheck could not see — I verified both halves
Confirmed on So That is a red CI run avoided by distrusting a green that was structurally incapable of covering the change. The ablation predicts what should not moveRemoving only the provenance
Predicting which assertions must hold still is harder than predicting which break, and it is what distinguishes an ablation from a demolition. The restRuling 2 upheld on fresh evidence rather than inheritance — the union is read at exactly one site, the key derives 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. Re-deriving a ruling you were inclined to keep is the right instinct. The changeset judgement is correct. The rebuild statement is stated rather than skipped: no dist boundary is crossed (both legs are same-package relative imports resolving to #10744 is well-filed and correctly routed to The context you didn't file separately — that Arming. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Closes#10474
This card was confirming or overturning two census rulings, not fixing a failing test. Step ① (premise verification) came first and is reported in full below, because it changed one of them.
Step ① — the premise, measured end to end
The #8999 census recorded
validate-sortable-fields.tsas not asking the #8116 provenance question, with the reason that an ADR-0015externalobject never reaches the union branch at all — skip ② was believed to catch it.The gap is CONFIRMED. The recorded reason is OVERTURNED.
①a — the external object does reach the union branch
declaredFieldTarget(packages/lint/src/validate-searchable-fields.ts) returnsnullon exactly one condition —fieldsmissing, unreadable, or naming nothing — and nothing in it testsexternal. Measured by running the real index over the showcase's own federated object:So the shipped shape — a federated object that declares a mapped field map, as
examples/app-showcase/src/data/objects/external/customer.object.tsdoes — is indexed like any other object and lands squarely in skip ③. The path existed and was shipped; only the warning was missing.①b — the axis asymmetry the card describes, on one object
Same stack, one external object and its local twin, each with a list view ordering by
created_atand asearchableFieldsnaming an anchor:①c — the runtime consequence, on a real driver over a real remote table
This is the half the card recorded as never observed. A real
SqlDriverover better-sqlite3, the object declared exactly as the showcase declares it, against a remotecustomerstable carrying[id, name, email, region, lifetime_value]and none of the seven injected anchors:ascanddescbyte-identical while the baseline reverses is what makes it a dropped sort rather than a coincidence — the same signature this rule already records forformula, reached by a second route.initObjectsmade its DDL injectcreated_at/updated_atinto the fixture, so the remote table under test carried the very column the measurement was about (["id","created_at","updated_at","name","email","region","lifetime_value"]) andasc/desccame back merely different, not identical. The fixture was rebuilt with raw DDL so the remote carries exactly the five columns the showcase declares. The numbers above are from the corrected run.①d — why an authoring gate is the only door available
Both runtime doors on this axis judge
formulaalone (UNMATERIALIZED_SORT_TYPES):assertSortFieldsExist(packages/metadata-protocol/src/protocol.ts, A non-dottedorderBynaming aformulafield answers 200 in arbitrary order — the sort is silently dropped (measured on driver-sql + driver-memory) #6994) — precedenceunknown>dotted> unmaterializable. An injected anchor is ingate.known, and it is undotted.assertOrderByIsMaterializable(packages/objectql/src/engine.ts,engine.find()still drops aformulaORDER BY silently — decide whether the engine refuses or keeps its internal-caller tolerance #7095) —schema.fields[f]?.type === 'formula'.An injected anchor is a
datetimeor alookup, so it clears every verdict and reaches the driver.Step ② — the wiring
New rule
sort-field-unprovisioned, mirroring #8404's SEARCH wiring on the identical index: an optional trailing anchor-index parameter on the exportedcheckSortDeclarationcore (same posture, same reason, ascheckSearchableFieldList's — an out-of-repo caller that never built the index keeps its pre-#10474 answers),indexUnprovisionedAnchorsbuilt once in the walk, and the question asked inside the skip-③ branch about a name the union already decided not to flag.warning, nevererrorand never gating (#4330's cost asymmetry). The remote schema is invisible to this pass, so the remote table may genuinely carry acreated_atof its own; declaring that column — the first remedy the shared hint prescribes — silences the finding, becauseunprovisionedInjectedColumnsForexcludes an author-declared column of the same name (#7859's security direction). The runtime publish gate splits on severity (runtime-gate.ts:errors=severity === 'error', everything elseadvisories), so this refuses no write.Two deliberate narrowings, both pinned:
resolveSearchFieldsmatches by exact string and drops a dotted entry like a typo, but a dotted SORT name is refused by the ingress gate as its own verdict (400 INVALID_SORT, loudly, on every fetch), so the silent degradation this finding reports cannot happen there. Answering would give the SORT axis its own dotted verdict — the posture the rule shares with the FILTER and PROJECTION axes (sort的点号路径(?sort=account.company_name)仍然静默降级为「不排序」——#4226 收口后唯一漏网的 sort 形态 #4256 / Data query: a dotted projection entry silently WIDENS the response —fields:["name","account.name"]returns every field #7532 / finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589) and declines to break.validate-page-field-bindingstakes the same posture (lint: view-filter / page-binding field checks resolve against the blanket SYSTEM_FIELDS union, so the #8116 unprovisioned-anchor warning cannot reach filter surfaces #8340).The rule's own skip-② docblock sentence — "external objects and datasource-introspected schemas" — was measured wrong and is corrected in place.
Ruling on
validate-translation-references.ts— UPHELD, on fresh evidenceThe card asked for confirm-or-overturn with fresh eyes rather than inheritance. Upheld, and the ledger
whynow carries the evidence rather than the assertion:validate-translation-references.ts:640, thefields.<name>orphan test);validate-page-field-bindings, lint: view-filter / page-binding field checks resolve against the blanket SYSTEM_FIELDS union, so the #8116 unprovisioned-anchor warning cannot reach filter surfaces #8340), not to the bundle that names it.The over-inclusion cost that does exist there (a
systemFields: falseobject, where the anchor is never injected and the translation really is an orphan) is an EXISTENCE question, not a provenance one —unprovisionedInjectedColumnsForreturns empty for that object too, so the provenance API could not close it either.Verification
Gate union re-derived from the real changeset with
node scripts/pm/dispatch-gates.mjs(no paths) and re-run on the final headf29ecbfd4b.pnpm --filter @objectstack/lint exec vitest run— 80 files, 2246 passed, 5 skippedpnpm --filter @objectstack/lint run typecheck— cleancheck:changeset-gate-self-tests,check:cross-package-test-inputs,check:objectui-changeset,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-cross-package-test-inputs,check-empty-changeset,check-affected-docs— all exit 0check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher— all exit 0check:nul-bytes(any edit) — clean over 6232 filesA ratchet the green typecheck could not see.
packages/lint's tsconfig excludes**/*.test.ts, sopnpm typecheckis green over code thecheck:type-check-coverage --re-measureprogram still measures. Reproducing that program's own TEST_DEBT project for this package caught the first version of the new tests moving the count 19 → 20 (one TS2352 from a tuple cast). Fixed in the second commit; re-measured at exactly 19, matching the ledger, with neither edited test file contributing an error.Ablation. Removing only the provenance
findings.pushreddened exactly the 6 cases predicted (warns on a list-view sort…,covers every anchor…,reads the legacy string sort form…,is additive…,reaches the defineView aggregate…,asks once the caller passes it), and 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. The four negative-direction cases stayed green, which is correct: they assert absence. Both legs are same-package relative imports resolving tosrc, so nodistis involved and no rebuild applies; the mutation and the restore were each confirmed on disk by anchor count (anchors?.has(head)1→0→1,rule: SORT_FIELD_UNPROVISIONED1→0→1, ablation marker 0→1→0).Inert on the current corpus, by design and by scan. No object under
packages/platform-objects/srcdeclaresexternal, and no view, page or list view anywhere inexamples/binds either of the showcase's two federated objects — so nothing shipped newly emits. That absence is also why the census could only ever record this as a code-path reading.Out of scope, filed not fixed
objectqlordriver-sql, so the rule's justification can go stale with nothing turning red. Includes the measurement above and the#7738precedent for pinning a premise rather than assuming it.Changeset
A real one,
@objectstack/lint: minor— notskip-changeset. The package publishes (17.1.0, noprivate) and this diff changes what it reports to consumers: a new finding, a new exported rule id, a new optional parameter on an exported core.Generated by Claude Code