Skip to content

fix(objectql,service-automation,runtime): the card's named warnings and endpoint hints state each decision in words instead of a tracker number - #20738

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20513-runtime-strings-no-tracker-numbers
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20513-runtime-strings-no-tracker-numbers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20513
Clause-②: no

The card's named producers are 11 author- and operator-shown messages in 4 files. Each one pointed the reader at a tracker number for the reason behind it. Each now says what the cited decision was, in the sentence being read (form D, as the migration-entry rewrite applied it). Text only: no error code, field name, HTTP status or behaviour changes. Every changed line in the four source files is a string-literal line.

This PR covers the card's first step only. The rest of the family is the maintainer's burn-down decision, and the census below is the input to it, so 20513 stays open.

What each sentence now says

Where (about) Cited The sentence now says
objectql engine.ts data.record.* warning (7077) 4639, 4626 It refuses to fabricate a per-record event with an empty recordId. A predicate (multi: true) write publishes its own data.records.* event carrying the affected-row count, so reaching this line is a driver defect.
objectql engine.ts data.records.* warning (7171) 4639 A bulk event states only the count (no records, no predicate), so a driver result that is not a count publishes nothing.
service-automation engine.ts resumeAuthority warning (3276) 3801, 5561, 3823 The resume-authority gate refuses this type's pauses. An undeclared resumeAuthority resolves to 'service', fail-closed, because guessing 'any' is how a raw resume once walked past an approval decision no service had recorded.
same file, resume refusal log line (6268) and error text (6275) 5561 An undeclared resumeAuthority resolves to 'service', fail-closed.
same file, undeclared-suspension refusal (6410) 5561 An undeclared resumeAuthority resolves to 'service', which the generic route refuses.
runtime api-endpoint-step.ts no-policy-context hint (245) 5040 E5b Execution is reachable only on the far side of the policy chain, so the composed runtime always threads the policy context.
same file, no-execution-wiring hint (264) 5040 E5b The composed runtime supplies the execution wiring together with the policy context; only a hand-mounted step can omit it.
same file, NOT_IMPLEMENTED message (346) 5040 No wiring was supplied, so nothing was executed.
runtime api-mapping.ts path hint (233) 5040 E7 The publish gate rejects the same shapes, so a declaration that reaches this check was stored without passing that gate.
same file, transform refusal (265) 5040 §3.4, E7 The sentence already said transform is rejected at publish rather than parsed and ignored; only the citation goes.

The integration pin in dispatcher-plugin.endpoint-fallback.integration.test.ts asserts not.toContain('without a policy context') and not.toContain('no wiring'). Both phrases are kept verbatim, so that negative pin still means what it meant.

Pins re-pinned: 6, one more than the order counted

Each of these now asserts the words that carry the decision instead of the number:

  • resume-authority-declaration.test.ts 84-85
  • engine-data-events.test.ts 374
  • api-endpoint-step.test.ts 143, 232
  • api-mapping.test.ts 230: a mustMention fragment list. The matcher-line grep behind the order's count of 5 cannot see it. It pins the transform refusal, one of the 11 named messages, so it had to move. The first full runtime run showed it red; after the re-pin it is green.

Ledger burn-down

scripts/doc-authoring-prose-id.baseline.json was regenerated with node scripts/check-doc-authoring.mjs --census-ledger. It was generated into a scratch file first, so the growth refusal ran against the checked-in baseline, and then copied into place. 14 id occurrences leave in 7 (file, id) pairs, and 2 files leave the ledger entirely. Pairs go 618 to 611, occurrences 945 to 931, files 229 to 227, and the gate's printed "pinned site(s)" (id-bearing string literals) 808 to 794. Nothing else in the file moved. pnpm check:doc-authoring is green on it.

Census: the input to the maintainer's burn-down decision

