Uh oh!
There was an error while loading. Please reload this page.
fix(lint): warn when searchableFields declares an unprovisioned injected anchor (#8404) - #8665
Conversation
…ted anchor (#8404) `validate-searchable-fields.ts` is the fifth blanket-SYSTEM_FIELDS read site. Both its gates were correct about existence and blind to provenance, so a declared anchor on an ADR-0015 external object lints clean and the view's `$searchFields` narrowing scans a column with no storage behind it. Adds `searchable-field-unprovisioned` (WARN, never gating -- #4330's cost asymmetry), reusing the #8340 shared message builders rather than re-wording them. The `:239` stub is kept: measured, it is what keeps the linter's resolution equal to the runtime's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
`SEARCHABLE_FIELD_UNPROVISIONED` is a new public export from `packages/lint/src/index.ts`, and `checkSearchableFieldList` gains an additive optional parameter. `patch` under-reports public-surface expansion to everyone reading the changelog downstream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz
Measured gap: dropping `unprovisionedAnchors` from the `<ListView>` `checkSearchableFieldList` call left typecheck AND the whole 72-file lint suite green. The index stays read by `checkBlockFieldProps` in the same function, so `noUnusedLocals` does not fire there (it does in validate-searchable-fields.ts, where the index would become unused), and the metadata-surface tests cannot see this call site. This test is the only thing that goes red when the argument is dropped -- verified by re-running the ablation with it in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz
qq9340100
commented
Aug 14, 2026
PM review — ACCEPT on substance, held unmerged on #8654Reviewed at The one defect I sent back, and it was fixedThe changeset declared What the dev measured instead of accepting from meI dispatched four assumptions marked unmeasured. Three came back measured and one came back falsified with a fix attached, which is the outcome that justifies writing them down as assumptions rather than instructions:
⭐ Recording the general form, because this seat has now been bitten by the inverse of it several times today: a gate running green is not evidence that the gate watches the thing you care about. I ran Verification I confirmed independently
Why it is not merged
⛔ The only input that would satisfy that detector is a breaking-change declaration, which would be a false statement about a WARN-only rule that gates nothing. Not done and not proposed. ⛔ Editing the gate is a maintainer floor. So this PR waits, visibly, rather than being armed into a required check it cannot pass — auto-merge on a blocked PR waits silently and surfaces no reason. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8404
validate-searchable-fields.tswas the fifth blanket-SYSTEM_FIELDSread site. Both of its gates were correct about EXISTENCE and structurally blind to PROVENANCE, exactly as the four filter/page-binding sites were before #8340::345keepssearchable-field-unknownsilent for any name in the object-independent union, andresolveAllowedSetgoes further — it manufactures a stub meta for such an entry so it survives the resolution's existence filter.On an ADR-0015
externalobject the platform registers its injected anchors and provisions no storage behind them (#7865 / #8116), sosearchableFields: ['name', 'owner_id']linted clean, the stub kept the entry in the resolved allow-list, and the view's$searchFieldsnarrowing scanned a column empty on every record — #4830's own failure mode reached by a different route.What changed
A new
searchable-field-unprovisionedrule warns on such an entry, on the object's own canonical set and on a list view's narrowing alike, reusingunprovisionedAnchorCause/unprovisionedAnchorHintso the sentence matches the four #8340 rules verbatim rather than becoming a second copy (#4830). WARN, never gating, per #4330's cost asymmetry — the remote schema is not visible to this pass, so the finding describes a degradation, not a refusal.checkSearchableFieldListtakes the index as an optional trailing parameter, the same shape #8340 gavecheckFieldRefs: its absence means the caller did not build the index and the provenance question goes unasked — the previous behaviour, preserved for out-of-repo callers (cloud graph-lint, the AI authoring path). Both in-repo callers pass it (validateSearchableFields,validate-react-page-props).Changeset is minor, not patch: this adds a public export (
SEARCHABLE_FIELD_UNPROVISIONED) plus an additive optional parameter on an exported function, andpatchwould under-report public-surface expansion downstream.Two decisions worth reviewing
The
:239stub is KEPT. It is not incidental — it is what makes the linter's resolution agree with the runtime's, which resolves the declared branch against the registry field map. Measured by disabling it: the existing "keeps runtime parity when the object declares system columns searchable" test goes red (expected [] to have a length of 1 but got +0), because the declaration existence-filters to empty and resolution falls through to the auto-default. Dropping it would have been a behaviour change dressed as a warning.The warning is emitted per declared entry in the checker's entry loop, never inside
resolveAllowedSet. That helper reads the OBJECT's declaration and runs once per narrowing, so warning there would repeat one object-level fact for every view and attribute it to the view's path. It also has nowhere/path/ entry index in scope.Tests
Six added — five on the metadata surface, one on the react surface. The load-bearing pair is the positive (external object, declared anchor, warns) and its negative twin (identical declaration on a non-external object, silent); the second is what proves the rule discriminates on provenance rather than on the name.
Mutation-checked in both directions, predictions written first:
SYSTEM_FIELDS.has(name)fails 6 tests — the 3 new negatives plus 3 pre-existing system-field-silence tests I had not predicted, all in the correct direction (warnings appearing where silence is required).The react-surface test exists because of a measured gap: dropping
unprovisionedAnchorsfrom the<ListView>call left typecheck and the whole 72-file suite green. The index stays read bycheckBlockFieldPropsin the same function, sonoUnusedLocalsdoes not fire there — unlike invalidate-searchable-fields.ts, where the same ablation givesTS6133. That test is now the only thing that goes red for the react call site, verified by re-running the ablation with it in place.Verification
pnpm --filter @objectstack/lint test— 72 files, 2024 passed (baseline 2014).typecheckclean. Gates derived against the actual changed paths viascripts/pm/dispatch-gates.mjsand run locally:check:cross-package-test-inputs,check:objectui-changeset,check:nul-bytes,check-empty-changeset,check-changeset-no-major,check:type-check-coverage,check:type-check-debt(full closure built first),check:query-options-erasure— all green.Two changeset-family reds, both repo-wide, neither this PR's:
check:changeset-gate-self-tests→check-changeset-no-major --self-test— the finding:check-changeset-no-major --self-testgoes red repo-wide after a release exits pre mode — its control requires major-declaring changesets AND apre.jsonthat no longer exists #8654 shape (missing major-declaring control stock, absent.changeset/pre.json).check-adr-0087-registration, which refuses a verdict because "not one of 3 changeset(s) in stock matches the breaking-change detector". Measured against a pristineorigin/mainworktree: the same script exits 1 there too ("not one of 2 changeset(s) in stock"), so it is stock depletion, not this diff. Same post-release root cause as finding:check-changeset-no-major --self-testgoes red repo-wide after a release exits pre mode — its control requires major-declaring changesets AND apre.jsonthat no longer exists #8654 but a different script than that card names.The rule-id barrel gate (
rule-id-barrel-exports.test.ts, #5648) caught the new constant needing ansrc/index.tsexport — added.Generated by Claude Code