Skip to content

Commit c32ae9b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-7505-readrowbyid-fail-closed
2 parents a9dc673 + 779ace4 commit c32ae9b

16 files changed

Lines changed: 1033 additions & 121 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/driver-mongodb": patch
3+
---
4+
5+
fix(driver-mongodb): the `$contains` family is case-SENSITIVE — the hardcoded `$options: 'i'` is gone (#6682)
6+
7+
**Row-set change for anyone filtering text on MongoDB.** `$contains`,
8+
`$notContains`, `$startsWith` and `$endsWith` translated to a `$regex` with a
9+
hardcoded `$options: 'i'` beside it, which is MongoDB's full-Unicode case fold.
10+
That flag is off all four arms. `{ name: { $contains: 'acme' } }` no longer
11+
returns `ACME Corp`.
12+
13+
This is #4706 **Q2 = A** — the family is case-sensitive on every backend —
14+
arriving at the last driver that was on the wrong side of it. #6518 flipped the
15+
SQL family (`GLOB` on the SQLite dialects, `LIKE` over a binary cast on MySQL,
16+
`LIKE` unchanged on Postgres); `formula`, ObjectQL's `having` and
17+
service-analytics' compilers were already case-exact.
18+
19+
**Both directions of the defect mattered.** The fold OVER-matched — it returned
20+
rows the filter excludes, which on an RLS read scope is over-reach rather than a
21+
loose filter (#3948) — and it folded the whole Unicode range, overshooting the
22+
ASCII-only boundary Q1 = A holds `$icontains` to.
23+
24+
**If you were relying on the fold, write `$icontains`.** It is the deliberate
25+
case-insensitive spelling, implemented on this driver since #6520, and it folds
26+
ASCII only (`café` does not match `CAFÉ`) — the one domain every backend can
27+
deliver.
28+
29+
Unchanged: `escapeRegex`, so the comparand is still matched LITERALLY (`a.b`
30+
matches `a.b`, not `axb`), and `$icontains`, whose fold has always lived in the
31+
pattern rather than in `$options`. Every face of this driver —
32+
`find`/`count`/`update`/`delete` and the aggregation `$match` — routes through
33+
the one `translateFilter`, so there is no second answer to align.
34+
35+
The driver's `FILTER_TEXT_CASES` conformance cell is now CLEARED: the new
36+
server-free suite `mongodb-filter-text-conformance.test.ts` imports the shared
37+
case-set and drives all seventeen cases, rejection rows included, and the DEBT
38+
row for this cell is deleted from `scripts/check-driver-conformance.mjs`.
39+
`driver-memory`'s half of #6682 stays open under the #5499 freeze, so that card
40+
remains open.

‎.changeset/tame-moons-shave.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/metadata': patch
4+
---
5+
6+
Fix commit-revert answering `VERSION_NOT_FOUND` over a row `/history` lists, and the package-level revert route answering 500
7+
8+
**Revert (`revertCommit` / `rollbackMetaItem`).** Both revert callers resolved their overlay repository from the caller's *active organization*, while the publish that recorded the commit routes each draft to the draft's **own** scope (the ADR-0005 / #3115 rule `SysMetadataRepository.listDrafts` states, and `publishPackageDrafts` already follows). So an env-wide artifact — what Studio and AI authoring write — published from a console request carrying an active org stored its `sys_metadata_history` rows at `organization_id = NULL` and was then read back at `organization_id = <org>`: no match, and the revert answered `VERSION_NOT_FOUND: No history row at version 2` for a version the history endpoint lists. The revert now resolves the scope the item's lineage actually lives in (the caller's own overlay first, env-wide second), per item for a batch revert. The same resolution reaches the `#6602` registry heal and the `#4636` package-binding read, which an org-scoped revert of an env-wide row was previously skipping while reporting success.
9+
10+
**`POST /packages/:id/revert`.** The route now answers a declared 4xx instead of 500 (ADR-0112). The cause was entirely in the thrown shape, not the route: `MetadataManager.revertPackage` threw bare `Error`s carrying no `code` or `status`, and `errorFromThrown` — which the route's handler already reaches through one enclosing `catch` — falls back to 500 only when it finds neither. An unknown package id now answers `RESOURCE_NOT_FOUND` / 404 and a never-published package `RESOURCE_CONFLICT` / 409; 500 remains only as the fallback for a genuinely unexpected throw.

‎content/docs/protocol/objectql/query-syntax.mdx‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,13 @@ metacharacters — `{ name: { $icontains: 'a.b' } }` matches `a.b` and not `axb`
339339

340340
One half of the case rules above is still landing: `$contains` /
341341
`$startsWith` / `$endsWith` / `$notContains` are case-**sensitive** by ruling and
342-
are so on the SQL family, but the in-memory driver's query path and MongoDB still
343-
fold them over the whole Unicode range —
344-
[#6682](https://github.com/objectstack-ai/objectstack/issues/6682). Until that
345-
lands, prefer `$icontains` when you *want* a fold rather than relying on
346-
`$contains` being loose on those two backends. The shared standard both halves
347-
are measured against is `FILTER_TEXT_CASES` (`@objectstack/spec/data`).
342+
are so on the SQL family and on MongoDB —
343+
[#6682](https://github.com/objectstack-ai/objectstack/issues/6682) removed the
344+
hardcoded `$options: 'i'` that had folded them there. The in-memory driver's
345+
query and analytics faces still fold over the whole Unicode range. Until that
346+
half lands, prefer `$icontains` when you *want* a fold rather than relying on
347+
`$contains` being loose on that backend. The shared standard both halves are
348+
measured against is `FILTER_TEXT_CASES` (`@objectstack/spec/data`).
348349
</Callout>
349350

350351
### `$regex` — removed
@@ -963,13 +964,15 @@ a user typing `acme` does not find `ACME Corp`. Only `select` / `status` option
963964
*labels* are matched case-insensitively by the expansion itself.
964965

965966
<Callout type="warn">
966-
**Measured today, and it does not match that rule yet.** The `$contains` alignment
967-
is [#5702](https://github.com/objectstack-ai/objectstack/issues/5702), so until it
968-
lands the answer is still the driver's: `SqlDriver` compiles a parameterised
969-
`LIKE '%…%'` and the dialect decides (SQLite folds ASCII, Postgres does not),
970-
`driver-mongodb` folds the full Unicode range through a hardcoded `$options: 'i'`,
971-
and `driver-memory`'s query path matches with a case-insensitive regex. Which
972-
driver you run therefore still changes which rows a search returns. Whether the
967+
**Measured today: one driver still does not match that rule.** The `$contains`
968+
alignment landed in two steps —
969+
[#6518](https://github.com/objectstack-ai/objectstack/issues/6518) made `SqlDriver`
970+
case-exact per dialect (`GLOB` on the SQLite dialects, `LIKE` unchanged on
971+
Postgres, `LIKE` over a binary cast on MySQL), and
972+
[#6682](https://github.com/objectstack-ai/objectstack/issues/6682) removed
973+
`driver-mongodb`'s hardcoded `$options: 'i'`. `driver-memory`'s query path still
974+
matches with a case-insensitive regex, so running your tests on the in-memory
975+
double can still return rows a SQL or MongoDB deployment would not. Whether the
973976
expansion should emit `$icontains` instead of `$contains` — i.e. whether search is
974977
case-insensitive by definition — is a separate question that rides with that issue,
975978
because it can only be answered once both operators mean one thing everywhere.

‎docs/qa/platform-checklist/areas/automation.json‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@
276276
"title": "Every demonstrable flow node type authors in the designer, executes in a run, and surfaces its step in run logs",
277277
"since": "v15",
278278
"status": "active",
279-
"revision": 2,
279+
"revision": 3,
280280
"priority": "P1",
281281
"surface": "mixed",
282282
"personas": [
@@ -323,7 +323,8 @@
323323
"approval chain: PATCH a showcase_expense_report to submitted (approval via showcase_expense_signoff); PATCH a showcase_project budget above 100000 to open showcase_budget_approval, then POST /api/v1/approvals/requests/:id/revise so the run parks at the approval_revise node",
324324
"webhook chain: POST /api/v1/automation/hooks/showcase_inbound_task_webhook/intake with a valid x-objectstack-signature HMAC (secret 'showcase-webhook-secret') to exercise create_record",
325325
"for EACH variant: GET the owning flow's runs (GET /api/v1/automation/<name>/runs + /runs/:runId) and locate a step whose nodeType equals the variant; record run id, step status, and — for suspending types — the paused-then-resumed transition",
326-
"screenshot the Runs panel of one composite run (showcase_project_escalation or showcase_fan_out_notify) showing the container-nested steps"
326+
"screenshot the Runs panel of one composite run (showcase_project_escalation or showcase_fan_out_notify) showing the container-nested steps",
327+
"unknown-type probe (the late-binding contract, ruled status quo in #7545): POST /api/v1/automation a flow whose one non-start node carries type 'bogus_node' — capture the HTTP status and the server log record the registration emits, then POST /api/v1/automation/<that flow>/trigger and read GET /api/v1/automation/<that flow>/runs/:runId"
327328
],
328329
"acceptance": [
329330
{
@@ -355,10 +356,16 @@
355356
"oracle": "api",
356357
"verify": "spot-check one body step per container kind for parentNodeId + regionKind (deep coverage lives in automation.flow-run-step-nesting)",
357358
"evidence": "tagged step excerpts"
359+
},
360+
{
361+
"clause": "an UNKNOWN node type is late-bound, not refused — and a run reads that as PASS BY DESIGN (maintainer ruling on #7545, 2026-08-11: registration-time soft-fail on an unknown node type is the intended posture, 'deliberate late binding with a declaration — not tolerance masking an error'). The contract has four parts and ALL FOUR must hold: (1) POST /api/v1/automation with type 'bogus_node' answers 200 and really registers the flow; (2) the registration emits a located WARN whose message carries the load-bearing phrase 'no registered executor or descriptor' and names the offending flow, with unknownTypes + the FULL knownTypes vocabulary in the record's structured meta slot (#6654 — both lists ride meta, never the message text); (3) triggering that flow answers success:false carrying the NO_EXECUTOR-class message \"No executor registered for node type 'bogus_node'\"; (4) the run is recorded status 'failed', with the offending step status 'failure' and error.code 'NO_EXECUTOR'. Rationale, do not re-litigate: ADR-0018 §M1 has registerFlow() soft-validate node types against the live registry ('warn, don't hard-fail') so a flow authored against a currently-absent plugin still registers, and #4771 is the ordering incident that seam protects — the check fires only once sealNodeTypeVocabulary() has closed the vocabulary, and only for an ENABLED flow",
362+
"oracle": "api",
363+
"verify": "from the unknown-type probe step, cite all four: the 200 + the stored flow read-back; the WARN record (message phrase + meta.unknownTypes + meta.knownTypes); the trigger response success:false + its message; and the run detail status 'failed' with the NO_EXECUTOR step. The 200 at registration is the EXPECTED answer and is never on its own a finding — the finding is any of the other three parts missing",
364+
"evidence": "registration status + WARN log record (message and meta) + trigger response + run-detail excerpt"
358365
}
359366
],
360367
"negative": [
361-
"registering a flow with an unregistered node type (e.g. type 'bogus_node') must be REFUSED at registerFlow / POST /api/v1/automation — the type is validated against the live action registry (ADR-0018), and silent acceptance of an inert node is the #1887 failure shape"
368+
"the unknown-type probe going SILENT is the FAIL this clause guards — registration answering 200 with no WARN at all, or a WARN that drops the phrase 'no registered executor or descriptor' / omits meta.unknownTypes / omits the full meta.knownTypes vocabulary, is the #1887 silent-inert-node failure shape. So is the run side going quiet: a trigger answering success:true, a run not recorded 'failed', or a failed run whose step carries no NO_EXECUTOR error code. ⛔ The 200 itself is NOT a finding — #7545 ruled the registration-time soft-fail intended (ADR-0018 §M1, #4771); a run reporting FAIL on the 200 alone is misreading this item"
362369
],
363370
"traps": [
364371
"wrong-panel",
@@ -375,7 +382,10 @@
375382
"packages/spec/src/automation/approval.zod.ts (APPROVAL_NODE_TYPE, APPROVAL_REVISE_NODE_TYPE)",
376383
"examples/app-showcase/src/coverage.ts (flowNodeTypes + FLOW_NODE_WAIVERS)",
377384
"examples/app-showcase/src/automation/flows/index.ts",
378-
"packages/runtime/src/route-ledger.ts (GET /automation/actions, POST /automation/:name/trigger, GET /automation/:name/runs)"
385+
"packages/runtime/src/route-ledger.ts (GET /automation/actions, POST /automation/:name/trigger, GET /automation/:name/runs)",
386+
"docs/adr/0018-unified-node-action-registry.md §M1 (registerFlow() soft-validates node types against the live registry — 'warn, don't hard-fail')",
387+
"packages/services/service-automation/src/engine.ts (registerFlow's ADR-0018 §M1 comment + the post-seal warn branch; warnUnknownNodeTypes's message and its unknownTypes/knownTypes meta; the NO_EXECUTOR step error in executeNode)",
388+
"#7545 (maintainer ruling 2026-08-11 — status quo upheld: soft-fail at registration is the intended posture, so 200-on-unknown-type is pass-by-design)"
379389
],
380390
"history": [
381391
{
@@ -389,6 +399,12 @@
389399
"date": "2026-08-08",
390400
"change": "pinned enumSource for the variants-freshness ratchet — spec enum drift is caught by the manual check on this item directly",
391401
"ref": "claude/platform-test-checklist-ocwugl"
402+
},
403+
{
404+
"revision": 3,
405+
"date": "2026-08-11",
406+
"change": "recorded the #7545 ruling: the unknown-node-type clause demanded a registration-time REFUSAL the platform never promised, so run #7516 read a by-design 200 as a FAIL. The clause now asserts the real four-part late-binding contract (200 + located WARN with unknownTypes/knownTypes in meta + trigger success:false with NO_EXECUTOR + run 'failed') as pass-by-design, and the negative names silence — not the 200 — as the failure shape",
407+
"ref": "#7545 (ADR-0018 §M1, #4771; QA run #7516)"
392408
}
393409
],
394410
"enumSource": {

0 commit comments

Comments
 (0)