Instrument. A TypeScript-compiler AST walk over every non-test src/ file of every public package under packages/ (69 manifests, 2,582 files). It works per message:

  • Folding. Before matching, each message is folded into one text: a maximal chain of pieces joined by +, a template literal, parentheses, and a string array whose parent is .join(sep). A string nested inside a span, or inside a call that is an operand of the chain, is folded into the outermost message.
  • No comments. A non-string operand renders only its string leaves, never raw source, so comments are never read.
  • Match. A hash plus 3-5 digits, not after an ampersand, a hash or a digit, and not followed by an alphanumeric. Word forms (issue, PR or card plus a number) were scanned too: 0 hits.
  • Classification. By syntactic context: logger calls, thrown errors, envelope fields (message, hint, reason, error, detail(s) and the like), error-factory calls, and refusal prose built in a const or return. Outside the population: test-facing strings shipped under src/ (testkits, bench, contract-suite case labels), metadata text (description, help or label, and generated translations), and one excluded false positive (CSS colours in the CLI's inline HTML).

Controls.

  • Lit, single line: objectql engine.ts 7077-7080 gives one logger message citing 4639 and 4626.
  • Lit, multi-line: the service-automation warning at 3276-3283 is one + chain over 8 lines and is read as ONE message citing 3801, 5561 and 3823.
  • Dark: the service-automation docblock at 3231 gives 0 hits, and so does a // comment interleaved inside a + chain in lint's validate-action-body-writes.ts (426). Across the whole tree, 1,125 hit lines were checked and 0 fall on a comment line.
  • Independent cross-check against the gate's own ledger: on all 211 files both read, per-file id-occurrence counts are identical. The totals reconcile exactly by scope.

The doc-authoring ledger finding. The card says check:doc-authoring does not read these positions. It does. Its ledgered sibling-package leg, added in 3f54efd, holds every one of them in a shrink-only baseline. It exits 0 because they are baselined, not because they are unread. So the gate's reach does not need to move: any burn-down is monotone, and each burn-down PR regenerates that one shared file. That makes parallel stages serialise on it.

Totals (tree 36d043b, before this PR):

  • 743 population messages cite 554 distinct ids in 39 of the 69 public packages.
  • The engine lane holds 201 of them, citing 128 ids: 145 author-facing refusal or prescription prose and 56 log lines.
  • Outside the population: 84 test-facing strings, 75 metadata texts and 1 excluded false positive.

This PR removes 11 messages. On the merged head the census reads 892 messages with an id where it read 903, with 0 new.

Per package (bold = this lane; "id occurrences" counts the ids the population messages cite):

Package Messages logger thrown envelope factory prose id occurrences test-facing metadata text
spec 175 0 0 26 0 149 373 45 3
lint 83 0 1 63 0 19 104 0 0
runtime 53 7 2 9 1 34 72 0 0
drivers/driver-sql (lane) 52 15 6 5 20 6 66 7 0
metadata-protocol (lane) 40 19 5 3 11 2 49 1 0
objectql (lane) 40 14 6 0 3 17 44 3 2
plugins/plugin-security 34 20 1 3 2 8 39 0 4
services/service-automation 28 12 2 2 2 10 34 0 5
plugins/plugin-auth 27 10 3 0 2 12 28 0 0
drivers/driver-turso (lane) 24 3 6 0 12 3 44 0 0
rest 23 1 0 1 0 21 33 0 1
plugins/plugin-webhooks 20 5 5 0 0 10 33 0 0
cli 19 0 2 2 1 14 22 0 2
services/service-analytics 15 1 3 0 9 2 34 0 0
core (lane) 12 0 2 3 2 5 13 0 0
drivers/driver-mongodb (lane) 10 0 5 0 5 0 12 3 0
plugins/plugin-approvals 10 8 1 0 0 1 10 0 15
drivers/driver-memory (lane) 9 0 1 0 8 0 12 0 0
plugins/plugin-sharing 7 3 2 0 0 2 7 0 0
verify 7 0 2 2 0 3 8 0 2
cloud-connection 6 0 0 0 0 6 6 0 0
metadata (lane) 6 3 1 0 0 2 6 0 0
services/service-datasource 6 0 0 1 0 5 9 0 0
services/service-messaging 6 0 4 0 0 2 8 0 15
metadata-core (lane) 4 0 0 0 0 4 4 25 0
platform-objects (lane) 3 2 0 1 0 0 3 0 17
plugins/plugin-dev 3 1 2 0 0 0 3 0 0
services/service-storage 3 1 0 0 0 2 4 0 0
connectors/connector-mcp 2 0 2 0 0 0 2 0 0
plugins/plugin-email 2 1 1 0 0 0 2 0 4
services/service-i18n 2 0 0 0 0 2 2 0 0
services/service-knowledge 2 2 0 0 0 0 3 0 0
services/service-queue 2 1 1 0 0 0 2 0 0
services/service-sms 2 1 0 0 0 1 2 0 0
types 2 0 0 0 0 2 2 0 0
formula (lane) 1 0 1 0 0 0 1 0 0
plugins/plugin-audit 1 0 0 0 0 1 1 0 5
plugins/plugin-hono-server 1 1 0 0 0 0 1 0 0
triggers/trigger-record-change 1 1 0 0 0 0 1 0 0

The full per-hit list (file, line and cited ids for all 903 messages) is in the os-dev-report comment on the card (5900801368).

Verification

Final head f636206f33: this branch plus a merge of main at 1940afd. That merge touched none of this PR's files.

  • Build. turbo run build over the closure of objectql, service-automation and runtime: 31/31 tasks. Then the whole workspace except docs: 72/72 tasks.
  • Built output. The replaced fragments appear in neither the ESM nor the CJS bundle of the three packages. The new sentences appear in both.
  • Tests, run before the merge:
    • @objectstack/service-automation: 155 files, 1,942 tests passed.
    • @objectstack/runtime: 289 of 290 files passed. The red one was the api-mapping pin above; after its re-pin, api-mapping.test.ts and api-endpoint-step.test.ts pass 59/59.
    • @objectstack/objectql: 336 files, 6,679 tests passed.
    • After the merge, the closure was rebuilt, and the incoming objectql test file plus engine-data-events.test.ts passed 50/50. That is AGENTS.md's scoped re-check: the incoming commits touch spec and metadata-protocol, not these packages or this behaviour.
  • Typecheck. typecheck for the three packages exits 0 each; each includes check:test-typecheck.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands on f636206f33 names 75 commands, and all 75 exit 0. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET on the partial build. It was re-run after the full build: exit 0, 104 entries measured. --ran reconciliation: 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint, narrowed. eslint --no-inline-config --format json over the 8 touched TypeScript files: 8 files reported, 0 errors, 0 warnings, on f636206f33. The narrowing is safe because eslint.config.mjs enables no type-aware linting (no parserOptions.project or projectService anywhere), so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's.
  • NOT MEASURED here (CI's): the Test Core shards, Temporal Conformance, the Dogfood jobs, Build Core's own steps and the workspace type-check lanes.

Acceptance notes

  • Other pins, other producers. service-knowledge's event-sync-data-events.test.ts pins service-knowledge's OWN warning (knowledge-service-plugin.ts, ids 4639 and 4672), not the objectql string. It is untouched here. Runtime also has 5040 citations beyond this order's five sites: endpoint-executor.ts 255 (pinned by endpoint-executor.test.ts 212) and route-ledger.ts 574. Both are census hits left for the burn-down decision.
  • Comments untouched. Comments and docblocks citing the same numbers in these files are not runtime strings and are not changed here.
  • A correction to the round-1 report. It called the gate's printed "808 pinned site(s)" file-id pairs. That figure counts id-bearing string literals; the ledger's pairs were 618.

Generated by Claude Code

… words instead of a tracker number

The objectql data-event warnings, the service-automation resumeAuthority
warning and its three refusal siblings, and the runtime endpoint-step and
mapping hints now say what the cited decision was. The pins that asserted
a number now assert the sentence that carries it. Text only: no code,
field or status changes.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…n warnings and hints

Regenerated with `node scripts/check-doc-authoring.mjs --census-ledger` (the
growth check ran against the checked-in baseline). Fourteen id occurrences
leave the ledger, all from the messages the previous commit rewrote.

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…f the tracker number

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/runtime, @objectstack/service-automation, touching 11 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via /automation/:name/runs/:runId/resume (route, a path literal in warnIfResumeAuthorityUndeclared))
  • content/docs/api/client-sdk.mdx (via automation.resume (sdk, the route ledger binds it to POST /automation/:name/runs/:runId/resume))
  • content/docs/automation/approvals.mdx (via /automation/:name/runs/:runId/resume (route, a path literal in warnIfResumeAuthorityUndeclared))
  • content/docs/automation/flows.mdx (via /automation/:name/runs/:runId/resume (route, a path literal in warnIfResumeAuthorityUndeclared))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via publishDataEvent (symbol, a method of class ObjectQL))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via automation.resume (sdk, the route ledger binds it to POST /automation/:name/runs/:runId/resume))
  • content/docs/releases/v17/17-1.mdx (via /automation/:name/runs/:runId/resume (route, a path literal in warnIfResumeAuthorityUndeclared))

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.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f927864ea056f79d04ad8d62f1a7c13afed31d07 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e7061f462fd95192fb5a1fd9f5d4793193245eeb — the merge of head f636206f33cb9ac0dbc6719b1fc748ea2d451ddd into base f927864ea056f79d04ad8d62f1a7c13afed31d07, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e7061f462fd95192fb5a1fd9f5d4793193245eeb && git checkout e7061f462fd95192fb5a1fd9f5d4793193245eeb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f927864ea056f79d04ad8d62f1a7c13afed31d07 f636206f33cb9ac0dbc6719b1fc748ea2d451ddd && git checkout -B drift-repro f927864ea056f79d04ad8d62f1a7c13afed31d07 && git merge --no-ff f636206f33cb9ac0dbc6719b1fc748ea2d451ddd

