fix(lint): walk page filterBy and lookup-field lookupFilters as authored filters - #19818
Conversation
…red filters FILTER_KEYS gains the two consumed rule-array carriers the walk never entered, so filter-preset-comparand (and filter-token-unknown) judge them. The preset rule binds filterBy through interfaceConfig.source and lookupFilters through the field's reference, never the owning object. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…ters Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…nt-filter-walk-carriers
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 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 7e3437801980c821e5f7aaef01f8b2cdf03e61fc && git checkout 7e3437801980c821e5f7aaef01f8b2cdf03e61fc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1f89ba0d704a797225ab8cf306e4cdec89edae8e 4dda1fd0fd8bda0888711cabaa737711350cce08 && git checkout -B drift-repro 1f89ba0d704a797225ab8cf306e4cdec89edae8e && git merge --no-ff 4dda1fd0fd8bda0888711cabaa737711350cce08
node scripts/docs-audit/affected-docs.mjs --json 1f89ba0d704a797225ab8cf306e4cdec89edae8e
|
…nt-filter-walk-carriers
… clause The lint's filter walk now descends a page's interfaceConfig.filterBy and a lookup field's lookupFilters, so the entry's surface group that put both keys beyond every publish door, and the by-hand search its acceptanceCriteria prescribed for them, no longer describe the platform. Both are deleted; the surface's group count goes with them. registry.ts is regenerated with gen:migration-registry. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
The lint now walks a page's interfaceConfig.filterBy and a lookup field's lookupFilters, so the reason clause saying both are reached by neither door is false. The clause is deleted; the sentence ends on the surface's groups. registry.ts is regenerated with gen:migration-registry. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
The pending preset-entry changeset's third group said filterBy and lookupFilters lint green and are swept by hand; this branch makes both false, so the item and the group count go. The spec patch changeset now names reason beside surface. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed and posted 2026-09-23T13:21Z by the at-tier review subagent the ① Derived judgments
② Semver level
③ Boundary flags
Blocking: F1 — three one-phrase edits ("two of the consumed rule-array carriers …; object-grid Implemented-by: VERDICT: FAIL |
…ule-array carriers
filterBy and lookupFilters are not the only consumed rule-array carriers
outside the walk (object-grid defaultFilters is another), so the article
that said so goes from the lint changeset, the FILTER_KEYS docblock and the
test header; relatedListFilter is no longer the one member off the filter
spelling either. The empty-combinator and flow-token rules did change for
values the lint receives (filterBy: {} and a flow node config.lookupFilters),
so the clauses and the changeset bullet calling them unchanged are cut.
Comments and changeset prose only; no code moves.
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
… preset note This branch deletes sentences that count was taken over, so the count no longer holds for the release that ships the note. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
With the two unchanged clauses gone, the sentence framed as what the entry changes per walking rule named two of the four rules and implied the rest held still, which the empty-node and flow-token measurements refute. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed and posted 2026-09-23T14:26Z by the at-tier review subagent the ① Derived judgments
② Semver level
③ Boundary flags
Blocking: F1 and F2 — two one-phrase cuts in test comments, no code moves: drop "— the lit control every block below keeps" (or scope it to block 1), and replace "the same silent literal it is in" with wording that does not assert a silent runtime (the engine refuses it). Implemented-by: VERDICT: FAIL |
|
…comments The preset test header claimed its dataSource.filter lit control in every block below; only the first block carries one. The token test comment called an unknown token a silent literal; the engine's token resolver throws FILTER_TOKEN_UNKNOWN on it. The FILTER_KEYS docblock said an ordering preset in either carrier was refused at query time; the engine's temporal door refuses only on a date, datetime or time field. Each clause is cut; comments only, no code moves. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed and posted 2026-09-23T15:27Z by the at-tier review subagent the ① Derived judgments
② Semver level
③ Boundary flags
Blocking: none. Implemented-by: VERDICT: PASS |
Maintainer confirmation — the DELIBERATE CORRECTION of
|
… paths (objectstack-ai#20056) Fixes objectstack-ai#20034 Clause-②: no ## Patch round 1 (head `10b1176328`) Added on top of the reviewed head `4c216561c6` (at-tier review PASS, comment 5824559177). It carries the implementer's own two out-of-scope findings and the reviewer's Clause ② reading: - **ADR-0087 D3 entry `automation-runs-cursor-retired`**: `packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts` `:11`, `:43` and `:72` now name `GET /api/v1/automation/:name/runs`, the path this PR's contract publishes and the dispatcher mounts. `packages/spec/src/migrations/registry.ts` was regenerated with `pnpm --filter @objectstack/spec gen:migration-registry` (not hand-edited; the diff is the same three lines at `:5943`, `:5975` and `:6004`). `gen:upgrade-guide` and `gen:spec-changes` were re-run and changed no bytes, because the entry is in step 18, beyond `PROTOCOL_MAJOR` 17. The text still ships today as data in `MIGRATIONS_BY_MAJOR[18]`, which is why it is corrected now. Open PR objectstack-ai#20031 regenerates a different region of `registry.ts`; whichever of the two lands second regenerates. - **Two comments**: `packages/runtime/src/query-param.ts:179` and `packages/services/service-automation/src/run-list-truncation.test.ts:6` now quote the `/api/v1` path. Both are comments only. - **Clause ②**: `.changeset/20034-automation-contract-api-v1-paths.md:9` and this body's line 2 now read `Clause-②: no`, with no arm. This diff adds no key, widens no accepted input and adds no export (`scripts/pm/clause2-line.mjs:70`). The level stays `minor`. - No pending changeset quotes a sentence of the D3 entry. `.changeset/19365-automation-runs-cursor-hasmore.md:117` carries only the registration marker naming the entry's id, and the id is unchanged. So this round needs no further deliberate correction. ## What this changes `AutomationApiContracts` (`@objectstack/spec/api`) declared its nine flow endpoints under `/api/automation`. The dispatcher mounts the automation door at `config.prefix || '/api/v1'` plus `/automation`, and `objectstack serve` passes no prefix, so every declared path answered `404 ENDPOINT_NOT_FOUND` on the default composition (measured on a composed runtime by the objectstack-ai#19966 dev). This PR takes remedy 1: the contract moves to the served paths. **The runtime and dispatcher are unchanged.** - `packages/spec/src/api/automation-api.zod.ts`: the nine `path` values, the module's `Base path` line and endpoint list, and every other in-file path quote (section headers, `@example`s, the resume docblock, and the `cursor` tombstone text `ListRunsRequestSchema` raises) move from `/api/automation…` to `/api/v1/automation…`. After the edit the file holds 0 occurrences of `/api/automation` (28 moved, plus the 10 docblock lines rewritten). - `packages/spec/src/api/automation-api.zod.test.ts`: the nine path pins move with the values. - `content/docs/references/api/automation-api.mdx`: regenerated with `pnpm --filter @objectstack/spec gen:docs` (not hand-edited). - `packages/runtime/src/automation-api-contract-mounts.test.ts` (new): the drift pin, below. - `.changeset/20034-automation-contract-api-v1-paths.md` (new): `@objectstack/spec` `minor`. - `.changeset/19365-automation-runs-cursor-hasmore.md`: a deliberate correction of a pending note, below. - Patch round 1: `migrations/entries/semantic/18.automation-runs-cursor-retired.ts` and the regenerated `migrations/registry.ts`, plus comments in `packages/runtime/src/query-param.ts` and `packages/services/service-automation/src/run-list-truncation.test.ts`. ## Reproduction, at base `adbbc5d01e` - Spec: `automation-api.zod.ts:14` `Base path: /api/automation`; `:658`–`:706` nine `path` values under `/api/automation`; the test pinned all nine to themselves (`automation-api.zod.test.ts:803`–`:811`). - Runtime: `dispatcher-plugin.ts:909` `const prefix = config.prefix || '/api/v1';`; `registerAutomationRoutes(base)` mounts `${base}/automation…` (`:1465` onwards), called with `prefix` at `:1746`, and with `${prefix}/environments/:environmentId` at `:1742` / `:1750` when project scoping is on. - Route ledger: `route-ledger.ts:429` `POST /automation` (client `automation.create`) and siblings; the header (`:17`) says to prepend `/api/v1` for the wire path. - CLI: `packages/cli/src/commands/serve.ts:4412` calls `createDispatcherPlugin({ scoping, enforceProjectMembership, observability, rateLimit })`, no `prefix`; scoping defaults to off (`:4340`). ## Consumer search: nothing depends on the unversioned form | candidate | reads the contract's `path`? | verdict | | --- | --- | --- | | `packages/adapters/hono/src/hono.test.ts:453` (`GET /api/automation delegates to dispatch()`) | no | Not a consumer. It drives `createHonoApp` with the adapter's own default `prefix` (`options.prefix \|\| '/api'`, `hono/src/index.ts:303`) against a mocked dispatcher and asserts the dispatcher-internal `/automation`. It never imports the contract. | | `packages/client` | no | Builds automation URLs from discovery or its `/api/v1/automation` convention (`getRoute('automation')`); it never names `AutomationApiContracts`. Two comments name the spec test file `automation-api.zod.test.ts`, not the constant. | | everything else in this repo | no | `AutomationApiContracts` occurs only in its declaring file, its spec test, `api-surface/api.json` (name only) and `export-origins/api.json`. No generator reads the path values. | | objectui at the pinned `.objectui-sha` `62597c588` | no | `git grep -F` at that commit: `AutomationApiContracts` 0 files, `/api/automation` 0 files; positive control `/api/v1/automation` 33 files. | | `objectstack-ai/cloud` and npm consumers | not measured | not checked out here | One served surface does use the unversioned form: a host built with `createHonoApp({ kernel })` and no `prefix` serves the whole dispatcher, automation included, under `/api`. That is a documented adapter default, and it applies to every contract family: under that host every other `*ApiContracts` row (`/api/v1/…`) is off by the same segment. The old automation paths matched it by coincidence, not by design, and no code reads the contract under that host, so remedy 2 does not apply. The changeset says how such a host maps the contract paths. ## Deliberate correction of a pending release note `.changeset/19365-automation-runs-cursor-hasmore.md` (pending, not yet released) quotes the `cursor` tombstone text in its FROM/TO block. That text is one of the path quotes this PR moves, so the note became false. Its line 32 changes from -> throws: '`cursor` was removed from GET /api/automation/:name/runs in to -> throws: '`cursor` was removed from GET /api/v1/automation/:name/runs in Nothing else in that note changes. This is the DELIBERATE CORRECTION class that `check-empty-changeset.mjs` names. `skip-changeset` is not applied, and `Check Changeset` stays red **by design**. The same-head at-tier review (comment 5824559177) names the note and judges the changed sentence. No other pending changeset quotes an unversioned automation path. At the base, `git grep -n "/api/automation" -- '.changeset/*.md'` showed only that line. Patch round 1 corrected no further note: no pending changeset quotes the D3 entry's sentences. ## Changeset level `minor`, not declared breaking. The `path` type stays `string`, no accepted input narrows, no method changes, and the old values named paths that no route served on the default composition, so a caller that read the constant gets a working URL now without changing code. This follows the precedent of the `PackageApiContracts.installPackage.path` rebind (`.changeset/18058-install-door-contract-rebind.md`, `minor`, not breaking). The declaration is `Clause-②: no` with no arm, in both this body and the changeset. It answers the reader's question "does this widen an accepted input or grow the public surface?" and the answer here is no: no key added, no accepted input widened, no export grown. It is not `(narrowing)` either, because nothing an author writes is removed. `minor` is valid under `no`: a published constant's value moves, and `patch` is a floor, not a ceiling. The first head declared `yes`, copied from the claim, and the reviewer judged that over-declared. ## The drift pin, and proof that it can fail `packages/runtime/src/automation-api-contract-mounts.test.ts` has two legs: 1. **mount**: it starts `createDispatcherPlugin` with **no** `prefix` (the composition `objectstack serve` builds) on a server that records registrations, and requires every contract `METHOD path` to be one of them. The prefix comes from the plugin's own default, not from a constant in the test. 2. **ledger**: every contract route must be a `route-ledger.ts` row under the documented `/api/v1` wire prefix. The live-mount parity gate probes those rows through the real router. The runtime vitest config aliases `@objectstack/spec/*` to spec **source**, so the spec side of the pin reads `src/`, not a build. The ablation was run on the committed tree (`4c216561c6`) with `scripts/ablation-replace.mjs`, one leg at a time, with restores anchored on `HEAD`: | leg | mutation (landed on disk: anchor 1 → 0, blob moved) | pin result | | --- | --- | --- | | contract | `getRun.path` back to `/api/automation/:name/runs/:runId` | mount red, ledger red (`getRun` named), 1 passed | | dispatcher | default prefix `'/api/v1'` → `'/api/v2'` | mount red (all nine named), 2 passed | | restored | none (blobs equal `HEAD`, `git diff HEAD` empty for both paths) | 3 passed | ## Environment-scoped mount The contract does not carry `/api/v1/environments/:environmentId/automation…`, and this PR does not add it. No `*ApiContracts` map declares the scoped variants. Scoping is one mount-time transformation the dispatcher applies to automation, actions, AI and packages alike, and the client derives scoped URLs from discovery. If the variants are ever declared, that belongs once in a contract shared by all the families, not copied into each map. Note that under `projectResolution: 'required'` none of the nine unscoped paths is mounted (`dispatcher-plugin.required-scoping-mounts.integration.test.ts` pins that). ## Verification (head `10b1176328`, patch round 1) - Tests, each package's full local project, under the verify lock at this head. The lock's verdict is `batch-last-exit 0`: the last part of the batch requires all three suites to exit 0, and the batch printed `SUITES spec=0 service-automation=0 runtime=0`. - `@objectstack/spec`: 532 files, 15648 passed, 2 todo. - `@objectstack/service-automation`: 144 files, 1725 passed. - `@objectstack/runtime` (`--project local`): 277 files, 3896 passed, 1 skipped, including the drift pin's 3 cases. - Build: `turbo run build --filter='./packages/*' --filter='./packages/*/*'` at this head: 72 of 72 tasks succeeded, including the tsup and declaration builds of spec, runtime and service-automation. - Spec generated artifacts: `check:generated` reports "All 15 generated artifacts are up to date". `check:migration-registry` reports "src/migrations/registry.ts is current (242 semantic, 210 retired-key, 183 retired-def)". `check:spec-changes` and `check:upgrade-guide` both report up to date. `check:api-surface` reports "public API surface + factory signatures unchanged", and `check:docs` reports "225 generated files in sync". - Derived gate set, taken after `git fetch origin main` (`dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, merge base `adbbc5d01`, 10 paths): 113 families, 6 more than round 0 (`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:future-spec-major`, and `check-tenant-audit-census` with its self-test). All 113 ran, and `--ran` reports "113 run, 0 NOT-MEASURED" with 0 UNRUN. 112 exited 0. `node scripts/check-empty-changeset.mjs --base origin/main` exits 1 on the deliberate correction, as designed. No family answered PREREQUISITE NOT MET this round. - The branch is behind `origin/main` (15 commits at the seat's re-read). Three of those commits regenerated one of this PR's 10 paths, `packages/spec/src/migrations/registry.ts`: objectstack-ai#20036 (`0bf85eaae6`), objectstack-ai#19909 (`5b9402d89b`) and objectstack-ai#19818 (`66960564d9`). `git merge-tree --write-tree` of `origin/main` and this head is clean, and the at-tier re-review measured that objectstack-ai#20036's and objectstack-ai#19909's hunks do not touch this PR's region (`:5940`–`:6004`). The merge queue's rebuilt generation regenerates the file. The derivation's one changed family input across that range is `scripts/sdui-manifest.record.json`, from objectstack-ai#20036. (Corrected by the seat after the at-tier review `5825376693` found the earlier sentence, "None of this PR's 10 paths is touched by those commits", false.) - Clause ② and ADR-0087, run offline with this body as the `pull_request` event: - `check-changeset-no-major --event` prints "✓ This diff introduces no `major` bump." and "✓ LEVEL AXIS: this PR declares clause-② `no`, so no package here is declared to have grown a published surface." (declaration line `Clause-②: no`, no arm). - `check-adr-0087-registration` prints "✓ … this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen)". This gate reads the Clause ② arm from the changeset body (`readClause2Line(parsed.body)`), not from the PR event. Its verdict is the same with and without `--event`. - Lint, narrowed and proven: all 7 touched TS files are in the eslint population (`--print-config` resolves each). `--no-inline-config --format json` reports 7 files, 0 errors, 0 warnings. `eslint.config.mjs` enables no type-aware linting: all seven `parserOptions` blocks are `{ ecmaVersion, sourceType }` only, with no `project` or `projectService`. So this diff cannot change the verdict on any untouched file. The repo-wide `pnpm lint` is left to CI. - Round-0 evidence still stands, and its sources are unchanged in round 1: the ablation above on `4c216561c6`, and the spec and runtime `typecheck` runs, both exit 0. Round 1 changes only string literals (the D3 entry and its registry mirror), comments and one changeset line. The type-check-debt gate re-measured at this head: "4 ledger entr(ies) … none above its recorded number". ## Acceptance notes (observations, not filed) - `RouterConfigSchema` (`spec/src/api/router.zod.ts`) defaults `basePath` to `/api` with `mounts.automation: '/automation'`. That spec-only declaration has no runtime reader in this repo. - The contract lists nine of the 17 routes `registerAutomationRoutes` mounts. The ones not listed are resume, cancel, restore-suspension, screen, actions, connectors, `_status` and the legacy trigger form. - **Every remaining `/api/automation` path at head `10b1176328`, and why it stays** (`git grep -n "/api/automation\b"`, with the `automation-api` file-name hits filtered out): - `.changeset/20034-automation-contract-api-v1-paths.md` `:5` and `:15`-`:20`: the FROM column of this PR's own FROM/TO table. - `packages/adapters/hono/src/hono.test.ts:453`-`:454`: the Hono adapter's own default `prefix` (`/api`), driven against a mocked dispatcher. It does not read the contract. - `packages/runtime/src/automation-api-contract-mounts.test.ts:9`: the pin's docblock, describing the drift it guards. - `packages/spec/scripts/file-description.test.ts:878`-`:937`: synthetic fixtures for the docblock-description extractor. They do not quote the contract. - `packages/spec/src/api/router.test.ts:343`: a custom-mounts fixture for `RouterConfigSchema`. - Released `CHANGELOG.md` entries: `packages/client/CHANGELOG.md:3230`, `packages/runtime/CHANGELOG.md:11589`, `packages/services/service-automation/CHANGELOG.md:5802` and `packages/spec/CHANGELOG.md:32723` quote `GET /api/automation/:name/runs` in the released ExecutionStatus-filter entry. These are release-owned and never edited in a code PR; an amendment would be a dedicated docs-only PR. `packages/spec/CHANGELOG.md:14982` names the file `automation-api.mdx`, not a path. Implemented in session `session_019c3Hi6ZMU1p6m6aA6Bz45d` (claim 5823821835; patch round 1 dispatched by the `domain:spec` seat 4). --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19791
Clause-②: no
Rewritten short by the
domain:spec#5seat (2026-09-23T15:28Z), which took this PR over from seat 2. The earlier body is in the edit history; the dev reports are on #19791.A page's
interfaceConfig.filterByand a lookup field'slookupFiltershold filter rules that reach the engine'swhere, but the lint's shared filter walk did not enter them. A date-range preset in either, such as{ field: 'close_date', operator: 'gt', value: 'last_30_days' }, parsed, linted and published green.What changed
@objectstack/lintpatch):FILTER_KEYSinpackages/lint/src/filter-walk.tsaddsfilterByandlookupFilters.filter-preset-comparandbinds afilterByrule tointerfaceConfig.source(else the page'sobject), and alookupFiltersrule to the field'sreference. ArelatedListFilteron the same field still binds to the owning object.filter-token-unknownreaches both keys through the same walk.18.filter-preset-ordering-comparand-refused(@objectstack/specpatch; rulings5793402649and5794625162on [finding] the lint's filter walk (FILTER_KEYS = filter · filters · runtimeFilter · relatedListFilter) never reaches a page's interfaceConfig.filterBy — a bare date-range preset in that rule array parses green AND lints green, so nothing refuses it at publish #19791): the fix makes parts of it false, and they are deleted with no replacement:surfacegroup (3) and the word "three" before it, the by-hand clause inacceptanceCriteria, and the "reached by neither" clause inreason.registry.tsis regenerated..changeset/19778-preset-entry-carriers.md(from fix(spec): the preset-comparand entry puts each carrier under the door that refuses it #19801, not yet released): its item 3 said both keys lint green and must be swept by hand. That is false after this PR, so item 3 is deleted, "The three groups" becomes "The groups", and the Reach sub-bullet counting "nine sentences" is deleted too.Check Changesetis red on purposeThis PR edits a pending changeset it did not add (the #19778 note above).
check-empty-changeset.mjscalls this a DELIBERATE CORRECTION: the check stays red. The correction is recorded on this PR (5796661306) and awaits the maintainer's written confirmation.Check Changesetis not a required context.Measured (dev reports on #19791)
os lint,os validateand the runtime publish gate refuse it at this head; none of them refuses it atafc3b64928. Lit control (a componentdataSource.filter): refused on both trees. Dark control (an ISO date): green on both trees.validate-preset-comparands.test.tsand 1 invalidate-filter-tokens.test.ts. Restoring the base source turns 5 red. Three ablations each turn their pin red: dropping the two keys, dropping thelookupFiltersbinding, and dropping theinterfaceConfigreader.5795612549,5796648031) failed sentences, not code. Rounds 4 and 5 cut them: three "the two carriers" exclusives (object-griddefaultFiltersis also outside the walk), a lit-control claim, an unknown token called "silent" (the engine throwsFILTER_TOKEN_UNKNOWN), and "refused only at query time".4dda1fd0fd: all seven required contexts pass.Check Changesetis red as described above; the gate comment is5796661306.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1