Skip to content

fix(lint): warn when searchableFields declares an unprovisioned injected anchor (#8404) - #8665

Merged
qq9340100 merged 4 commits into
mainfrom
claude/issue-8404-searchable-fields-anchor-provenance
Aug 14, 2026
Merged

fix(lint): warn when searchableFields declares an unprovisioned injected anchor (#8404)#8665
qq9340100 merged 4 commits into
mainfrom
claude/issue-8404-searchable-fields-anchor-provenance

Conversation

@qq9340100

@qq9340100qq9340100 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes#8404

validate-searchable-fields.ts was the fifth blanket-SYSTEM_FIELDS read 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: :345 keeps searchable-field-unknown silent for any name in the object-independent union, and resolveAllowedSet goes further — it manufactures a stub meta for such an entry so it survives the resolution's existence filter.

On an ADR-0015 external object the platform registers its injected anchors and provisions no storage behind them (#7865 / #8116), so searchableFields: ['name', 'owner_id'] linted clean, the stub kept the entry in the resolved allow-list, and the view's $searchFields narrowing scanned a column empty on every record — #4830's own failure mode reached by a different route.

What changed

A new searchable-field-unprovisioned rule warns on such an entry, on the object's own canonical set and on a list view's narrowing alike, reusing unprovisionedAnchorCause / unprovisionedAnchorHint so 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.

checkSearchableFieldList takes the index as an optional trailing parameter, the same shape #8340 gave checkFieldRefs: 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, and patch would under-report public-surface expansion downstream.

Two decisions worth reviewing

The :239 stub 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 no where / 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:

  • Reverting the condition to the blanket 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).
  • Removing the emission fails exactly the 3 predicted positives.

The react-surface test exists because of a measured gap: dropping unprovisionedAnchors from the <ListView> call left typecheck and the whole 72-file suite green. The index stays read by checkBlockFieldProps in the same function, so noUnusedLocals does not fire there — unlike in validate-searchable-fields.ts, where the same ablation gives TS6133. 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). typecheck clean. Gates derived against the actual changed paths via scripts/pm/dispatch-gates.mjs and 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:

  1. check:changeset-gate-self-testscheck-changeset-no-major --self-test — the finding: check-changeset-no-major --self-test goes red repo-wide after a release exits pre mode — its control requires major-declaring changesets AND a pre.json that no longer exists #8654 shape (missing major-declaring control stock, absent .changeset/pre.json).
  2. CI's Check Changeset job → 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 pristine origin/main worktree: 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-test goes red repo-wide after a release exits pre mode — its control requires major-declaring changesets AND a pre.json that 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 an src/index.ts export — added.


Generated by Claude Code

…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
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 11:54am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx(via @objectstack/lint)
  • content/docs/deployment/validating-metadata.mdx(via packages/lint)
  • content/docs/permissions/authorization.mdx(via @objectstack/lint)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v17.mdx(via @objectstack/lint)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 14, 2026
`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
@qq9340100Claude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT on substance, held unmerged on #8654

Reviewed at 75c0eda4 by the domain:devx execution PM seat (#6023). ⛔ Deliberately not flipped out of draft and not armed for auto-merge — reasoning at the end.

The one defect I sent back, and it was fixed

The changeset declared patch while the PR adds a new public export (SEARCHABLE_FIELD_UNPROVISIONED from packages/lint/src/index.ts) plus an additive optional parameter on the exported checkSearchableFieldList. patch under-reports public-surface expansion to everyone reading the changelog downstream. Now minor (9318a094). Verified by reading the file at head, not by trusting the commit message.

What the dev measured instead of accepting from me

I 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:

  • A — keep the :239 stub. Confirmed by ablation, not argument: disabling it turns the existing "keeps runtime parity when the object declares system columns searchable" test 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.
  • B — one warning per (object, field). Emission placed 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.
  • D — ⚠️ FALSIFIED, and this is the valuable one. I asked whether check:cross-package-test-inputs would notice if an in-repo caller stopped passing the index. Measured answer: nothing would. Dropping unprovisionedAnchors from the <ListView> call site in validate-react-page-props.ts left typecheck and the entire 72-file lint suite green — the index stays read by checkBlockFieldProps in the same function, so noUnusedLocals never fires (it does fire in validate-searchable-fields.ts, where the index would become unused), and the metadata-surface tests cannot see that call site. A pinning test was added and verified by re-running the ablation with it in place (75c0eda4).

⭐ 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 check:cross-package-test-inputs green and could easily have let that stand in for coverage. Only deliberately reproducing the harm — removing the argument and watching everything stay green — showed the protection was absent.

Verification I confirmed independently

  • File surface: 5 files — the changeset, index.ts (+1 export), validate-react-page-props.ts (+5), validate-searchable-fields.ts (+75/−2), and the test (+126). No unrelated drift.
  • Mutation run in both directions with predictions written first. Reverting the condition to the blanket SYSTEM_FIELDS.has(name) failed 6 tests where 3 were predicted — the extra 3 being pre-existing system-field-silence tests, all failing in the correct direction. ⚠️ Reporting the mismatch instead of smoothing it is the right call; a number that matches the prediction exactly is weaker evidence than a documented over-shoot whose direction is checked.
  • WARN, never gating, per lint: five hand-copied SYSTEM_FIELDS lists in packages/lint should derive from the spec's declarations #4330 — as ruled, and correct: the remote schema is not visible to this pass, so the finding describes a degradation rather than a refusal.

Why it is not merged

Check Changeset fails on check-adr-0087-registration's input assertion — "not one of 3 changeset(s) in stock matches the breaking-change detector". Confirmed identical across three head SHAs (5593258b, 9318a094, 75c0eda4) by reading the job log each time rather than assuming the red carried over. This is #8654, repo-wide and post-release: the release consumed the changeset populations these gates' positive controls read. Recorded there as the second instance, with a different blast radius from the ESLint-hosted one.

⛔ 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

@qq9340100
qq9340100 added this pull request to the merge queueAug 14, 2026
Merged via the queue into main with commit b849e69Aug 14, 2026
27 checks passed
@qq9340100
qq9340100 deleted the claude/issue-8404-searchable-fields-anchor-provenance branch August 14, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@qq9340100@claude