node scripts/docs-audit/affected-docs.mjs --json f927864ea056f79d04ad8d62f1a7c13afed31d07

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f927864ea056f79d04ad8d62f1a7c13afed31d07 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f636206f33cb9ac0dbc6719b1fc748ea2d451ddd
Local-runs: none

Inputs read: card #20513 (body and all 5 comments: triage 5879525283, folded runtime producers 5880028473, claim 5900600330, round-1 os-dev-report 5900801368, round-2 os-dev-report 5901412858); PR #20738 body, file list (10 files) and the net diff against merge base 1940afdaf9 (the head is a merge of origin/main at that commit; the merge commit's diff against its branch parent d48c36565f is empty on all 10 paths, so the net diff IS the PR); the check-runs on the head; the cited cards #4639, #4626, #3801, #5561, #3823 and #5040 with their rulings; and the head's own code at every site named. The head was still f636206f33 when read. origin/main moved two commits past the merge base while reading (fbec216e2d); neither touches any of the 10 files, so every judgment below holds against today's origin/main too.

① Derived judgments

Accept-set and public surface: no change, judged right. Every changed line in the four source files is a string-literal fragment inside an existing template or + chain. No error code, no httpStatus, no field name, no exported symbol and no control flow moves: PERMISSION_DENIED / NOT_IMPLEMENTED / 501 and the { object } log context are byte-identical around the rewritten text. The integration negative pins in dispatcher-plugin.endpoint-fallback.integration.test.ts:266-267 (not.toContain('no wiring'), not.toContain('without a policy context')) still mean what they meant, because both phrases survive verbatim in the new sentences.

The 11 rewritten messages, each judged against the cited card's ruling and today's code:

  1. objectql engine.ts data.record.* warning (cites declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639, client realtime declared ≠ enforced:subscribeData 回调把 RealtimeEventPayload 硬铸成 DataEvent,顶层 recordId/changes/id 运行时是 undefined #4626) — RIGHT. client realtime declared ≠ enforced:subscribeData 回调把 RealtimeEventPayload 硬铸成 DataEvent,顶层 recordId/changes/id 运行时是 undefined #4626 ruled producer fulfilment: no fabricated event with recordId: ''; the sentence now says exactly that. declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639 ruled option 2: a predicate write gets its own data.records.* event carrying matched; the sentence says a multi: true write publishes its own event carrying the affected-row count. "Which is a driver defect" is not invented: the same function's docblock on main already says reaching the gate is "a driver bug, because the bulk callers no longer come through here", and the code confirms it (both multi branches route to publishBulkDataEvent; the per-record path takes recordId from input.id ?? result.id or record.id). Nothing an author needs is dropped.
  2. objectql engine.ts data.records.* warning (cites declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639) — RIGHT. The declared ≠ enforced:引擎 multi:true 谓词写入没有 data.record.* 事件,webhook / 知识同步对其静默(REST 批量端点不受影响) #4639 correction (5159177633) struck where? from the payload: the event states object + matched and carries no predicate, with a pin does NOT carry the query predicate. BulkDataEventSchema on the head is { id, type, object, organizationId?, matched, userId?, timestamp }: no records, no where. "It carries no records and no predicate" is true.
  3. service-automation warnIfResumeAuthorityUndeclared (cites automation: the generic run-resume route needs an authorization gate keyed on the suspended node #3801, automation: resumeAuthority defaults to 'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561, automation: the revise-window wait pause is service-owned but type-keyed gating can't see it #3823) — RIGHT. automation: the generic run-resume route needs an authorization gate keyed on the suspended node #3801 ruled the resume gate keyed on the suspended node; "the resume-authority gate" names it by what it is. automation: resumeAuthority defaults to 'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561's step two (flip an undeclared value to fail-closed 'service') HAS landed: resolveResumeAuthority returns resolveDeclaredResumeAuthority(nodeType) ?? RESUME_AUTHORITY_WHEN_UNDECLARED with that constant 'service' as const, and the spec field is .optional() with a TSDoc saying an omission is fail-closed. automation: resumeAuthority defaults to 'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561's body records that inheriting the fail-open 'any' "is exactly how automation: the revise-window wait pause is service-owned but type-keyed gating can't see it #3823 happened", and automation: the revise-window wait pause is service-owned but type-keyed gating can't see it #3823's demonstrated repro was a raw resume walking past resubmit's submitter-only check and audit with no sys_approval_action row; "guessing 'any' is how a raw resume once walked past an approval decision no service had recorded" states that, and keeps the pre-existing wording's meaning. The prescription (declare 'any' or 'service', and which restores the route) is untouched.
  4. same file, refusal log line why (cites automation: resumeAuthority defaults to 'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561) — RIGHT: "an undeclared resumeAuthority resolves to 'service', fail-closed" is the constant above.
  5. same file, undeclaredFix error text (cites automation: resumeAuthority defaults to 'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561) — RIGHT, same fact, prescription intact.
  6. same file, supportsPause: false suspension refusal (cites automation: resumeAuthority defaults to 'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561) — RIGHT: "resolves to 'service', which the generic route refuses" is what refuseGatedResume does.
  7. runtime api-endpoint-step.ts no-policy-context hint (cites 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 E5b) — RIGHT. E5b (feat(runtime): 端点链接线 —— 策略 → 执行,兜底器全上下文(#5040 E5b) #5156) wired policies and execution into one step; the module header states the execution call "sits INSIDE the post-policy branch, which is unreachable without a policy context", and the only composed caller (dispatcher-plugin.ts:1876) passes policy and execution together. "Execution is reachable only on the far side of the policy chain" is that decision.
  8. same file, no-execution-wiring hint (cites 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 E5b) — RIGHT: "supplies it together with the policy context; only a host that mounts the step by hand can leave it out" matches the single composed caller and the exported function.
  9. same file, NOT_IMPLEMENTED message (cites 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040) — RIGHT: "so nothing was executed" is the 501's stated meaning (the test's own comment: matched but not executed must never read as executed and empty).
  10. runtime api-mapping.ts PATH_HINT (cites 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 E7) — RIGHT. E7 (E7(#5040 执行器):翻转 —— publish 硬拒收窄为「不支持子集 + 命名空间门」,声明式端点随 v17 放行执行 #5111) hung the publish gates; E7b (MetadataManager.publishPackage 不经 E7 端点门 —— ADR-0121 D6「匿名须限流」在 Studio 发布路径上不成立 #5189) found the direct-write bypass and kept the runtime backstop. "A declaration that reaches this check was stored without passing that gate (for example through a direct metadata register() call)" is stated on main in both the mapping module's header and endpoint-publish-gate.ts ("a declaration can still reach the store through a direct metadata.register() that never passed publish"). Not an invented reason.
  11. same file, transform refusal (cites 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 §3.4, E7) — RIGHT. Design §3.4: transform stays refused at publish because parsing-then-ignoring is declared-not-enforced. "Rejected at publish rather than parsed and ignored" is that sentence; the prescription ("Drop the key, or shape the value where it is produced") is untouched.

Re-pinned assertions (6, judged each): all pin the decision, none an incidental phrase. resume-authority-declaration.test.ts:84-85: the fail-closed resolution and the walked-past incident. engine-data-events.test.ts:374: the no-records-no-predicate ruling. api-endpoint-step.test.ts:143 and :232: execution behind the policy chain, and wiring supplied with the policy context (E5b). api-mapping.test.ts:230 (expectRefusal mustMention): "rejected at publish rather than parsed and ignored" (§3.4). One-for-one: no assertion added, none removed.

Pins correctly left alone: service-knowledge/src/__tests__/event-sync-data-events.test.ts:166 pins that plugin's OWN warning (knowledge-service-plugin.ts:210, ids 4639/4672), not the objectql string. endpoint-executor.test.ts:212 pins endpoint-executor.ts:255 (#5040 §7-3), a producer outside the card's 11. A repo-wide grep on the head finds no test asserting any removed fragment; every remaining #5040 E5b/E7 hit is a comment or describe title.

Ledger: scripts/doc-authoring-prose-id.baseline.json loses exactly the removed occurrences and nothing else: objectql engine.ts #4626:1 + #4639:2; api-endpoint-step.ts #5040:3 and api-mapping.ts #5040:2 (both entries leave whole); service-automation engine.ts #3801:1 + #3823:1 + #5561:4. That is 14 occurrences in 7 (file, id) pairs, 2 files gone, and the diff has no other hunk. Verified against the source diff's 14 removed id literals. Pairs 618 to 611, occurrences 945 to 931, files 229 to 227 reconcile with the PR body; the gate's own "pinned sites" 808 to 794 is the dev's reading and is consistent with 14 single-id literals (not re-run here; Lint & Repo Gates carries it).

Shipped changeset prose: carries no tracker number; each package paragraph states the same decisions as the strings above and nothing more.

Non-blocking observation, not this PR's: the per-record warning interpolates ${action} and can print data.records.created on the insert path, where no bulk event exists (pre-existing on main, reachable only on a driver defect). The objectql stage of the burn-down touches this file anyway.

② Semver level

.changeset/20513-named-runtime-strings-state-the-decision.md: patch for @objectstack/objectql, @objectstack/service-automation, @objectstack/runtime, all three released (not private). RIGHT: the diff changes runtime strings shipped in three npm tarballs and nothing else, which is a fix in released packages, so patch and never skip-changeset; nothing widens or narrows an accept set, so no minor, no ADR-0087 marker owed. Clause-②: no is on the PR body's second line and in the changeset body; both agree with the diff. Check Changeset on the head: success.

③ Boundary flags

Dev flags (round-2 deviations), each answered:

  • Six pins re-pinned, not five — CORRECT. The order's count of 5 came from a matcher-line grep; api-mapping.test.ts:230 passes the number as a mustMention fragment and pins one of the 11 messages, so it had to move. Six assertion lines, verified: 84, 85, 374, 143, 232, 230.
  • origin/main merge commit on the branch — ACCEPTED. f636206f33 has parents d48c36565f (branch) and 1940afdaf9 (main); its diff against the branch parent is empty on all 10 PR paths; no force push (fast-forward and merge only). The scoped post-merge re-check is what AGENTS.md section 10 prescribes when the incoming commits touch other packages; the head's own check-runs validate the merge.
  • One-for-one pin replacement (standard-text tension) — RIGHT. The six tests already pin prose on purpose (their comments name the text as what is under test); replacing each number with the decision's own words keeps the pin population unchanged and pins the decision, not a phrase.
  • Corrected ledger units — RIGHT. 808/794 are id-bearing string literals ("pinned sites"), 618/611 are (file, id) pairs, 945/931 occurrences; the pairs, occurrences and file counts are verified from the diff above.
  • Clause-②: no in the changeset body — RIGHT. AGENTS.md's Post-Task Checklist step 3 places the PR's Clause-② line in the changeset body, where check:adr-0087-registration reads it; the PR body carries it at line start too.

open_questions: round 2 reports none. Round 1's two questions (how #20513 burns down the remaining 732 ledgered messages: options A to D; which buckets) are ESCALATED, not answered here: this PR is option A's first step only, the PR body carries the census as the maintainer's input, and the card stays open (Part of, not Closes; the Part-of PR must not also close its card check is green). The seat handles that decision separately.

Scope held: no check:doc-authoring reach change, no comment or docblock edits (#20595 / #20234), no ADR-0087 migration-entry prose (#20233), no governed surface in the file list, changed lines 98 (size/s).

Check-runs on f636206f33 at the moment read (31): success 15 (Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check · debt ledger, Type Check · source gates, filter); skipped 3 (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)); in_progress 13 (Lint & Repo Gates, Test Core 1-6, Dogfood Regression Gate 1-3, Temporal Conformance (live PG + MySQL), Type Check · consumer gates, Type Check · workspace); failure 0. Landing waits on the required contexts reaching success; this record does not stand in for them.

Implemented-by: claude/issue-20513-runtime-strings-no-tracker-numbers
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 36651223609 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Neither spec appears in the built console — no @objectstack/spec
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 4 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ongs is refused INVALID_FILTER / 400 on every driver (objectstack-ai#20546) (objectstack-ai#20744)

Fixes objectstack-ai#20546
Clause-②: no (narrowing)

## What this changes

A plain object with no `$`-operator key where a scalar field's value
belongs, for example `where: { amount: { a: 1 } }` on a `number` field,
is now refused with `INVALID_FILTER` / 400. The refusal names the field,
its declared type, the object's keys (never its values) and the path. It
runs before any driver is resolved, on every driver, at the three
positions the engine judges: `where` (object form and `FilterArray`
sugar, on `find` / `findOne` / `count` / `aggregate` / `update` /
`delete` and the judge-only `judgeFilter`), `aggregations[i].filter`,
and `having`.

**Landing site: the number-comparand door's walk, as a second arm. It
adds no second traversal.** Triage said: "If the same walk is the
natural site, it lands serially after that PR, in the same walk. ⛔ No
second traversal of the filter." PR objectstack-ai#20545's walk (`walkCondition` in
`number-comparand-declared-type-door.ts`) is the only filter walk the
engine runs at all three positions with each column's declaration in
hand. It already stood on the exact branch: a field spec with no `$`
key, which it stepped past (`return kept(spec)`). It now asks one
question per field key before the number arm runs:

- `packages/objectql/src/no-operator-object-door.ts` (new) holds the
arm's classification (`holdsScalarValues`), its structure test
(`isNoOperatorObject`) and its words. ⛔ Nothing in it walks a filter.
- `number-comparand-declared-type-door.ts`: the walk's per-key resolver
now supplies two facts, the number arm's meta and the column's
scalar-valued type. The first refusal the walk meets is either arm's.
- `having-filter.ts`: `aggregatedRowColumnTypes` reads each aggregated
column's type off the query. `aggregatedRowColumnClasses` is now derived
from it, so the class and the type are one reading of the query. The
`having` arm needs the type because the `text` class lumps a `json` or
`lookup` groupBy in with a real text column.
- `engine.ts`: the `having` call passes the types; the other hunks are
comments. PR objectstack-ai#20738's warning-text region is untouched.

**Which columns are judged (H3): a closed definition from spec's
classes.** `SCALAR_FILTER_HEAD_TYPES` (spec's published "stores one
scalar value" set, derived from the ADR-0104 value classes; the objectstack-ai#8371
dotted-head verdict reads the same set) with or without `multiple:
true`, plus `MULTI_OPTION_TYPES`. The accepted side is never judged:
relation types (`lookup`, `master_detail`, `user`, `tree`, single or
multiple), structured-JSON types, file and media types (the objectstack-ai#8371
carve-out: a legacy stored value is an inline object), `formula`
(refused one door earlier, `INVALID_FIELD`), undeclared keys, and
unknown types.

## Before, measured on `origin/main` `fbec216e2d`

Through `engine.find` / `engine.aggregate` and `POST
/api/v1/data/:object/query` (both doors answered alike). Three rows
(`amount` 5 / 12 / 30; `owner` u1 / u2 / u1 with u1 in region NA; `meta`
`{a:1}` / `{a:2}` / `{b:1}`). InMemoryDriver, SqlDriver on SQLite
(better-sqlite3), SqlDriver on a live PostgreSQL 16.13:

| position · filter | InMemoryDriver | SQLite | PostgreSQL 16 |
|:--|:--|:--|:--|
| `where` `{ amount: { a: 1 } }` (number, the card) | 200, no rows | 400
`INVALID_FILTER`, the driver's words ("cannot be bound") | same as
SQLite |
| `where` `{ title: { a: 1 } }` (text) | 200, no rows | 400, the
driver's words | 400, the driver's words |
| `where` single select, boolean, date, autonumber, `multiple: true`
select, `multiselect`, `tags` | 200, no rows | 400, the driver's words |
400, the driver's words |
| `where` `{ $not: { amount: { a: 1 } } }` | 200, **every row** | 400
("not one this driver evaluates") | same |
| `where` `{ $or: [{ amount: { a: 1 } }, { amount: 30 }] }` | 200, 1 row
| 400 | 400 |
| `where` sugar `[['amount', '=', { a: 1 }]]` | 200, no rows | 400 | 400
|
| `where` `{ amount: {} }` | 400, the objectstack-ai#5240 words | 400, the objectstack-ai#5240 words
| same |
| `aggregations[1].filter` `{ amount: { a: 1 } }`, `{ title: { a: 1 }
}`, `{ amount: {} }` | 200, count 0 | 200, count 0 | 200, count 0 |
| `having` `{ total: { a: 1 } }` (a `sum`), `{ title: { a: 1 } }` (a
groupBy), `{ total: {} }` | 200, no group | 200, no group | 200, no
group |
| control: `where` `{ owner: { region: 'NA' } }` (lookup;
`master_detail` and a multiple lookup alike) | 200, no rows | 400, the
driver's words | same |
| control: `where` `{ meta: { a: 1 } }` (json) | 200, 1 row (deep
equality) | 400, the driver's words | same |
| control: `where` `{ amount: { $gt: { $field: 'cap' } } }` | 200 | 200,
2 rows | 200, 2 rows |

## After, the same run on this branch

Every non-control row above answers `400 INVALID_FILTER` in the engine's
words on all three drivers, at the path the object sits at
(`where.amount`, `where.$not.amount`, `where.$or[0].amount`,
`aggregations[1].filter.amount`, `having.total`). No read of the object
runs. Every control answers exactly as before: the lookup,
master-detail, multiple-lookup and JSON filters reach the driver as
written, and so do the file field, the `$field` reference, the
undeclared key and the `id` key. Example of the words:

```text
find('rp_ledger_20546'): filter on 'amount' puts an object with no operator key (keys "a") at where.amount, where a value of the declared number field 'amount' belongs. An object with no "$" operator is filter structure, not a value: beneath a field it is a nested-relation condition, which only a relation field (lookup, master-detail, user, tree) can carry, or a whole-value match, which only a JSON-bearing field can hold. A number column holds scalar values — one, or a list of them — so no record can match an object there, and an empty answer would read exactly like a real one. The filter was NOT applied. Compare 'amount' with a value ({ "amount": VALUE }) or an operator ({ "amount": { "$eq": VALUE } }).
```

## Hypotheses (zone 2), which held

- **H1: held, with one refinement.** `lowerWhereFilterArray` is the
seam, and `narrowNumberComparands` is called there on both branches (the
object branch and the lowered array branch). The number door's walk was
number-specific only at its per-field gate
(`numberComparandFieldVerdict(meta) !== 'judged'`), and its `where`
resolver already returned every declared field's type. The text door and
the temporal door each walk too, but neither runs at `having` with a
column declaration, so neither covers every position. The number door's
walk is the one walk that does. The arm rides it, and no traversal was
added.
- **H2: held, and all three positions are reached.** Measured above:
`where` answered per driver, and `aggregations[i].filter` and `having`
answered a silent empty on every driver. Each is pinned.
- **H3: refined.** The closed definition is above. Multi-value fields
were measured on their own: a `multiple: true` select, `multiselect` and
`tags` split exactly as a scalar field does (memory 200 no rows, SQL
400). That includes `{ tags: { 0: 'x' } }`, the spelling the objectstack-ai#8371
multi-value carve-out exists for: the nested-object form does not reach
an array member on InMemoryDriver. So they are judged. A multiple lookup
stays on the relation side. A `{ $field }` reference carries a `$` key,
so it is never this arm's (measured: served 2 rows on SQL, as before).
- **H4: held.** No driver file changes (`git diff fbec216 HEAD --
packages/drivers` is empty). The SQL driver's own `INVALID_FILTER` stays
as defence in depth for driver-direct callers and for the columns this
arm does not judge (the lookup and JSON controls above still meet it).

## Tests

All from `b50627aca9` or from a commit whose non-test source is
byte-identical to it (the last two commits touch only the changeset).

- `pnpm --filter @objectstack/objectql test`: **338 files / 6704 tests
passed**. `test:repo`: 1 file / 5 passed.
- `pnpm --filter @objectstack/objectql typecheck`: exit 0
(`check:test-typecheck` OK, the debt ledger held).
- `pnpm --filter @objectstack/rest typecheck && pnpm --filter
@objectstack/rest test`: **229 files / 4391 passed / 63 skipped** (the
live-dialect cells, no URL set).
- New pin `packages/objectql/src/engine-no-operator-object-door.test.ts`
(17 tests). It uses a recording driver, which is InMemoryDriver's cell
by construction because the arm answers before a driver is resolved. It
covers every scalar class, `{}`, every verb and the judge, `$and` /
`$or` / `$not` paths, sugar, the three REST doors into `findData`, the
per-aggregation filter, `having` (sum, groupBy, max of a date, a month
bucket), the accepted side at all three positions, a `Map` and the
classification GUARD over every `FieldType`.
- New pin `packages/rest/src/data-no-operator-object-door.test.ts`:
SQLite always, PostgreSQL and MySQL where `OS_TEST_POSTGRES_URL` /
`OS_TEST_MYSQL_URL` are set. `where` refusals, the per-aggregation
filter, `having`, and the **two controls** (a lookup nested-relation
filter and a JSON object comparand: the driver is asked, and the answer
is never the arm's). Local run with a live PostgreSQL 16.13: **8 passed
(sqlite 4, live postgres 4) / 4 skipped (mysql, no URL)**. ⚠️ As with
the sibling door suites, no CI job sets these URLs for
`@objectstack/rest`, so the live cells run only locally.
- Consumer suites (downstream of `@objectstack/objectql`):
`service-analytics` 137 files / 3216 passed; `plugin-security` 147 files
/ 3202 passed / 23 skipped. The other downstream consumers are declared
to CI.

**Reverse verification (ablation), from the committed fix.** It ran
through `scripts/ablation-replace.mjs` (WRAP mode, trap-restored). The
anchor `if (facts.scalarType !== null && isNoOperatorObject(value)) {`
was replaced by `if (facts.scalarType === '__ablated_20546__' && …) {`.
On disk the anchor went 1 → 0 and the marker 0 → 1, with blob
`16151b29f6c1` → `0e8acf882100`. Then objectql was rebuilt and
`ablation-dist-preflight` reported the marker present in 4 built files.
Predicted direction: red. Observed: red. The objectql pin went **10
failed / 7 passed**: every refusal case failed, and every control and
GUARD stayed green. The rest pin went **4 failed / 4 passed / 4
skipped**: the `where` and aggregate refusals failed on SQLite and live
PostgreSQL, and the controls stayed green. Restore leg: blob equals HEAD
(`16151b29f6c1`), `git diff HEAD` empty, the whole-tree `git status
--porcelain` empty, rebuilt, `--absent` preflight (marker absent from
all 14 built files), then both pins green again (17 / 17; 8 passed + 4
skipped).

## Gates

`node scripts/pm/dispatch-gates.mjs --commands` at `b50627aca9` derived
65 commands. All were run on `b50627aca9`, and `--ran` reconciles them:
**65 derived, 63 run, 2 NOT-MEASURED, 0 UNRUN**. 63 exit 0, including
`check:adr-0087-registration --base origin/main` (`not-required
(no-migration-prescription)` accepted), `check:changeset-no-major`,
`check:empty-changeset`, `check:doc-authoring`, `check:nul-bytes`,
`check:engine-double-contract`, `check:where-matcher`,
`check:driver-memory-census`, `check:cross-package-test-inputs`,
`check:test-source-alias`, `check:type-check-coverage` and
`check:query-options-erasure`.

- NOT MEASURED: `check:dual-build-cjs-loads` and
`check:type-check-debt`. Reason: each exits 3 (PREREQUISITE NOT MET)
because it reads the built closure of every package, and this box built
only the objectql/rest closure. CI's `Lint & Repo Gates` builds that
closure.
- Driver-related families read before (on `fbec216e2d`, a detached
comparison worktree) and after (on `b50627aca9`):
- `check:where-matcher`: 440 matchers, 440 correct or loudly refusing,
before and after.
- `check:driver-memory-census`: 12 bindings / 2 ruled consumers, before
and after.
- `check:engine-double-contract`: pinned rows 825 → 825 and discovered
files 953 → 953. Test files went 4231 → 4233 and production files 2997 →
2998, which are the two new tests and the new module. No new fake
engine.
- Lint, narrowed and proven: `pnpm exec eslint --no-inline-config
--format json` over the 7 changed `.ts` files, at `b50627aca9`, found
**7 files, 0 errors, 0 warnings**. The checked population comes from
eslint's own config: `calculateConfigForFile` answers
`isPathIgnored=false` for all 7. The file count comes from the JSON
output (7 results). Untouched files cannot change verdict:
`parserOptions.project` and `projectService` are `null` for every file,
so type-aware linting is not enabled and this diff cannot move any
untouched file's lint result.

## Changeset

`.changeset/20546-no-operator-object-on-scalar.md`:
`@objectstack/objectql` `minor`, a BREAKING banner, `Clause-②: no
(narrowing)` and the ADR-0087 marker `not-required
(no-migration-prescription)`, following the objectstack-ai#20501 / objectstack-ai#20545 precedent.
Its "Who is affected" section names a caller that sends the shape to the
in-memory driver: a test suite, a local or embedded deployment on
`InMemoryDriver`, or a flow or hook calling the engine in-process. No
export or published type changes: the door modules are internal, and
`@objectstack/objectql`'s root and `./core` exports are unchanged.

## Acceptance notes

- **Out of scope, reported to the PM, not filed:** the two controls
still answer two ways, because this card's direction keeps them
accepted. `{ owner: { region: 'NA' } }` on a `lookup` gives memory 200
with no rows and SQL 400. `{ meta: { a: 1 } }` on a `json` field gives
memory 200 with 1 row and SQL 400. So does the undeclared `id` key (`{
id: { a: 1 } }`: memory 200 no rows, SQL 400), because the registry's
declared map carries no `id`. Spec's `FilterCondition` declares the
nested-relation form, but no data-path driver serves it. objectstack-ai#20546 is not
the card for that.
- File and media fields keep the objectstack-ai#8371 carve-out and stay unjudged. `{
photo: { url: 'x' } }` answered memory 200 with no rows (on fresh rows)
and SQL 400. A legacy inline value could still match on memory.
- At `where`, a `{}` under a judged column is now answered in the
engine's words instead of each driver's objectstack-ai#5240 words, with the same
`INVALID_FILTER` / 400 envelope. Under a column this arm does not judge,
`{}` keeps the drivers' refusal.
- `findNonNumericComparand` (internal, tests only) still answers the
number arm alone. When the walk's first refusal is the new arm's, it
answers `null`, and its docblock says so.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants